Architecture Document + ADR-E001
LeaveTrack v1.0 — stack, data model, API design, and the Day 0 mock-auth decision.
Raw files, for copying into your project
sample-project/docs/arch-docs/ARCH-LeaveTrack-v1.0.md · ADR-LeaveTrack-E001-Auth-Session-Strategy.md — traces to PRD-LeaveTrack-v1.0.md
Stack
- Frontend/Backend: Next.js 16 (App Router), TypeScript
- Database: Postgres via Supabase
- Auth (Day 0): mock cookie session — see ADR-LeaveTrack-E001 below. Real provider wired in Day 1.
- Deployment target: Vercel (app), Supabase (managed Postgres)
Rationale: the team already runs Next.js + Supabase on other projects (fits nexus's Day 0 mock-auth-mock-data pattern directly); no NFR here demands anything heavier.
Data Model
users (id, name, email, role[employee|manager|hr_admin], manager_id nullable FK -> users.id)
leave_types (id, name, tracks_balance boolean) -- seeded: Vacation, Sick, Unpaid
leave_policies (id, user_id FK, leave_type_id FK, accrual_per_month numeric)
leave_balances (id, user_id FK, leave_type_id FK, balance numeric, updated_at)
leave_requests (id, user_id FK, leave_type_id FK, start_date, end_date, reason,
status[pending|approved|rejected], decided_by FK -> users.id nullable,
decided_at nullable, is_deleted boolean default false, created_at)
audit_log (id, leave_request_id FK, actor_id FK, action, occurred_at)
leave_requests.is_deleted— soft delete only, per BR-04 (3-year retention). No hard-delete path exists anywhere in the codebase.users.manager_idis the sole source of truth for the scoped-visibility rule (US-04). A manager's queue query always filtersWHERE manager_id = :current_user_id.
API Design (selected)
| Method | Route | Auth | Notes |
|---|---|---|---|
| POST | /api/leave-requests | Employee | US-01. Validates date range server-side (AC2). |
| GET | /api/leave-requests?scope=mine | Employee | US-02 balance context. |
| GET | /api/leave-requests?scope=team | Manager | US-04 — server-side filter on manager_id, never client-side. |
| POST | /api/leave-requests/:id/decision | Manager | US-03. 403 if requester is not a direct report (AC3). |
| GET | /api/audit/:userId | HR Admin | US-05. Reads soft-deleted-excluded, never hard-deleted, rows. |
| POST | /api/jobs/accrual | System (cron) | US-07. Idempotent — see ADR-E001's sibling decision below. |
Error format: { success: false, error: { code, message } } on every non-2xx response, per AGENTS.md convention.
Security
- Every manager-scoped endpoint enforces the
manager_idfilter in the query itself, not in application logic that could be bypassed — this is the direct implementation of PRD NFR "enforced server-side, never client-side only." - Session cookie is httpOnly, 8h expiry (Day 0 mock auth pattern; unchanged shape when real auth replaces it in Day 1).
Deployment Topology
Single Next.js app on Vercel, single Supabase Postgres instance (pooled connection, not direct). Monthly accrual job (US-07) runs as a scheduled Vercel Cron hitting /api/jobs/accrual.
Open Questions
Should leave_policies support mid-year policy changes (e.g., a promotion changing accrual rate)? Flagged for Sprint 2 — not required for Q3 launch (BRD scope).
ADR-LeaveTrack-E001 — Auth & Session Strategy
Status: Accepted
Context
Day 0 (/scaffold) must produce a running app with zero external credentials — no real auth provider configured yet. But the app still needs role-gating (Employee / Manager / HR Admin) from commit one, since every page in the UI shell renders differently per role.
Options Considered
| Option | Pros | Cons |
|---|---|---|
| 1. No auth at all on Day 0 — every route public, role picked via a UI toggle | Simplest possible Day 0 | Doesn't exercise the route-guard pattern the real app needs; Day 1 becomes a bigger jump |
| 2. Mock cookie session — login accepts any input, sets an httpOnly cookie holding a mock role, route guard reads it | Exercises the exact route-guard shape the real app will use; Day 1 just swaps the cookie's origin, not the guard logic | Slightly more Day 0 setup than option 1 |
| 3. Mock JWT in localStorage | Familiar pattern | Not readable server-side without extra plumbing; server components can't gate on it directly |
Decision
Option 2 — mock cookie session, per nexus's Day 0 convention (/scaffold's "Mock auth pattern"). Cookie holds mock-role, 8h expiry. Route guard (Next.js proxy.ts) reads the cookie and redirects unauthenticated requests to login. Server components read the cookie directly for the current session — no Context provider needed.
Consequences
- Positive: Day 1's real-auth task (wiring an actual provider) only replaces how the cookie gets set — the guard, the server-component read pattern, and every role-gated page stay untouched.
- Negative: until Day 1, "login" accepts any input — this must never ship past the scaffold stage, and AGENTS.md flags it explicitly so no developer mistakes it for production auth.