The ordered narrative of the system's design, with diagrams rendered inline. Read top to bottom.
index.ts is the process entry: it installs source-map support, imports the MSW server when MOCKS=true, then imports server/index.ts. That file builds one Express app and layers, in order: an HTTPS redirect driven by Fly's X-Forwarded-Proto, a trailing-slash redirect, compression, @nichtsam/helmet security headers, a 404 handler for /img/* and /favicons/*, morgan logging, and three express-rate-limit tiers — 10 requests a minute for non-GET requests whose path contains login, signup, verify, onboarding, reset-password, settings/profile or admin (and for GETs to /verify), 100 for other non-GET requests, 1000 for everything else. Outside production every limit is multiplied by 10,000.
In development it mounts Vite in middleware mode and loads server/app.ts through ssrLoadModule on every request, so route edits need no restart. In production it serves build/client (fingerprinted /assets with a one-year immutable cache) and imports build/server/index.js, which Vite built from server/app.ts. Either way server/app.ts is only createRequestHandler from @react-router/express.
React Router runs the root loader and the matched route loaders, or the matched action, and app/entry.server.tsx streams the document: it generates a CSP nonce, sets a report-only Content-Security-Policy, waits for onAllReady for bots and onShellReady for people, and stamps Fly region and LiteFS primary headers on document and data responses. app/root.tsx's loader is the one place that loads the signed-in user (with roles and permissions), the theme cookie, the flash toast, honeypot props and the public ; components read the user from it with .
ENVuseOptionalUser()Deployed, two proxies sit in front of Express: Fly's edge sends traffic to internal port 8080, where the LiteFS proxy listens and forwards to the app on 8081, sending write requests to the primary machine.
Routes are files under app/routes/, turned into a manifest by react-router-auto-routes (app/routes.ts). Folders prefixed with _ (_auth, _marketing, _seo) group files without adding a URL segment, $ marks a parameter, _layout.tsx wraps its sibling routes, a trailing _ escapes nesting ($noteId_.edit.tsx is /users/:username/notes/:noteId/edit but does not render inside the note view), [.] escapes a dot (robots[.]txt.ts), and + folders hold colocated modules that never become routes (notes/+shared/note-editor.tsx). Anything named *.server.* is ignored as a route, which is how route-specific server logic sits beside its route: _auth/verify.server.ts, _auth/login.server.ts, settings/profile/change-email.server.tsx.
Shared server code is one module per concern in app/utils/*.server.ts: auth.server.ts (sessions, login, signup, password hashing, the common-password check), permissions.server.ts, db.server.ts (the Prisma singleton), cache.server.ts, storage.server.ts (hand-signed S3 requests to Tigris), email.server.ts (Resend over fetch), toast.server.ts and honeypot.server.ts. There is no auth middleware: each loader and action calls a guard itself, and a layout loader's guard does not protect a child route's action, which is why every action under settings/profile calls requireUserId again. Resource routes under app/routes/resources/ are loaders and actions with no UI: image optimisation, the health check, the theme cookie and the personal data export.
Sign-in state is a database row, not a token. login(), signup(), the passkey flow and the GitHub callback all create a Session row and put only its id in the en_session cookie, signed with SESSION_SECRET. getUserId() looks that row up on every request and treats a missing or expired row as a forced logout. requireUserId, requireAnonymous, requireUserWithRole and requireUserWithPermission are the whole guard vocabulary.
Everything that must prove control of an email address or an authenticator goes through one mechanism. prepareVerification() in _auth/verify.server.ts upserts a Verification row holding a TOTP secret, keyed by target and type, and returns a six-character code plus a /verify URL to email. The /verify action checks the code against that row and dispatches on type — onboarding, reset-password, change-email or 2fa — to a handler colocated with the feature. What a handler proves is carried to the next step in a separate en_verification cookie with a ten-minute max age: the email being onboarded, the username being reset, or a session still waiting for its second factor.
Two-factor reuses the same table. Enabling it stores a 2fa-verify row that becomes 2fa once the user confirms a code from their app. At login, handleNewSession() in _auth/login.server.ts looks for that row; if it exists, the new session id is parked in en_verification and the user is sent to /verify?type=2fa instead of receiving en_session. Sensitive settings pages call requireRecentVerification(), which sends 2FA users back through /verify when their last verification is older than the window shouldRequestTwoFA() computes.
prisma/schema.prisma models a small domain — a User owns Notes, each with NoteImages, plus one optional UserImage — and a larger identity model: Password (a bcrypt hash), Session, Connection (OAuth accounts), Passkey, Verification, and many-to-many Role and Permission. Images are not stored in the database: rows hold an objectKey into Tigris, and /resources/images fetches the object with a signed GET and resizes it with openimg, caching results on disk. The admin and user roles and their sixteen permissions (create, read, update, delete × user, note × own, any) are inserted by the initial migration SQL, not by prisma/seed.ts: user holds the eight own variants and admin the eight any variants, so every environment has them after prisma migrate deploy.
In production both SQLite files sit on a LiteFS FUSE mount and replicate to every machine, but only the primary — elected through Consul among machines whose region equals PRIMARY_REGION — can write. other/litefs.yml runs prisma migrate deploy and the WAL pragmas only on the primary candidate, then npm start everywhere. The LiteFS proxy forwards write requests (POST and friends) to the primary, but a GET loader that writes must call ensurePrimary() itself, as the OAuth callback does. The cache follows the same rule by hand: app/utils/cache.server.ts offers an in-memory LRU and a SQLite-backed cache for cachified; on a replica, cache.set and cache.delete POST the change to the primary's /admin/cache/sqlite with a Bearer INTERNAL_COMMAND_TOKEN, a value the Dockerfile generates for every image.