หน้าหลัก All guides
ZYRA คู่มือ developer ใหม่ New developer guide
วันแรก · อ่านให้จบก่อนแตะโค้ด Day one · read before touching code

Zyra สำหรับ developer ใหม่

Zyra เป็น monorepo ของ virtual office แบบ 2D — frontend Next.js หนึ่งตัว คุยกับ REST API (Go), WebSocket service (Go) และ LiveKit SFU แยกกันชัดเจน หน้านี้สรุปว่า “อะไรอยู่ที่ไหน”, “รัน local ยังไง” และ “กฎอะไรที่ห้ามพลาด” เพื่อให้คุณส่ง PR แรกได้ภายในวันเดียว

Zyra for new developers

Zyra is a monorepo behind a 2D virtual office — one Next.js frontend talking to a Go REST API, a Go WebSocket service and a LiveKit SFU, each with a strict responsibility. This page covers what lives where, how to run it locally, and the rules you must not miss, so you can land your first PR on day one.

1แผนที่ระบบService map

RepoStackPort (local)หน้าที่
zyra-appNext.js 16.2 · React 19 · TS 5 · Tailwind v4 · PixiJS 83000UI ทั้งหมด + game canvas ของ Virtual Office (สิ่งเดียวที่ผู้ใช้เจอตรง)
zyra-apiGo 1.26 · Gin · pgx/v5 · minio-go3002REST data plane: auth, workspace/map/object, member/invite, mint LiveKit token
zyra-wsGo 1.25 · gorilla/websocket · go-redis3003Real-time control plane: ตำแหน่ง avatar, presence, chat, wave/follow/knock
zyra-notificationsGo 1.25 · Gin · net/smtp3004ส่งอีเมลตาม template (OTP, invite, reset password) — service เดียวที่รู้จัก SMTP
zyra-sfuLiveKit server v1.13 (Docker)7880-7882Media plane: forward เสียง/วิดีโอ/แชร์จอ (ไม่มี application code)
zyra-infraTerraformGCE VM ของ SFU, k3s + Argo CD GitOps, data plane (AlloyDB/Redis/GCS)
zyra-serviceShell scriptsเข้า prod DB/Redis ผ่าน IAP tunnel + เช็ก schema drift
zyra-landingHTML/CSS/JS ล้วนเว็บ marketing — ไม่เรียก service ใดเลย
zyra-docMarkdownเอกสารทั้งหมด: plan, glossary, issue, migration guide
RepoStackPort (local)Responsibility
zyra-appNext.js 16.2 · React 19 · TS 5 · Tailwind v4 · PixiJS 83000All UI plus the Virtual Office game canvas — the only thing users touch directly
zyra-apiGo 1.26 · Gin · pgx/v5 · minio-go3002REST data plane: auth, workspace/map/object, members/invites, minting LiveKit tokens
zyra-wsGo 1.25 · gorilla/websocket · go-redis3003Real-time control plane: avatar positions, presence, chat, wave/follow/knock
zyra-notificationsGo 1.25 · Gin · net/smtp3004Templated email (OTP, invite, password reset) — the only service that knows SMTP
zyra-sfuLiveKit server v1.13 (Docker)7880-7882Media plane: forwards audio/video/screen. No application code here
zyra-infraTerraformThe SFU's GCE VM, k3s + Argo CD GitOps, shared data plane (AlloyDB/Redis/GCS)
zyra-serviceShell scriptsReach prod DB/Redis through an IAP tunnel; check schema drift
zyra-landingPlain HTML/CSS/JSMarketing site — calls no service at all
zyra-docMarkdownAll documentation: plans, glossary, issues, migration guides

ทางเดินของ request

How a request travels

Browser
  ├─ HTTP   ─▶ zyra-app (Next.js) ──rewrite /api/*──▶ zyra-api ──▶ PostgreSQL
  │                                                     │        ╰─▶ Cloudflare R2 (S3)
  │                                                     ╰──HTTP──▶ zyra-notifications ──SMTP──▶
  ├─ WS     ─▶ zyra-ws  ──▶ Redis  (presence / cooldown, TTL-based)
  └─ WebRTC ─▶ zyra-sfu (LiveKit)  (audio · video · screen)
กฎเหล็กของ 2 plane: track เสียง/วิดีโอจริงไหลผ่าน LiveKit เท่านั้น ส่วน state (ใคร mute, ใคร share, ใครอยู่ในโซน) ไหลผ่าน zyra-ws เสมอ เพื่อให้ client ทุกตัวเห็นตรงกันแม้ยังไม่ได้ join media room · และ zyra-ws ไม่แตะ PostgreSQL ตรง — state ถาวรอ่าน/เขียนผ่าน zyra-api
The two-plane rule: real audio/video tracks flow through LiveKit only; state (who is muted, who is sharing, who is in a zone) always flows through zyra-ws so every client agrees even before joining the media room. And zyra-ws never touches PostgreSQL directly — durable state goes through zyra-api.

2ตั้งเครื่องและรัน localLocal setup

ต้องมีบนเครื่อง

  • Go 1.26+ (zyra-api) และ Go 1.25+ (ws, notifications) · Node.js 20+ · Docker Desktop
  • gcloud CLI + บัญชี @hpktechnology.com (เฉพาะเวลาต้องแตะ prod DB)
  • ขอไฟล์ .env ของแต่ละ repo จากทีม — ไม่มีอยู่ใน git

วิธีรันที่ทีมใช้จริง

Docker รันแค่ infra ที่ตั้งเองยาก (LiveKit + Redis) ส่วน app/api/ws รัน native เพื่อให้ hot-reload ทำงาน

# 1) infra: LiveKit SFU + Redis
docker compose up -d

# 2) แต่ละ terminal แยกกัน
cd zyra-api && go run .            # :3002  (หรือ `air` เพื่อ hot-reload)
cd zyra-ws  && go run main.go      # :3003
cd zyra-notifications && go run .  # :3004
cd zyra-app && npm install && npm run dev   # :3000 (Turbopack)

อยากให้ backend อยู่ใน container แทน: docker compose --profile backend up -d --build (native กับ container ใช้ port เดียวกันไม่ได้ — ปิดฝั่งหนึ่งก่อน)

Env ที่สำคัญ

Repoตัวแปรความหมาย
zyra-appBACKEND_URLเป้าหมายของ rewrite /api/* → ต้องชี้ไป port จริงของ zyra-api (local http://127.0.0.1:3002)
zyra-appNEXT_PUBLIC_SOCKET_URLURL ของ zyra-ws ที่ browser ต่อตรง (ws://localhost:3003)
zyra-apiDATABASE_URLdev Postgres (ไม่ได้ containerize) — ขอจากทีม
zyra-api / zyra-wstokenKeyJWT secret — ต้องเป็นค่าเดียวกันทั้งสอง service ไม่งั้น WS join จะ 401 หมด
zyra-api / zyra-wsREDIS_URLRedis ร่วม (presence, cooldown, pub/sub)
zyra-apiAWS_BUCKET_*, AWS_PUBLIC_URLCloudflare R2 — ที่เก็บรูป/ไฟล์ทั้งหมด
zyra-apiNOTIFICATION_SERVICE_URLถ้าว่าง จะ fallback ไปส่ง SMTP ตรงจาก zyra-api
!
Port ในเอกสารกับใน .env ไม่ตรงกันได้ — ค่า default ในโค้ดคือ api 3001 / ws 3004 แต่ layout ที่ทีมใช้ local คือ api 3002 / ws 3003 / notifications 3004 (มาจาก PORT ใน .env) ยึด .env + header ของ docker-compose.yml เป็นหลัก

Migration

zyra-api สร้างเฉพาะ 10 ตารางหลักตอน start (inline DDL ใน internal/database/postgres.go) ไฟล์ใน zyra-api/migrations/*.sql ไม่ auto-run — ต้อง apply ด้วยมือด้วย psql

Prerequisites

  • Go 1.26+ (zyra-api) and Go 1.25+ (ws, notifications) · Node.js 20+ · Docker Desktop
  • gcloud CLI with an @hpktechnology.com account (only needed to reach prod DB)
  • Ask the team for each repo's .env — they are not in git

How the team actually runs it

Docker runs only the infra that is painful natively (LiveKit + Redis); app/api/ws run natively so hot-reload works.

# 1) infra: LiveKit SFU + Redis
docker compose up -d

# 2) one terminal each
cd zyra-api && go run .            # :3002  (or `air` for hot reload)
cd zyra-ws  && go run main.go      # :3003
cd zyra-notifications && go run .  # :3004
cd zyra-app && npm install && npm run dev   # :3000 (Turbopack)

Want the backend containerised instead? docker compose --profile backend up -d --build — native and container cannot share a port, so stop one side first.

Env vars that matter

RepoVariableMeaning
zyra-appBACKEND_URLTarget of the /api/* rewrite — must point at zyra-api's real port (locally http://127.0.0.1:3002)
zyra-appNEXT_PUBLIC_SOCKET_URLzyra-ws URL the browser connects to directly (ws://localhost:3003)
zyra-apiDATABASE_URLThe dev Postgres (not containerised) — ask the team
zyra-api / zyra-wstokenKeyJWT secret — must be identical in both services, otherwise every WS join 401s
zyra-api / zyra-wsREDIS_URLShared Redis (presence, cooldowns, pub/sub)
zyra-apiAWS_BUCKET_*, AWS_PUBLIC_URLCloudflare R2 — where every image and file lives
zyra-apiNOTIFICATION_SERVICE_URLIf empty, zyra-api falls back to sending SMTP itself
!
Docs and .env disagree on ports. Code defaults are api 3001 / ws 3004, but the local layout the team uses is api 3002 / ws 3003 / notifications 3004 (from PORT in each .env). Trust .env and the header of docker-compose.yml.

Migrations

zyra-api only bootstraps the 10 core tables on start (inline DDL in internal/database/postgres.go). Files in zyra-api/migrations/*.sql are not auto-run — apply them by hand with psql.

3zyra-app — ไม่ใช่ Next.js ที่คุณเคยเจอzyra-app — not the Next.js you know

app/                route export เท่านั้น — ห้ามมี JSX หรือ state ในนี้
views/<feature>/    UI จริง: hero-<feature>.tsx (top level) + components/
components/         ของที่ reuse ข้าม feature (admin/admin-sidebar.tsx, app-navbar, auth-guard)
lib/api/            REST call — แยก /api/user/* กับ /api/admin/* ชัดเจน
lib/auth/           session.ts · register.ts · forgot-password.ts
stores/             Zustand + Dexie (draft persistence)
hooks/              use-user-guard, use-admin-guard, ...
zyra-engine/        PixiJS engine ของ Virtual Office — import จาก @/zyra-engine
i18n/ messages/     next-intl — locale เก็บใน cookie zyra_locale (URL ไม่เปลี่ยน)

Auth / session

  • Access token เก็บ in-memory เท่านั้น ผ่าน lib/auth/session.ts — ห้ามลง localStorage
  • Refresh token เป็น httpOnly cookie · zyra_token cookie ใช้เป็นแค่ธง presence
  • Guard 2 ชั้น: proxy.ts (edge) + components/auth-guard.tsx (client) — หน้า public ใหม่ต้องเพิ่มใน PUBLIC_PATHS ทั้งสองไฟล์

zyra-engine (Virtual Office canvas)

แยกเป็น system: InputSystem · CameraSystem · PlayerSystem · HitboxSystem · SitSystem · PlacementSystem · ค่าคงที่หลัก TILE_SIZE=32, PLAYER_SPEED=120 px/s

การเดินเป็น path-based: engine ยิง onPathStarted → client ส่ง move_to (path) → server คำนวณ duration_ms แล้ว broadcast moving → peer interpolate ตาม path → จบเดินส่ง stop · move/moved แบบเดิมเหลือใช้เฉพาะ state ที่ไม่ใช่ตำแหน่ง (นั่ง, ทิศทาง)

บั๊กตระกูล “ตัวละครวาร์ป / นั่งแล้วหลุด / peer เห็นไม่ตรงกับเรา” เป็น bug family ที่ลึกมาก — อ่าน skill vo-desync-debug และ zyra-doc/plan/VO-Movement-V2/ ก่อนเริ่ม debug ทุกครั้ง
app/                route exports only — no JSX, no state in here
views/<feature>/    the real UI: hero-<feature>.tsx (top level) + components/
components/         cross-feature reuse (admin/admin-sidebar.tsx, app-navbar, auth-guard)
lib/api/            REST calls — /api/user/* strictly separate from /api/admin/*
lib/auth/           session.ts · register.ts · forgot-password.ts
stores/             Zustand + Dexie (draft persistence)
hooks/              use-user-guard, use-admin-guard, ...
zyra-engine/        the Virtual Office PixiJS engine — import from @/zyra-engine
i18n/ messages/     next-intl — locale in the zyra_locale cookie (URL never changes)

Auth / session

  • The access token lives in memory only, via lib/auth/session.ts — never localStorage
  • The refresh token is an httpOnly cookie; the zyra_token cookie is just a presence flag
  • Two guard layers: proxy.ts (edge) and components/auth-guard.tsx (client) — a new public page must be added to PUBLIC_PATHS in both files

zyra-engine (the Virtual Office canvas)

Split into systems: InputSystem · CameraSystem · PlayerSystem · HitboxSystem · SitSystem · PlacementSystem. Core constants: TILE_SIZE=32, PLAYER_SPEED=120 px/s.

Movement is path-based: the engine fires onPathStarted → the client sends move_to (the path) → the server computes duration_ms and broadcasts moving → peers interpolate along the path → on arrival the client sends stop. Legacy move/moved is now only for non-positional state (sitting, direction).

The "avatar warps / slides off a chair / peers see something else" bug family runs eight rounds deep — read the vo-desync-debug skill and zyra-doc/plan/VO-Movement-V2/ before debugging any of it.

4zyra-api — 3 ชั้นเสมอzyra-api — always three layers

ทุก endpoint ใหม่ต้องมี Handler → Service → SQL ใน service (ไม่มี ORM หรือ repository layer แยก)

  • Handler — รับ input, validate ผิวเผิน, ตอบ JSON
  • Service — business logic + transaction + sentinel error (ErrNotFound, ErrDuplicate)
  • Model — struct ข้าม layer อยู่ใน internal/model/
// response envelope — ต้องใช้ทุก endpoint
model.APIResponse{ Status: 200, Message: "success", Data: &logon, Token: token }

// transaction pattern
tx, err := s.db.BeginTx(ctx, pgx.TxOptions{})
if err != nil { return fmt.Errorf("begin tx: %w", err) }
defer tx.Rollback(ctx)
// ...
return tx.Commit(ctx)

Middleware / เส้นแบ่งสิทธิ์

GuardBase pathใช้ที่ไหน
user_guard/api/user/*, /api/objectsmember page, lobby, virtual office
admin_guard/api/admin/*admin dashboard เท่านั้น (role_ ∈ ADMIN, SYSADMIN)

ไฟล์อัปโหลดทุกชนิดขึ้น Cloudflare R2 ผ่าน storage.S3Client แล้วเก็บ public URL ลง DB — ห้ามเขียนไฟล์ถาวรลง filesystem ของ API server

Every new endpoint is Handler → Service → SQL inside the service. There is no ORM and no separate repository layer.

  • Handler — take input, validate at the surface, return JSON
  • Service — business logic, transactions, sentinel errors (ErrNotFound, ErrDuplicate)
  • Model — cross-layer structs live in internal/model/
// response envelope — required on every endpoint
model.APIResponse{ Status: 200, Message: "success", Data: &logon, Token: token }

// transaction pattern
tx, err := s.db.BeginTx(ctx, pgx.TxOptions{})
if err != nil { return fmt.Errorf("begin tx: %w", err) }
defer tx.Rollback(ctx)
// ...
return tx.Commit(ctx)

Middleware / the permission line

GuardBase pathUsed by
user_guard/api/user/*, /api/objectsMember pages, lobby, virtual office
admin_guard/api/admin/*Admin dashboard only (role_ ∈ ADMIN, SYSADMIN)

Every upload goes to Cloudflare R2 through storage.S3Client, and only the public URL is stored in the DB. Never write permanent files to the API server's filesystem.

5zyra-ws และ media planezyra-ws and the media plane

ต่อที่ GET /ws?workspace_id=&token= (JWT ต้องออกโดย zyra-api ด้วย tokenKey เดียวกัน) ทุก message เป็น JSON envelope { "type": ..., "payload": ... } ยกเว้น moved ความถี่สูงที่ส่งเป็น binary frame (type byte 0x01)

กลุ่มตัวอย่าง message
ตำแหน่งmove_to · stopmoving · stopped · moved
presence / statewelcome · joined · left · status_changed · heartbeat
socialwave · follow · knock / knock_decision
zone / โซนประชุมroom_enter · room_exit · section_sync
ระบบcapacity_reached (ปิด connection) · server_drain ก่อน shutdown

สิ่งที่ควรรู้เวลาแก้ zyra-ws

  • AOI grid 16×16 tile — broadcast moved แค่ neighbourhood 3×3 cell (พื้นที่ราว 48×48 tile) เฉพาะพื้นที่เปิด; ในห้องส่วนตัวใช้ broadcast ระดับห้อง
  • Server คำนวณ duration_ms = pathDistancePx / 120 * 1000 เอง — client ไม่ได้กำหนด
  • State ทุกอย่างอยู่ใน Redis พร้อม TTL (vo:presence:* 35s, vo:last_pos:* 7 วัน, cooldown ของ wave/knock ฯลฯ)
  • ทุกครั้งที่ยกเลิกการเดิน ต้องบอกอีกฝ่าย ไม่งั้นจะเกิดอาการ warp-back / เดินทะลุกำแพงในสายตา peer

zyra-sfu ไม่รู้ business rule อะไรเลย (single-presenter, force-mute, capacity) — บังคับกฎที่ zyra-ws/zyra-api เสมอ

Connect to GET /ws?workspace_id=&token= — the JWT must be minted by zyra-api with the same tokenKey. Messages use a JSON envelope { "type": ..., "payload": ... }, except high-frequency moved updates, which are binary frames (type byte 0x01).

GroupExample messages
Positionmove_to · stopmoving · stopped · moved
Presence / statewelcome · joined · left · status_changed · heartbeat
Socialwave · follow · knock / knock_decision
Zones & meetingsroom_enter · room_exit · section_sync
Systemcapacity_reached (closes the connection) · server_drain before shutdown

Things to know before editing zyra-ws

  • AOI grid of 16×16 tiles — moved only reaches the 3×3 cell neighbourhood (~48×48 tiles), and only on the open floor; private rooms use room-level broadcast
  • The server computes duration_ms = pathDistancePx / 120 * 1000 itself — clients don't get to decide
  • All state lives in Redis with TTLs (vo:presence:* 35s, vo:last_pos:* 7 days, wave/knock cooldowns, …)
  • Whenever a walk is cancelled, you must tell the other side — otherwise peers see warp-backs and avatars standing in walls

zyra-sfu knows no business rules at all (single presenter, force mute, capacity). Enforce those in zyra-ws/zyra-api.

6กฎบ้านนี้ (.claude/rules/)House rules (.claude/rules/)

กฎเหล่านี้ถูกบังคับใน review — ละเมิดคือ PR ไม่ผ่าน

เรื่องกฎ
UITailwind utility class เท่านั้น — ห้าม import จาก @/components/ui/* (shadcn) ยกเว้น skeleton.tsx / icon.tsx · ห้าม inline style
Iconlucide-react เท่านั้น — ห้าม react-icons / heroicons / inline SVG
Componentค้นหาก่อนสร้างใหม่: components/ui/components/admin/ → feature อื่น · ต้องการ variant ให้เพิ่ม prop ห้าม fork · ทุกหน้า admin ต้องใช้ AdminSidebar
API separationmember page เรียก /api/user/* เท่านั้น — ห้ามแตะ /api/admin/*
Storageไฟล์/รูปทุกอย่างขึ้น R2 ผ่าน storage.S3Client — ห้าม os.WriteFile ถาวร, ห้ามเก็บ path /uploads/... ลง DB
Figmaก่อนเขียน UI ต้องดึง spec จาก Figma MCP (get_design_context + screenshot) — ห้ามเดา spacing/สี/ฟอนต์ ใช้ exact hex เสมอ
No-overreachทำเฉพาะที่สั่ง — ได้ Figma node ใหม่แต่ไม่รู้ว่าอยู่ flow ไหน ต้องถามก่อน ห้ามเดาแล้วสร้าง flow เอง
เอกสารdoc ใหม่ทุกชิ้นลง zyra-doc/ — ห้ามสร้าง docs/ ใน sub-repo
ClickUpอ่านได้อย่างเดียว · ห้ามแตะ status เด็ดขาด (เป็นสิทธิ์ของ PM/QA)

These are enforced in review — breaking one means the PR doesn't land.

TopicRule
UITailwind utilities only — no imports from @/components/ui/* (shadcn) except skeleton.tsx / icon.tsx; no inline styles
Iconslucide-react only — no react-icons, no heroicons, no ad-hoc inline SVG
ComponentsSearch before creating: components/ui/components/admin/ → other features. Need a variant? Add a prop, don't fork. Every admin page uses AdminSidebar
API separationMember pages call /api/user/* only — never /api/admin/*
StorageAll files and images go to R2 via storage.S3Client — no permanent os.WriteFile, never store /uploads/... paths in the DB
FigmaPull the spec from the Figma MCP first (get_design_context + screenshot) — never guess spacing, colour or type; always use exact hex
No overreachBuild only what was asked. Given a Figma node with no flow context, ask first — don't invent a flow around it
DocsEvery new doc goes in zyra-doc/ — never create a docs/ folder in a sub-repo
ClickUpRead-only, and never touch status — that belongs to PM/QA

7Code styleCode style

ทำ

  • Prettier: ไม่มี semicolon · double quotes · trailing comma · width 100 · indent 2 · (x) => x
  • npx tsc --noEmit และ npm run lint ผ่านก่อน commit ทุกครั้ง
  • Go: wrap error ด้วย context fmt.Errorf("save user: %w", err) · ใช้ slog · defer tx.Rollback(ctx)
  • ตัวแปรที่จงใจไม่ใช้ ให้ prefix _
  • <Image> จาก next/image และ <Link> สำหรับลิงก์ภายใน
  • Commit แบบ Conventional: feat(api): · fix(app): (lowercase, ≤72 ตัวอักษร)

ห้าม

  • any ใน TypeScript — ใช้ unknown
  • console.log ค้างใน PR · fmt.Println ใน Go production
  • <img> · <a href="/..."> ในลิงก์ภายใน
  • useEffect ที่ deps ไม่ครบ
  • hardcode secret หรือ commit .env
  • log PII (email, password, JWT, OTP)
  • รัน prettier --write ทั่ว repo — repo ยัง format ไม่สะอาดทั้งหมด จะกลบ diff ของคุณ ให้จัด format แค่ไฟล์ที่คุณแก้

Do

  • Prettier: no semicolons · double quotes · trailing commas · width 100 · 2-space indent · (x) => x
  • Run npx tsc --noEmit and npm run lint clean before every commit
  • Go: wrap errors with context — fmt.Errorf("save user: %w", err) · use slog · defer tx.Rollback(ctx)
  • Prefix deliberately unused variables with _
  • Use <Image> from next/image and <Link> for internal links
  • Conventional Commits: feat(api): · fix(app): (lowercase, ≤72 chars)

Don't

  • any in TypeScript — use unknown
  • Leave console.log in a PR, or fmt.Println in Go production code
  • Use <img>, or <a href="/..."> for internal links
  • Ship a useEffect with incomplete deps
  • Hardcode secrets or commit .env
  • Log PII (email, password, JWT, OTP)
  • Run prettier --write across the repo — it isn't format-clean yet and it will bury your diff. Format only the files you touched

8Git workflow & deployGit workflow & deploy

feat/<name>  ──▶  develop  ──▶  main  ──▶  tag v<x.y.z>
   local          dev        uat          prod
                auto deploy  auto deploy  auto deploy (gated)
RefEnvironmentTrigger
push developdevauto deploy (web:dev-<sha>)
push mainuatauto deploy (web:uat-<sha>)
tag v*prodauto deploy (web-prod:<tag>)
git checkout develop && git pull origin develop
git checkout -b feat/<feature-name>    # หรือ fix/<name>
# ...ทำงาน แล้ว push branch ตัวเอง...
git push origin feat/<feature-name>
!
ห้าม commit/push ตรงเข้า develop หรือ main · ห้าม merge ข้ามขั้น (feat/*main) · ห้ามแตก feature branch จาก main · push เข้า main = deploy uat ทันที และ tag = deploy prod ทันที จึงต้อง ขอ confirm จากผู้ใช้ก่อนทุกครั้ง

ก่อน release เช็ก: go test ./... ผ่าน · npm run lint + next build ผ่าน · env ครบบน target · migration รันก่อน deploy · /healthz ตอบ 200 หลัง deploy

feat/<name>  ──▶  develop  ──▶  main  ──▶  tag v<x.y.z>
   local          dev        uat          prod
                auto deploy  auto deploy  auto deploy (gated)
RefEnvironmentTrigger
push developdevauto deploy (web:dev-<sha>)
push mainuatauto deploy (web:uat-<sha>)
tag v*prodauto deploy (web-prod:<tag>)
git checkout develop && git pull origin develop
git checkout -b feat/<feature-name>    # or fix/<name>
# ...work, then push your own branch...
git push origin feat/<feature-name>
!
Never commit or push straight to develop or main, never skip a step (feat/*main), and never branch a feature off main. Pushing to main deploys uat immediately and a tag deploys prod immediately — so both need explicit confirmation from the user first.

Release gate: go test ./... green · npm run lint + next build green · env vars complete on the target · migrations applied before deploy · /healthz returns 200 afterwards.

9TestTesting

Go (zyra-api / ws)

Table-driven test + testify · mock DB ด้วย interface — ห้ามต่อ PostgreSQL จริงใน unit test · test ทุก sentinel error · เป้า coverage internal/service/* ≥ 80%

Go (zyra-api / ws)

Table-driven tests with testify; mock the DB behind an interface — never hit a real PostgreSQL in a unit test. Cover every sentinel error. Target: internal/service/* ≥ 80%

TypeScript (zyra-app)

npm run test (Vitest) · vi.mock เสมอ ห้ามยิง /api/* จริง · test happy path + error path ของทุก function · เป้า lib/*.ts ≥ 80%

TypeScript (zyra-app)

npm run test (Vitest); always vi.mock — never call real /api/*. Cover the happy path and the error path of every function. Target: lib/*.ts ≥ 80%

E2E

npm run e2e (Playwright) · npm run e2e:report ดูผล

E2E

npm run e2e (Playwright) · npm run e2e:report to read results

Virtual Office

บั๊ก real-time ต้อง verify ด้วย 2 client จริง — unit test จับ desync ไม่ได้ · ดูสูตรใน zyra-doc/plan/Real-time-Engine/

Virtual Office

Real-time bugs need two live clients to verify — unit tests can't catch a desync. Recipes in zyra-doc/plan/Real-time-Engine/

10อ่านต่อRead next

ไฟล์มีอะไร
AGENTS.md (root)route table เต็มทุก service, DB schema, business rule, env vars — แหล่งอ้างอิงหลัก
<repo>/README.mdบทบาทของ repo นั้นในระบบ + วิธีรัน
<repo>/AGENTS.mdรายละเอียดสถาปัตยกรรมภายใน repo (zyra-app มีเรื่อง Virtual Office ละเอียดสุด)
.claude/rules/*.mdกฎ 17 ข้อแยกตาม phase (plan → design → develop → test → review → release → monitor)
zyra-doc/guides/glossary.mdคำศัพท์ทางการ — ใช้คำให้ตรงกับที่นี่เวลาเขียน doc/PR
zyra-doc/plan/<Feature>/spec + technical design ของแต่ละฟีเจอร์ (Chat, VirtualOffice, UserGuide, ...)
zyra-doc/issues/บันทึกบั๊ก/ปัญหาที่เคยเจอ
zyra-doc/guides/prod-db-access.mdcredential/access model ของ dev & prod DB
FileWhat's in it
AGENTS.md (root)Full route table for every service, DB schema, business rules, env vars — the primary reference
<repo>/README.mdThat repo's role in the system and how to run it
<repo>/AGENTS.mdInternal architecture per repo (zyra-app's is the most detailed on Virtual Office)
.claude/rules/*.md17 rule files by phase (plan → design → develop → test → review → release → monitor)
zyra-doc/guides/glossary.mdThe canonical vocabulary — match it in docs and PRs
zyra-doc/plan/<Feature>/Spec plus technical design per feature (Chat, VirtualOffice, UserGuide, …)
zyra-doc/issues/Recorded bugs and incidents
zyra-doc/guides/prod-db-access.mdCredential and access model for the dev & prod databases
PR แรกที่ดี: หยิบ bug เล็ก ๆ จาก zyra-doc/issues/ แตก branch จาก develop แก้ ใส่ test แล้วเปิด PR เข้า develop — จะได้เดินผ่านทุกขั้นของ workflow ครบรอบหนึ่ง
A good first PR: pick a small bug from zyra-doc/issues/, branch off develop, fix it with a test, and open a PR into develop — that walks you through the whole workflow once.