Skip to content

Latest commit

 

History

History
299 lines (235 loc) · 16.9 KB

File metadata and controls

299 lines (235 loc) · 16.9 KB

Architecture

System overview

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 in apps/web
  • admin.<domain> for the admin SPA in apps/admin
  • auth.<domain> for OTP login, session refresh, token refresh, and agent OTP bootstrap in apps/auth
  • api.<domain> for the main backend, sync API, AI chat transport, and the machine-facing agent API in apps/backend

The apex <domain> is an optional CloudFront redirect to app.<domain>.

Monorepo shape

  • apps/backend: Hono backend for human clients and agents
  • apps/auth: Hono auth service for browser, native, and agent OTP flows
  • apps/web: React + Vite web app with IndexedDB local storage
  • apps/admin: React + Vite admin app with server-side analytics data loading
  • apps/ios: SwiftUI iOS app with SQLite local storage
  • db/migrations: PostgreSQL schema, security, and runtime-role migrations
  • db/views: SQL views applied after migrations
  • infra/aws: CDK stack for networking, database, auth, API, web hosting, CI/CD, backups, and monitoring
  • infra/docker: local migration container assets

Deployment architecture

Web

  • infra/aws/lib/web.ts deploys the web app to S3 behind CloudFront.
  • SPA routing is handled by serving index.html for 403 and 404.
  • The web app derives api.<domain>/v1 and auth.<domain> from the current hostname unless local overrides are provided.

Admin

  • infra/aws/lib/admin.ts deploys the admin app to S3 behind CloudFront.
  • The admin app derives api.<domain>/v1 and auth.<domain> from the current admin.<domain> hostname.
  • The supported admin browser entrypoints are http://localhost:3001 and https://admin.<domain>.
  • Admin report data is loaded from backend-owned /v1/admin/* routes, not from direct browser access to Postgres.

Auth service

  • infra/aws/lib/gateways/auth-gateway.ts deploys 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.

Backend API

  • infra/aws/lib/gateways/api-gateway.ts deploys 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 route GET /v1/global/snapshot.

Database and operations

  • infra/aws/lib/database.ts deploys PostgreSQL 18 on RDS in private subnets.
  • A separate migration runner Lambda applies SQL migrations and views, then configures runtime role passwords for backend_app and auth_app.
  • Monitoring, backups, and CI/CD are provisioned from the same CDK stack.

Database roles

  • The migration runner connects with the database owner credentials and is the only component that applies schema migrations and view updates.
  • backend_app is the main runtime database role for apps/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, and sync.
  • auth_app is the narrower runtime database role for apps/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 app was the original shared runtime database role before the split into backend_app and auth_app.

Backend runtime structure

apps/backend/src/server/app.ts mounts ten route modules:

  • system: runtime discovery, source-discovery probes, health, session/account inspection, and account deletion
  • agent: machine-facing source-discovery aliases, workspace bootstrap, and SQL endpoints
  • workspaces: list/create/select workspaces, workspace lifecycle actions, and human-session agent API key management
  • admin: admin session and report query endpoints
  • cards: workspace card query and tag summary endpoints
  • globalSnapshot: one public route, GET /v1/global/snapshot
  • guestAuth: guest session and upgrade endpoints
  • chatTranscriptions: transcription upload endpoint
  • chat: backend-owned chat control-plane endpoints
  • sync: 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.

Client architecture

Web app

  • React 19 SPA in apps/web
  • Local source of truth is IndexedDB, not the network
  • IndexedDB stores:
    • cards
    • cardTags
    • decks
    • reviewEvents
    • workspaceSettings
    • outbox
    • meta
  • The app loads /me, resolves the selected workspace, persists cloud-link metadata locally, then syncs
  • Browser auth recovery is automatic through POST /api/refresh-session on the auth host

iOS app

  • 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, and WorkspaceSettingsStore
  • 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

Authentication model

Human users

  • Production auth mode is Cognito ID-token verification.
  • Local dev can run with AUTH_MODE=none.
  • Browser login flow:
    1. POST /api/send-code on auth.<domain>
    2. POST /api/verify-code
    3. auth service sets signed browser session cookies
    4. web app calls GET /v1/me
  • Native login flow:
    1. POST /api/send-code
    2. POST /api/verify-code
    3. auth service returns idToken and refreshToken in JSON
    4. iOS refreshes through POST /api/refresh-token

Agents

  • GET /v1/ and GET /v1/agent return the machine discovery envelope.
  • Agents authenticate through auth-service OTP endpoints:
    • POST /api/agent/send-code
    • POST /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.

User bootstrap

  • The backend always runs request auth before loading app data.
  • The first authenticated human request auto-provisions:
    • org.user_settings
    • a default Personal workspace if none exists
    • the selected workspace pointer
  • Agent API keys also auto-provision or auto-select a workspace when possible.

Data model

The Postgres schema is split by responsibility:

  • org: users, workspaces, memberships, user settings
  • content: cards, decks, review events
  • sync: devices, hot-state change metadata, workspace sync metadata, idempotency ledger
  • auth: agent OTP challenges, API keys, OTP rate-limit state, deleted subjects
  • security: helpers for runtime database context

Important tables and responsibilities:

  • org.user_settings: human profile metadata and selected workspace
  • org.workspaces: workspace metadata and persisted FSRS scheduler settings
  • org.workspace_memberships: workspace access control
  • content.cards: card state, including persisted FSRS fields
  • content.decks: saved deck filters
  • content.review_events: append-only review history
  • sync.devices: known client devices per workspace
  • sync.applied_operations_current: bounded idempotency ledger for batched push requests
  • sync.hot_changes: compact hot-state change metadata for mutable roots
  • sync.workspace_sync_metadata: per-workspace sync retention and bootstrap metadata
  • auth.agent_api_keys: long-lived terminal/agent connections

Offline-first sync

The sync contract is the same across web, iOS, and Android:

  1. Write locally first.
  2. Add a record to the local outbox.
  3. Push pending operations to POST /v1/workspaces/:workspaceId/sync/push.
  4. Bootstrap hot current state from POST /v1/workspaces/:workspaceId/sync/bootstrap when the workspace has not been hydrated locally yet.
  5. Pull hot-state deltas from POST /v1/workspaces/:workspaceId/sync/pull.
  6. Pull append-only review history from POST /v1/workspaces/:workspaceId/sync/review-history/pull in the background.
  7. 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:
    • clientUpdatedAt
    • lastModifiedByDeviceId
    • lastOperationId
  • 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_assets by media asset id; those workspace-scoped rows enforce access and point to deduplicated backend-internal content.media_blobs rows 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/push contract for both live and historical submissions. For review_event operations, clientUpdatedAt must equal payload.reviewedAtClient.
  • Mutable state and review history are synchronized through separate lanes:
    • hot mutable roots use sync.hot_changes plus direct materialization from canonical tables
    • review history uses content.review_events.review_sequence
  • 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_client in the requested timezone, not from server ingest time.

Client media transfer

Client card content and media byte transfer are intentionally separate:

  • Card Markdown references media as fcasset:<mediaAssetId>.
  • Clients sync logical media_assets rows 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.

Scheduling architecture

  • 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 use due_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.ts when it needs the next local dueAt and FSRS state.

There is no separate background scheduler worker.

AI chat architecture

All current first-party clients use the backend-owned chat surface:

  • GET /v1/chat
  • POST /v1/chat
  • DELETE /v1/chat
  • POST /v1/chat/stop
  • POST /v1/chat/transcriptions
  • The backend-owned chat stores canonical sessions, transcript items, and detached run state in Postgres under the ai schema.
  • V2 is intentionally server-owned: the backend owns session ids, run state, transcript history, run execution, cancellation, and recovery behavior.
  • POST /v1/chat creates a persisted run and asynchronously dispatches the detached worker.
  • GET /v1/chat reads 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.

Agent API architecture

The machine-facing API is intentionally narrower than the human app API:

  • discovery at GET /v1/ and GET /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 MCP next_review_card, reveal_answer, and submit_review; see the review contract
  • SQL reads at POST /v1/agent/sql/query (read-only) and SQL writes at POST /v1/agent/sql/execute; both accept an optional workspaceId in the JSON body, the same argument as the sql_query and sql_execute MCP tools
  • one reference guide at a time at GET /v1/agent/guide/{topic}, serving the same bodies as the MCP get_guide tool
  • conventional document probes at GET /v1/openapi.json, GET /v1/swagger.json, GET /v1/agent/openapi.json, and GET /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.

Security model

  • 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_id and app.workspace_id session settings.
  • Runtime role split:
    • backend_app is the default runtime role for application reads and writes.
    • auth_app is the restricted runtime role for auth-service flows.
    • Legacy app is not a valid runtime role anymore and exists only in historical migrations.

Local development entrypoints

  • 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.