90x · how it fits together

90x Architecture Atlas

Ten diagrams of the real system, drawn from the code on main. Start with the big picture, then follow content from source to screen, a learner's day, one tap end to end, and what keeps it safe. Tap any box to see what it does and the files it lives in.

main @ e82417c · 2026-10-10 · facts traced from source, every box links to its files
How to read the boxes and lines People 90x app code Guard or check Data store Scheduled job Offline (your machine, CI) Outside service Main path Writes production Refused or failed Scheduled or async

The big picture

01 · Architecture

Everything goes through the proxy, and all AI spend goes through one gate

One Next.js app on Vercel (Mumbai) serves every screen. It keeps state in Supabase Postgres, uses Upstash for fast counters and schedules, and asks an outside AI model only after the AI gate says yes. Content arrives from the offline pipeline you run on your machine.

Vercel · bom1 · deploys main only Managed data Outside services Offline · your machine and CI every request approve, review uptime ping signed trigger signed in admins only API calls taps acts reads plan, cards, XP feed queue, locks grade a written answer may it spend? weekly review only if under caps spend meters sync, 6 h reminders publish content audio mp3 nightly backup LearnerPWA · phone or desktop Adminsame app, /admin MonitoringSentry · UptimeRobot Upstash QStashruns jobs on a timetable Proxysession refresh · maintenance switch · /admin lock Pagesthe five tabs Server actionsevery tap that changes data Admin pagesaccounts · cards · caps API routesCoach chat · push · health Job routeshourly tick · LeetCode sync AI gatepause · caps · spend LeetCoderecent solves Web pushphone notifications AI providerDeepSeek fast · smart SupabasePostgres · Google sign-in Cloudflare R2audio · backups Upstash Redisqueues · limits · meters Python pipelinecontent · cards · audio GitHub ActionsCI · nightly backup

Tap any box for what it does and the files it lives in.

What to take away

  • The proxy is the front door for people; QStash is the front door for scheduled work.
  • Server actions are the hub: nearly every change is a transaction in Postgres.
  • Three paths reach the AI provider, and all three pass the AI gate first.
  • Content is never written by the app. Only the offline pipeline publishes it.
02 · Workflow

Code ships only after CI is green, and the data is guarded by a nightly encrypted backup

A pull request runs CI, and only a merge to main reaches Vercel. Production database changes are never part of that deploy: you apply them by hand. Separately, every night the database is dumped, encrypted and kept for 30 days, and monitoring watches the live site.

Lane 1 · Code path GitHub Actions · CI Lane 2 · Data safety Monitoring opened scope always, whole tree e2e scope required every other check must be green deploys up? db:push pg_dump encrypted run status pings errors Pull requestone change filterdecides which jobs webtypes, tests, build databasemigrate, RLS pipelineuv sync, pytest depsaudit, deprecated gitleakssecret scan, always e2e feedPlaywright shard e2e coachPlaywright shard e2eall shards pass Merge to mainchecks green first Vercel deploymain only · bom1 /api/health503 if a store is down Prod migrationby hand, owner's go Supabase Postgresproduction data Nightly dump21:30 UTC · GPG R2 backupsprivate · kept 30 days job_runs rowshown in admin A failed backup emails you. Uptime checksUptimeRobot · Sentry Sentryerrors, no PII

Tap any box for what it does and the files it lives in.

What to take away

  • The filter saves time: untouched areas skip their heavy steps, but gitleaks always reads the whole tree.
  • Two e2e shards sit behind one check named e2e, so the required name never changes.
  • Vercel deploys main only. A pull request never gets a preview.
  • Production migrations are separate and manual. Nothing in the deploy touches the schema.
  • Backups are encrypted, private, kept 30 days and recorded in job_runs.

How content is made

03 · Dataflow

Content is built from downloaded sources on your machine, and only three steps write production

Sources are downloaded, normalized and kept in a local staging database. Lessons, cards, audio and Coach chunks are built from there, a lesson is never written without a source, and every paid AI call stops at a spend cap. You review the work, then publish.

1 · Download 2 · Prepare 3 · Stage 4 · Generate 5 · Review 6 · Publish (prod) raw files rows source docs lessons lessons all text each LLM call lessons drafts publish audio-upload embed Sourcessaved to .data/no LLM Normalizeenrich + importanceLLM: pattern tags only staging.duckdblocal working copyyour machine only Spend stopPIPELINE_MAX_USD Lessonsno source, no lessonLLM · capped Cardswriter + quality gatesLLM · capped Audioscript, check, mp3LLM · flat rate Coach chunks300-450 word passagesno LLM Your reviewyou read the packsno code gate Supabasepublish, cards as draftswrites prod Cloudflare R2audio mp3 fileswrites prod Upstash VectorCoach search indexwrites prod

Tap any box for what it does and the files it lives in.

What to take away

  • Everything before publish stays on your machine, in staging.duckdb.
  • No downloaded source, no lesson: the model cannot answer from memory.
  • Paid steps stop at PIPELINE_MAX_USD. Audio is flat-rate and has its own limits.
  • Three steps write production: embed to Upstash Vector, audio-upload to R2, publish to Supabase.
  • Cards land as drafts. Reading is the app's job, never writing content back.
04 · Lifecycle

A card goes staged, draft, live, retired, and "hidden" is a separate switch, not a status

The pipeline only ever publishes drafts. A card goes live when you pass its batch in the admin, or when you run the pipeline swap. Readers cannot retire a card; they can only hide it, and then you decide whether it comes back.

Status · cards.status Separate switch · cards.hidden · not a status value publishdrafts only admin batch reviewor pipeline swap admin retiresor swap retires 2 reader flags,or 14 days all skipped admin keepsflags cleared admin retires hidden is a flag on a live card. Its status stays live underneath. Readers see only cards that are live and not hidden. stagedin staging, passed gates draftpublished, invisible livereaders get it retiredhistory kept shownhidden = false hiddenwaits for admin

Tap any box for what it does and the files it lives in.

What to take away

  • Publish only creates drafts. Going live always needs an admin batch review or the pipeline swap.
  • Hidden is its own switch. A hidden card is still live, just out of the Feed.
  • Two reader flags hide a card, and so does 14 days of every reader skipping it. Only you can bring it back or retire it.
  • Lessons have no status in production, and a problem that history points at is hidden rather than deleted.

A learner's day

05 · Workflow

Today plans the day once, then every piece of work ticks a mission and pays XP

Opening Today closes old days and claims today's row in one transaction, then planDay picks the missions in a fixed order. After that, each check-in, LeetCode solve or Feed card ticks a mission, pays XP, and moves the day toward done. An hourly job keeps all of this running on each learner's own clock.

Scheduled jobs · QStash 1 · Open Today 2 · planDay picks missions in this order 3 · Work, credit, day status chosen hour if enabled Sunday 6 pm, reader's clock rolls each day over saves next week's focus page load no row yet leans on it in this order none due then plus one optional learner works them same problem newly done problems re-count the day all resolved keeps streak restores day day left unfinished: closes partial or missed Morning pushonly if missions are open Hourly tickQStash, minute 5, each reader's own clock 8 pm reminderopen missions, names streak Sunday review6 pm local, writes next focus Open Todayfirst tap of the day ensureTodayclose past days, claim today's row planDaya pure function Weekly focusnewest review, last 8 days Due reviewsoldest due firstempty slot: new problem New problemsweakest pattern firstranked by importance Topicweakest area first"new to me" comes first 10 cardsexactly one a dayon top of the slots Want more?tap adds an extrabonus, not required 1 2 3 4 Today's missionsweekday template sets the slots · weekly focus adds at most one problem and one topic a day Work recordedmanual check-in, LeetCode syncor a Feed card answer Tick missionsmatches by problem10 cards counts as one awardXponce per thingcard XP capped daily Review ladderfailed or hints: back in 3 daysthen 7, then 21 refreshDaydone when every countedmission is done or skipped Day done+20 XP day bonuspaid once a day Streakdone and revived daysrest days are skipped Revive a daymissed or partial dayswithin the last 2 days

Tap any box for what it does and the files it lives in.

What to take away

  • The day is planned once. Claiming today's row in one transaction keeps two tabs or the hourly job from planning it twice.
  • Missions come in a fixed order: reviews, new problems, a topic, then exactly one 10 cards. A review slot with nothing due becomes a new problem.
  • The weekly focus nudges the plan by at most one problem and one topic a day. Want more adds bonus extras.
  • XP is paid once per thing. Failed attempts earn nothing, and Feed card XP is capped each day.
  • A missed or partial day can be revived for two days to keep the streak.
06 · Lifecycle

Problems come back on a fixed 3, 7, 21 day ladder; Feed cards are timed by FSRS with a minimum wait

Two separate schedulers. A problem only gets reviews if you failed it or needed hints, and then it climbs a fixed ladder. A Feed card gets a due date from the FSRS library, and a right answer is held back for at least a week.

Problems · fixed ladder · ladder.ts Feed cards · FSRS · srs.ts failed or hintedcheck-in clean solve clean solve clean solve failed or hinted check-in restarts at step 1, from any state I've got this First-try clean solves never enter. Not today: same step, back tomorrow. Failed or hinted always re-enters at step 1. No reviewclean solve stays here Step 1back in 3 days Step 2back in 7 days Step 3back in 21 days Graduatedoff the ladder Dismissedreader's choice first answer, right first answer wrong or skipped wrong, skipped or New to me answered right right answer on an Easy cardor in a mastered topic A right answer waits at least 7 days (Hard), 14 (Good) or 30 (Easy). FSRS can push it later, never earlier. Wrong means a score under 70% or a skip. FSRS then brings the card back soon, and counts a lapse. Waiting and Back soon are our names: the code stores only a due date, not a state. New cardno answer yet Waitingdue in 7, 14 or 30+ days Back soonFSRS decides when Out of rotationdue in 365 days

Tap any box for what it does and the files it lives in.

What to take away

  • Only failed or hinted problems enter the ladder, at step 1. A first-try clean solve never comes back.
  • Each clean solve climbs one step: 3, then 7, then 21 days. A clean solve at the top graduates it.
  • Any failed or hinted check-in sends a problem back to step 1, even a graduated one.
  • For Feed cards, FSRS picks the date but a right answer always waits at least 7, 14 or 30 days.
  • An Easy card, or a mastered topic, answered right leaves the rotation for 365 days.

One tap, end to end

07 · Sequence

A Feed answer is graded, saved in one transaction, and the next card comes back in the same request

Most card kinds are marked by code, free, and worth 2 XP. A written answer asks the AI gate first, then the fast model, and is worth 1 XP. If the gate says no or the model fails twice, nothing is saved and the card returns later.

1 answer + clientId 2 who is asking? 3 hourly Feed counter too fast · stop 4 answerCard 5 claim clientId seen before · once 6 load the live card Pick, order, map, number, skip:graded by code, free, 2 XP 7 written: may it spend? refused 8 grade key points fails twice 9 hits per key point nothing saved · back later 10 save review + FSRS + XP didn't save · retry 11 graded result 12 nextCard, same request 13 lock, pop, balance 14 under 10: weak, due, new 15 push refill to queue 16 result + next card BrowserFeed screen Server actionsubmitAnswer Feed servicegrade · save · queue Redisfeed queue · locks Postgresreviews · FSRS · XP AI gatepause · caps Fast modelgrades key points

Tap any box for what it does and the files it lives in.

What to take away

  • Most kinds (pick, order, map, number, skip) are graded in code: no AI, no cost, 2 XP.
  • Only a written answer reaches the AI, and only if the AI gate allows it. That grade is worth 1 XP.
  • A refused or failed grade saves nothing, so the card comes back later instead of counting wrong.
  • Review, memory schedule and XP are written together, so a card is never half saved.
  • The next card rides back with the result, taken from a Redis queue that refills when it drops under 10.
08 · Sequence

A Coach message passes three checks before the model sees it, and the reply is saved even if you leave

The chat route checks you are signed in, asks the AI gate, and takes one slot from your message allowance. Only then does it build the prompt and stream the answer. Past a spend line the smart model drops to the fast one.

1 one new message 2 who is asking? 401 · sign in first 3 aiGate: pause, caps 503 · resting 4 message slot 429 · fails closed 5 load thread, save yours 6 mode prompt, tools 7 fenced prompt, tools 8 past a spend line? 9 smart model, or fast 10 stream the answer error · try again 11 tokens 12 streamed answer 13 save reply, log usage BrowserCoach chat Chat routePOST /api/coach/chat Viewer checkgetViewer Cost guardsAI gate · message slot Postgresthreads · usage Coach modeprompt · tools Modelsmart, or fast

Tap any box for what it does and the files it lives in.

What to take away

  • Three doors come first: signed in (401), the AI gate (503), your message allowance (429).
  • The message allowance fails closed: if Redis is down the message is refused, never sent free.
  • Past a spend line the Coach switches to the fast model instead of stopping.
  • Only the new message is sent. History, prompt and tools are built on the server.
  • The reply is saved even if you close the app. Only an explicit Stop cancels the model.

Safety and accounts

09 · Sequence

Every request meets the firewall, the proxy, a verified identity and a scoped query before it gets data

The Vercel firewall and the proxy come first. Then the page proves who you are, reads your approval from the database, and queries only your rows. Paid AI also passes the AI gate and rate windows. The browser has no direct path to the tables.

1 request refused: too many calls 2 the rest pass on 3 maintenance switch 503 · maintenance 4 /admin: who, approved? 404 · not an admin 5 passes on, cookie refreshed 6 verify ES256 + expiry any doubt: signed out 7 approval, admin, email 8 row, or account gone 9 viewer, or redirect 10 WHERE user_id = viewer App role skips row security,so the WHERE is the lock.Browser Data API: closed. 11 AI gate + rate windows refused · fails closed 12 response + CSP headers Browserpage or action Vercel firewallbefore our code Proxyproxy.ts Upstash Redisswitches · limits Postgresserver only Page or actionserver code getViewersignature · DB row

Tap any box for what it does and the files it lives in.

What to take away

  • The firewall and the proxy act before any page code runs.
  • A token that cannot be proven is a signed-out visitor, never a trusted one.
  • Approval, admin and email come from the database each request, so a blocked user is stopped on the next tap.
  • The app's database role skips row security, so every query is scoped to the viewer in code.
  • Paid AI passes the AI gate and rate windows, and a meter that cannot be read means no.
10 · Lifecycle

A new account waits as pending until approved, then sets up, and an admin can block or delete it later

Every Google sign-in creates a pending account. Approval comes from you, or from the auto_approve setting. After approval the person finishes set-up and becomes active. Block keeps the account but shuts the app, and Delete removes the data and leaves a short-lived record.

Gone Google sign-in,DB trigger admin: Let in finishes set-up auto_approve on: callback approves at once admin: Block admin: Block admin: Unblock self or admin: Delete self or admin: Delete hourly job,after 90 days Welcome is a one-time overlay on Today after set-up. It is not a state. Self delete: type DELETE. Admin delete: type the person's email. Admins are never blocked or deleted from the app. Pendingsees /pending only Set upapproved, only /setup Activethe whole app Blockedcan sign in, no app Deletedrecord kept 90 days Count onlymonth and a count

Tap any box for what it does and the files it lives in.

What to take away

  • Every sign-in starts as pending, created by a database trigger. Only you, or the auto_approve setting, lets someone in.
  • Blocked is stored as "rejected". That person can still sign in, but only to sign out or delete their account.
  • Status is read on every request, so a Block works on the next tap.
  • Delete is typed: DELETE for yourself, the person's email for an admin. Admins are never blocked or deleted from the app.
  • Deleted details last 90 days, then only a count remains.