Files
OG/docs/03-architecture.md
T
2026-07-30 14:35:52 +08:00

4.7 KiB

System Architecture

SOP Governance Tool — Stage 1

1. Stack (fixed — do not substitute)

Layer Choice Why
Frontend + backend Next.js 14+ (App Router, TypeScript) One codebase for pages and API routes; industry standard
Database / Auth / Storage Supabase (Postgres + Auth + Storage + RLS) Hands a beginner the hard parts; RLS becomes Stage-2 tenant isolation
Hosting Vercel Push-to-deploy; free tier sufficient
AI Anthropic API (claude-sonnet for drafting/translation) Called ONLY from server routes
Styling Tailwind CSS + shadcn/ui Fast, consistent, well-documented

2. High-Level Diagram

[Browser: Admin/Editor desktop UI · Staff mobile UI]
        │  (HTTPS)
        ▼
[Vercel: Next.js App]
  ├─ Server Components / Pages ──────────► reads via Supabase client (RLS enforced)
  ├─ Route Handlers /api/* ─────────────► writes + workflow logic (service role where needed)
  │     └─ /api/ai/* ───────────────────► Anthropic API (server-side key)
  ▼
[Supabase]
  ├─ Postgres (all tables, RLS policies)
  ├─ Auth (email/password, JWT with role + org_id claims)
  └─ Storage (bucket: sop-photos)

3. Key Architectural Decisions

AD-1: org_id everywhere, single org today. Every table carries org_id. Stage 1 seeds exactly one org row and all data references it. RLS policies already filter by org_id. Stage 2 = add org signup + billing; data layer barely changes. Do NOT skip this "because we only have one company."

AD-2: Roles live in a profiles table, mirrored into the JWT. profiles(id → auth.users.id, org_id, role, department_id, preferred_language, full_name, active). A Postgres function copies role/org_id into JWT claims (custom access token hook) so RLS can check them cheaply.

AD-3: Reads through RLS; privileged writes through route handlers. Simple reads (my SOPs, dashboard queries) use the anon/user Supabase client and rely on RLS. Workflow mutations (submit/approve/publish/assign/acknowledge) go through /api/* route handlers that (a) re-verify the session and role, (b) run the transition logic in one transaction via the service-role client, (c) write the audit_log row in the same transaction. This keeps state-machine rules in ONE place.

AD-4: Published content is a frozen snapshot. sop_versions.content stores the full JSON of all sections at publish time. The editor works on the draft row; publish copies it into an immutable version row. Acknowledgements and translations reference the version, never the live draft.

AD-5: AI calls are stateless, logged, and cheap-by-default. One route per capability (/api/ai/draft, /api/ai/translate). Each call: validate role + rate limit → build prompt from templates in lib/ai/prompts.ts → call Anthropic → validate/parse JSON output (retry once on parse failure) → log tokens to ai_log. Use a smaller/cheaper model for translation, a stronger one for drafting.

AD-6: No client-side secrets, no localStorage for auth. Supabase handles session cookies. ANTHROPIC_API_KEY and SUPABASE_SERVICE_ROLE_KEY exist only in Vercel env vars and are only imported in server files.

4. Directory Layout

/app
  /(auth)/login
  /(admin)/dashboard  /sops  /sops/[id]/edit  /approvals  /users  /audit
  /(staff)/my-sops  /my-sops/[id]
  /api/sops/... /api/ai/... /api/ack ...
/components        (ui/ = shadcn, sop/ = editor pieces, staff/ = mobile viewer)
/lib
  supabase/ (browser client, server client, service client)
  ai/ (anthropic client, prompts, parsers)
  workflow.ts (status machine: canTransition(), nextVersion())
  audit.ts (writeAudit())
/db
  schema.sql  policies.sql  seed.sql
/docs (this spec pack)
CLAUDE.md

5. Environments

  • Local: supabase start (local Postgres) or a free "dev" Supabase project; .env.local for keys.
  • Production: Supabase project in Singapore region (PDPA/data-residency story), Vercel production.
  • One branch (main) is fine for a solo dev; Vercel preview deploys per push give you a test URL.

6. Security Checklist (must all be true before real staff data goes in)

  • RLS enabled on every table; anonymous access returns zero rows
  • Staff role cannot read drafts, other users' profiles beyond names, or the audit log
  • All /api routes verify session server-side (no trust of client-sent role)
  • Storage bucket: authenticated read, Editor+ write, no public listing
  • Service-role key and Anthropic key absent from client bundle (next build + search)
  • Supabase daily backups enabled (dashboard toggle) and a restore has been tested once