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
| Repo | Stack | Port (local) | หน้าที่ |
|---|---|---|---|
zyra-app | Next.js 16.2 · React 19 · TS 5 · Tailwind v4 · PixiJS 8 | 3000 | UI ทั้งหมด + game canvas ของ Virtual Office (สิ่งเดียวที่ผู้ใช้เจอตรง) |
zyra-api | Go 1.26 · Gin · pgx/v5 · minio-go | 3002 | REST data plane: auth, workspace/map/object, member/invite, mint LiveKit token |
zyra-ws | Go 1.25 · gorilla/websocket · go-redis | 3003 | Real-time control plane: ตำแหน่ง avatar, presence, chat, wave/follow/knock |
zyra-notifications | Go 1.25 · Gin · net/smtp | 3004 | ส่งอีเมลตาม template (OTP, invite, reset password) — service เดียวที่รู้จัก SMTP |
zyra-sfu | LiveKit server v1.13 (Docker) | 7880-7882 | Media plane: forward เสียง/วิดีโอ/แชร์จอ (ไม่มี application code) |
zyra-infra | Terraform | — | GCE VM ของ SFU, k3s + Argo CD GitOps, data plane (AlloyDB/Redis/GCS) |
zyra-service | Shell scripts | — | เข้า prod DB/Redis ผ่าน IAP tunnel + เช็ก schema drift |
zyra-landing | HTML/CSS/JS ล้วน | — | เว็บ marketing — ไม่เรียก service ใดเลย |
zyra-doc | Markdown | — | เอกสารทั้งหมด: plan, glossary, issue, migration guide |
| Repo | Stack | Port (local) | Responsibility |
|---|---|---|---|
zyra-app | Next.js 16.2 · React 19 · TS 5 · Tailwind v4 · PixiJS 8 | 3000 | All UI plus the Virtual Office game canvas — the only thing users touch directly |
zyra-api | Go 1.26 · Gin · pgx/v5 · minio-go | 3002 | REST data plane: auth, workspace/map/object, members/invites, minting LiveKit tokens |
zyra-ws | Go 1.25 · gorilla/websocket · go-redis | 3003 | Real-time control plane: avatar positions, presence, chat, wave/follow/knock |
zyra-notifications | Go 1.25 · Gin · net/smtp | 3004 | Templated email (OTP, invite, password reset) — the only service that knows SMTP |
zyra-sfu | LiveKit server v1.13 (Docker) | 7880-7882 | Media plane: forwards audio/video/screen. No application code here |
zyra-infra | Terraform | — | The SFU's GCE VM, k3s + Argo CD GitOps, shared data plane (AlloyDB/Redis/GCS) |
zyra-service | Shell scripts | — | Reach prod DB/Redis through an IAP tunnel; check schema drift |
zyra-landing | Plain HTML/CSS/JS | — | Marketing site — calls no service at all |
zyra-doc | Markdown | — | All 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)
zyra-ws ไม่แตะ PostgreSQL ตรง — state ถาวรอ่าน/เขียนผ่าน zyra-api
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
gcloudCLI + บัญชี@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-app | BACKEND_URL | เป้าหมายของ rewrite /api/* → ต้องชี้ไป port จริงของ zyra-api (local http://127.0.0.1:3002) |
| zyra-app | NEXT_PUBLIC_SOCKET_URL | URL ของ zyra-ws ที่ browser ต่อตรง (ws://localhost:3003) |
| zyra-api | DATABASE_URL | dev Postgres (ไม่ได้ containerize) — ขอจากทีม |
| zyra-api / zyra-ws | tokenKey | JWT secret — ต้องเป็นค่าเดียวกันทั้งสอง service ไม่งั้น WS join จะ 401 หมด |
| zyra-api / zyra-ws | REDIS_URL | Redis ร่วม (presence, cooldown, pub/sub) |
| zyra-api | AWS_BUCKET_*, AWS_PUBLIC_URL | Cloudflare R2 — ที่เก็บรูป/ไฟล์ทั้งหมด |
| zyra-api | NOTIFICATION_SERVICE_URL | ถ้าว่าง จะ fallback ไปส่ง SMTP ตรงจาก zyra-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
gcloudCLI with an@hpktechnology.comaccount (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
| Repo | Variable | Meaning |
|---|---|---|
| zyra-app | BACKEND_URL | Target of the /api/* rewrite — must point at zyra-api's real port (locally http://127.0.0.1:3002) |
| zyra-app | NEXT_PUBLIC_SOCKET_URL | zyra-ws URL the browser connects to directly (ws://localhost:3003) |
| zyra-api | DATABASE_URL | The dev Postgres (not containerised) — ask the team |
| zyra-api / zyra-ws | tokenKey | JWT secret — must be identical in both services, otherwise every WS join 401s |
| zyra-api / zyra-ws | REDIS_URL | Shared Redis (presence, cooldowns, pub/sub) |
| zyra-api | AWS_BUCKET_*, AWS_PUBLIC_URL | Cloudflare R2 — where every image and file lives |
| zyra-api | NOTIFICATION_SERVICE_URL | If empty, zyra-api falls back to sending SMTP itself |
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_tokencookie ใช้เป็นแค่ธง 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 ที่ไม่ใช่ตำแหน่ง (นั่ง, ทิศทาง)
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_tokencookie is just a presence flag - Two guard layers:
proxy.ts(edge) andcomponents/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).
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 / เส้นแบ่งสิทธิ์
| Guard | Base path | ใช้ที่ไหน |
|---|---|---|
user_guard | /api/user/*, /api/objects | member 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
| Guard | Base path | Used by |
|---|---|---|
user_guard | /api/user/*, /api/objects | Member 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 · stop → moving · stopped · moved |
| presence / state | welcome · joined · left · status_changed · heartbeat |
| social | wave · 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).
| Group | Example messages |
|---|---|
| Position | move_to · stop → moving · stopped · moved |
| Presence / state | welcome · joined · left · status_changed · heartbeat |
| Social | wave · follow · knock / knock_decision |
| Zones & meetings | room_enter · room_exit · section_sync |
| System | capacity_reached (closes the connection) · server_drain before shutdown |
Things to know before editing zyra-ws
- AOI grid of 16×16 tiles —
movedonly 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 * 1000itself — 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 ไม่ผ่าน
| เรื่อง | กฎ |
|---|---|
| UI | Tailwind utility class เท่านั้น — ห้าม import จาก @/components/ui/* (shadcn) ยกเว้น skeleton.tsx / icon.tsx · ห้าม inline style |
| Icon | lucide-react เท่านั้น — ห้าม react-icons / heroicons / inline SVG |
| Component | ค้นหาก่อนสร้างใหม่: components/ui/ → components/admin/ → feature อื่น · ต้องการ variant ให้เพิ่ม prop ห้าม fork · ทุกหน้า admin ต้องใช้ AdminSidebar |
| API separation | member 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.
| Topic | Rule |
|---|---|
| UI | Tailwind utilities only — no imports from @/components/ui/* (shadcn) except skeleton.tsx / icon.tsx; no inline styles |
| Icons | lucide-react only — no react-icons, no heroicons, no ad-hoc inline SVG |
| Components | Search before creating: components/ui/ → components/admin/ → other features. Need a variant? Add a prop, don't fork. Every admin page uses AdminSidebar |
| API separation | Member pages call /api/user/* only — never /api/admin/* |
| Storage | All files and images go to R2 via storage.S3Client — no permanent os.WriteFile, never store /uploads/... paths in the DB |
| Figma | Pull the spec from the Figma MCP first (get_design_context + screenshot) — never guess spacing, colour or type; always use exact hex |
| No overreach | Build only what was asked. Given a Figma node with no flow context, ask first — don't invent a flow around it |
| Docs | Every new doc goes in zyra-doc/ — never create a docs/ folder in a sub-repo |
| ClickUp | Read-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 — ใช้unknownconsole.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 --noEmitandnpm run lintclean before every commit - Go: wrap errors with context —
fmt.Errorf("save user: %w", err)· useslog·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
anyin TypeScript — useunknown- Leave
console.login a PR, orfmt.Printlnin Go production code - Use
<img>, or<a href="/...">for internal links - Ship a
useEffectwith incomplete deps - Hardcode secrets or commit
.env - Log PII (email, password, JWT, OTP)
- Run
prettier --writeacross 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)
| Ref | Environment | Trigger |
|---|---|---|
push develop | dev | auto deploy (web:dev-<sha>) |
push main | uat | auto deploy (web:uat-<sha>) |
tag v* | prod | auto 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>
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)
| Ref | Environment | Trigger |
|---|---|---|
push develop | dev | auto deploy (web:dev-<sha>) |
push main | uat | auto deploy (web:uat-<sha>) |
tag v* | prod | auto 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>
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.md | credential/access model ของ dev & prod DB |
| File | What's in it |
|---|---|
AGENTS.md (root) | Full route table for every service, DB schema, business rules, env vars — the primary reference |
<repo>/README.md | That repo's role in the system and how to run it |
<repo>/AGENTS.md | Internal architecture per repo (zyra-app's is the most detailed on Virtual Office) |
.claude/rules/*.md | 17 rule files by phase (plan → design → develop → test → review → release → monitor) |
zyra-doc/guides/glossary.md | The 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.md | Credential and access model for the dev & prod databases |
zyra-doc/issues/ แตก branch จาก develop แก้ ใส่ test แล้วเปิด PR เข้า develop — จะได้เดินผ่านทุกขั้นของ workflow ครบรอบหนึ่ง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.