# 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