| title | Data model |
|---|
Cloudflare D1 is the signed-in persistence layer. The schema lives in
src/lib/db-schema.sql (source of truth — do not duplicate it here).
This page documents the table groups, the guest parity contract, and
the non-obvious constraints.
22 tables, grouped by concern:
| Group | Tables |
|---|---|
| Identity | users |
| Resumes | resumes, tailored_resumes, cover_letters |
| Jobs | job_applications, saved_job_searches, saved_job_shortlist, job_discovery_alerts, company_watches |
| Evidence | achievement_evidence, interview_stories, skills_roadmaps, fit_scores, outreach_emails |
| Stash | stash_entries |
| Tokens / payments | token_balances, token_transactions, payments |
| Apply-agent | application_queue, application_receipts, profile_answers, recruiter_reply_events |
For column-level detail, read src/lib/db-schema.sql. The single
migration is migrations/0001_initial_schema.sql.
Every entity that exists in D1 has a parallel implementation in
src/lib/local-storage.ts under rt-* keys. The contract:
- Same interface.
getItems/setItemsare generic helpers that mirror the DB layer interface, so server actions can swap between DB and localStorage based ongetCurrentUserId(). - Guest writes never touch D1. Server actions throw if
userIdis null on a write path — see the security audit for the historical unauthenticated-write leak that motivated this. - Guest reads return empty/null for cross-user queries.
WHERE user_id IS NULLqueries must return empty arrays or null for unauthenticated requests, never another guest's data. - Migration on sign-in.
src/lib/actions/migration-actions.tsbulk-inserts localStorage data into D1 with the user's id on first sign-in, then clears thert-*keys.
See ADR-0002 for why this dual layer exists.
token_balances (per-user balance, atomic debit), token_transactions
(per-change ledger with balance_after snapshot), payments (Dodo
payment idempotency — dodo payment_id as PK, INSERT OR IGNORE
prevents double-credit). The atomic debit is a single SQL statement:
UPDATE token_balances
SET balance = balance - 1, total_used = total_used + 1, updated_at = unixepoch()
WHERE user_id = ? AND balance >= 1
RETURNING balancePayment insert + token credit are wrapped in a single D1 transaction
(see the security audit — the original
implementation had a no-transaction bug where a failed creditTokens
left a payment recorded but tokens never granted, and the idempotency
check prevented retry).
application_queue— review-first queue entries with readiness state (ready_to_submit,blocked,submitted, etc.).application_receipts— submitted/failed receipts with final visible ATS field snapshots and machine-readable failure codes.profile_answers— reusable sensitive profile answers learned from successful submitted receipts (file/upload fields ignored).recruiter_reply_events— inbound recruiter replies routed via Cloudflare Email Routing to per-user forwarding addresses.
See ADR-0003 for the review-first boundary and the apply-agent CLI runbook for the HTTP API.
Wrangler D1 migrations. Apply with wrangler d1 migrations apply rolepatch
(production) — never run migrations from agent sessions without explicit
approval. The single migration is
migrations/0001_initial_schema.sql; src/lib/db-schema.sql is the
canonical schema source (idempotent CREATE TABLE IF NOT EXISTS).