Web browser -> Cloudflare -> app.<domain> -> CloudFront + S3 web SPA
Admin browser -> Cloudflare -> admin.<domain> -> CloudFront + S3 admin SPA
Browser auth -> Cloudflare -> auth.<domain> -> API Gateway -> Auth Lambda -> Cognito EMAIL_OTP
iOS app -> Cloudflare -> auth.<domain> -> API Gateway -> Auth Lambda -> Cognito EMAIL_OTP
iOS + web sync/API -> Cloudflare -> api.<domain> -> API Gateway -> Lambda backend -> Postgres
AI chat v2 worker -> Cloudflare -> api.<domain> -> API Gateway -> backend Lambda -> detached chat worker Lambda -> model provider APIs
Agent bootstrap -> Cloudflare -> auth.<domain> -> API Gateway -> Auth Lambda -> Cognito EMAIL_OTP -> API key
Apex fallback -> Cloudflare -> <domain> -> CloudFront redirect -> app.<domain>
The repository currently implements three public service surfaces:
app.<domain>for the web SPA inapps/webadmin.<domain>for the admin SPA inapps/adminauth.<domain>for OTP login, session refresh, token refresh, and agent OTP bootstrap inapps/authapi.<domain>for the main backend, sync API, AI chat transport, and the machine-facing agent API inapps/backend
The apex <domain> is an optional CloudFront redirect to app.<domain>.
apps/backend: Hono backend for human clients and agentsapps/auth: Hono auth service for browser, native, and agent OTP flowsapps/web: React + Vite web app with IndexedDB local storageapps/admin: React + Vite admin app with server-side analytics data loadingapps/ios: SwiftUI iOS app with SQLite local storagedb/migrations: PostgreSQL schema, security, and runtime-role migrationsdb/views: SQL views applied after migrationsinfra/aws: CDK stack for networking, database, auth, API, web hosting, CI/CD, backups, and monitoringinfra/docker: local migration container assets
infra/aws/lib/web.tsdeploys the web app to S3 behind CloudFront.- SPA routing is handled by serving
index.htmlfor403and404. - The web app derives
api.<domain>/v1andauth.<domain>from the current hostname unless local overrides are provided.
infra/aws/lib/admin.tsdeploys the admin app to S3 behind CloudFront.- The admin app derives
api.<domain>/v1andauth.<domain>from the currentadmin.<domain>hostname. - The supported admin browser entrypoints are
http://localhost:3001andhttps://admin.<domain>. - Admin report data is loaded from backend-owned
/v1/admin/*routes, not from direct browser access to Postgres.
infra/aws/lib/gateways/auth-gateway.tsdeploys a dedicated API Gateway + Lambda for auth.- The auth Lambda runs in the VPC, reads its own DB secret, and talks to Cognito.
- Browser and native sign-in use the same passwordless email OTP foundation, but different session handling:
- Web uses signed cookies plus session refresh.
- iOS receives tokens in JSON and stores refresh credentials locally.
- Agents use a separate OTP bootstrap flow that ends with a long-lived API key.
infra/aws/lib/gateways/api-gateway.tsdeploys the main API Gateway and two backend Lambda entrypoints:- a buffered backend Lambda for normal JSON endpoints
- a detached worker Lambda for backend-owned chat runs
- The backend Lambda runs in the VPC, reads the backend DB secret, verifies Cognito ID tokens, and can optionally read model-provider secrets.
- The detached chat worker Lambda uses the same backend runtime environment, but it is invoked asynchronously by the main backend Lambda and is not published through API Gateway.
- API Gateway predeclares the public route tree, including the system/root documents,
agent,workspaces,agent-api-keys,admin,cards,guest-auth,chat,sync, and the single global metrics routeGET /v1/global/snapshot.
infra/aws/lib/database.tsdeploys PostgreSQL 18 on RDS in private subnets.- A separate migration runner Lambda applies SQL migrations and views, then configures runtime role passwords for
backend_appandauth_app. - Monitoring, backups, and CI/CD are provisioned from the same CDK stack.
- The migration runner connects with the database owner credentials and is the only component that applies schema migrations and view updates.
backend_appis the main runtime database role forapps/backend.- It has the runtime grants needed for human API traffic, sync, cards, workspaces, account deletion, and agent SQL execution.
- Its row-level-security policies are the canonical runtime policies for app data in
org,content, andsync.
auth_appis the narrower runtime database role forapps/auth.- It is limited to the auth and bootstrap paths needed for OTP verification, token refresh side effects, user bootstrap, and initial device/workspace creation.
- Legacy
appwas the original shared runtime database role before the split intobackend_appandauth_app.- It was removed by
db/migrations/0025_remove_legacy_app_role.sql. - Older migrations still mention
appbecause they are historical schema steps. - New migrations must not add grants, policies, or dependencies for
app.
- It was removed by
apps/backend/src/server/app.ts mounts ten route modules:
system: runtime discovery, source-discovery probes, health, session/account inspection, and account deletionagent: machine-facing source-discovery aliases, workspace bootstrap, and SQL endpointsworkspaces: list/create/select workspaces, workspace lifecycle actions, and human-session agent API key managementadmin: admin session and report query endpointscards: workspace card query and tag summary endpointsglobalSnapshot: one public route,GET /v1/global/snapshotguestAuth: guest session and upgrade endpointschatTranscriptions: transcription upload endpointchat: backend-owned chat control-plane endpointssync: offline-first push, pull, bootstrap, and review-history endpoints
The backend is Hono-based in local dev and in Lambda. In local dev it serves on http://localhost:8080/v1.
- React 19 SPA in
apps/web - Local source of truth is IndexedDB, not the network
- IndexedDB stores:
cardscardTagsdecksreviewEventsworkspaceSettingsoutboxmeta
- The app loads
/me, resolves the selected workspace, persists cloud-link metadata locally, then syncs - Browser auth recovery is automatic through
POST /api/refresh-sessionon the auth host
- SwiftUI app in
apps/ios - Local source of truth is SQLite via
LocalDatabase - Persistence is split into dedicated stores such as
CardStore,DeckStore,OutboxStore,SyncApplier, andWorkspaceSettingsStore - Cloud linking stores Cognito refresh credentials locally and refreshes ID tokens through the auth service
- The iOS app mirrors backend FSRS scheduling and local AI-tool behavior closely
- Production auth mode is Cognito ID-token verification.
- Local dev can run with
AUTH_MODE=none. - Browser login flow:
POST /api/send-codeonauth.<domain>POST /api/verify-code- auth service sets signed browser session cookies
- web app calls
GET /v1/me
- Native login flow:
POST /api/send-codePOST /api/verify-code- auth service returns
idTokenandrefreshTokenin JSON - iOS refreshes through
POST /api/refresh-token
GET /v1/andGET /v1/agentreturn the machine discovery envelope.- Agents authenticate through auth-service OTP endpoints:
POST /api/agent/send-codePOST /api/agent/verify-code
- Successful agent verification creates a long-lived API key stored in
auth.agent_api_keys. - Agent requests use
Authorization: ApiKey .... - Each API key stores its own selected workspace, independent of the human session selection.
- The backend always runs request auth before loading app data.
- The first authenticated human request auto-provisions:
org.user_settings- a default
Personalworkspace if none exists - the selected workspace pointer
- Agent API keys also auto-provision or auto-select a workspace when possible.
The Postgres schema is split by responsibility:
org: users, workspaces, memberships, user settingscontent: cards, decks, review eventssync: devices, hot-state change metadata, workspace sync metadata, idempotency ledgerauth: agent OTP challenges, API keys, OTP rate-limit state, deleted subjectssecurity: helpers for runtime database context
Important tables and responsibilities:
org.user_settings: human profile metadata and selected workspaceorg.workspaces: workspace metadata and persisted FSRS scheduler settingsorg.workspace_memberships: workspace access controlcontent.cards: card state, including persisted FSRS fieldscontent.decks: saved deck filterscontent.review_events: append-only review historysync.devices: known client devices per workspacesync.applied_operations_current: bounded idempotency ledger for batched push requestssync.hot_changes: compact hot-state change metadata for mutable rootssync.workspace_sync_metadata: per-workspace sync retention and bootstrap metadataauth.agent_api_keys: long-lived terminal/agent connections
The sync contract is the same across web, iOS, and Android:
- Write locally first.
- Add a record to the local outbox.
- Push pending operations to
POST /v1/workspaces/:workspaceId/sync/push. - Bootstrap hot current state from
POST /v1/workspaces/:workspaceId/sync/bootstrapwhen the workspace has not been hydrated locally yet. - Pull hot-state deltas from
POST /v1/workspaces/:workspaceId/sync/pull. - Pull append-only review history from
POST /v1/workspaces/:workspaceId/sync/review-history/pullin the background. - Advance the local hot/history cursors and clear only acknowledged outbox rows.
Implemented sync behavior:
- Operations are scoped to a workspace and device.
- Push is idempotent through
sync.applied_operations_current. - Push is processed in batches instead of one transaction per operation.
- Cards, decks, and workspace scheduler settings use last-writer-wins metadata:
clientUpdatedAtlastModifiedByDeviceIdlastOperationId
- Media asset registry metadata and tombstones can be emitted by pull/bootstrap reads when clients opt into
includeMediaAssets, but clients create or update media assets only through the media upload API. File bytes move only through the media upload/download URL APIs. - Cards reference logical
content.media_assetsby media asset id; those workspace-scoped rows enforce access and point to deduplicated backend-internalcontent.media_blobsrows that own SHA-256, MIME type, size, and object storage keys. - Review events are append-only and deduplicated by
(workspace_id, device_id, client_event_id). - Review events use the normal
POST /v1/workspaces/:workspaceId/sync/pushcontract for both live and historical submissions. Forreview_eventoperations,clientUpdatedAtmust equalpayload.reviewedAtClient. - Mutable state and review history are synchronized through separate lanes:
- hot mutable roots use
sync.hot_changesplus direct materialization from canonical tables - review history uses
content.review_events.review_sequence
- hot mutable roots use
- Local sync state keeps separate cursors for hot state and review history instead of one global checkpoint.
- Progress, streaks, and chart day buckets are derived from
content.review_events.reviewed_at_clientin the requested timezone, not from server ingest time.
Client card content and media byte transfer are intentionally separate:
- Card Markdown references media as
fcasset:<mediaAssetId>. - Clients sync logical
media_assetsrows through the normal sync/bootstrap reads, but create or update media assets through the media upload API instead of the JSON sync outbox. - File bytes do not move through the JSON sync outbox. Clients use local upload/download queue rows as operational state, request backend-authorized signed upload/download URLs from the media API, and transfer bytes through those transient URLs.
- Signed URLs are short-lived transport credentials, not persisted user content. Public CDN delivery remains a future optimization; current first-party clients request authorized signed URLs from the backend.
- Each client may keep a local device cache keyed by blob
sha256. Queue rows and cache entries are local operational state and are not synced as user content.
- FSRS scheduling is persisted on the card row and workspace scheduler settings.
- Review submission is compute-on-write:
- append a review event
- recompute the card schedule
- update the card snapshot
- emit sync changes
- Review eligibility is compute-on-read: due cards use
due_at <= now(), new cards usedue_at IS NULL, and future or malformed due values stay out of the active queue. - Backend, iOS, and Android maintain mirrored scheduling logic and state validation.
- The web app computes review submissions locally too, but it does not keep a fourth standalone scheduler copy in
apps/web. - Instead, the web review flow reuses the backend scheduler module directly from
apps/backend/src/scheduling/index.tswhen it needs the next localdueAtand FSRS state.
There is no separate background scheduler worker.
All current first-party clients use the backend-owned chat surface:
GET /v1/chatPOST /v1/chatDELETE /v1/chatPOST /v1/chat/stopPOST /v1/chat/transcriptions- The backend-owned chat stores canonical sessions, transcript items, and detached run state in Postgres under the
aischema. - V2 is intentionally server-owned: the backend owns session ids, run state, transcript history, run execution, cancellation, and recovery behavior.
POST /v1/chatcreates a persisted run and asynchronously dispatches the detached worker.GET /v1/chatreads the persisted snapshot and is the canonical recovery path for v2 progress.- The model-facing tools a chat run can call come from the shared agent tool registry, apart from the generated-image tool the chat appends itself for a signed-in run; see agent tool surfaces.
The machine-facing API is intentionally narrower than the human app API:
- discovery at
GET /v1/andGET /v1/agent - account context at
GET /v1/agent/me - workspace listing and bootstrap at
GET/POST /v1/agent/workspaces - workspace selection at
POST /v1/agent/workspaces/{workspaceId}/select - conversational review at
POST /v1/agent/reviews/next,/reveal, and/submit, mirrored by MCPnext_review_card,reveal_answer, andsubmit_review; see the review contract - SQL reads at
POST /v1/agent/sql/query(read-only) and SQL writes atPOST /v1/agent/sql/execute; both accept an optionalworkspaceIdin the JSON body, the same argument as thesql_queryandsql_executeMCP tools - one reference guide at a time at
GET /v1/agent/guide/{topic}, serving the same bodies as the MCPget_guidetool - conventional document probes at
GET /v1/openapi.json,GET /v1/swagger.json,GET /v1/agent/openapi.json, andGET /v1/agent/swagger.json; all four return the same concise source-discovery JSON linking to the open-source repository and the relevant backend and auth route source files, not an OpenAPI document
The SQL dialect is not full PostgreSQL. It is a constrained contract implemented in apps/backend/src/aiTools.
How this surface relates to the MCP server and the in-app chat, and which contract modules all three share, is agent tool surfaces.
- Cloudflare is the public DNS and edge TLS layer.
- RDS is private and reachable only from the VPC.
- Lambda functions connect with dedicated security groups and per-service secrets.
- Browser OTP state is HMAC-signed and expires after 3 minutes.
- Browser session-authenticated unsafe API requests require CSRF validation plus allowed-origin checks.
- The backend derives the browser CSRF token from the session JWT using a dedicated secret, so it does not need extra CSRF database state.
- PostgreSQL row-level security is enabled for the runtime tables.
- Backend and auth DB access is scoped through runtime roles plus
app.user_idandapp.workspace_idsession settings. - Runtime role split:
backend_appis the default runtime role for application reads and writes.auth_appis the restricted runtime role for auth-service flows.- Legacy
appis not a valid runtime role anymore and exists only in historical migrations.
- web:
http://localhost:3000 - backend:
http://localhost:8080/v1 - auth:
http://localhost:8081
These local URLs are wired into the app config and the agent/auth discovery helpers.