Reading T3 Code

Chapter 18 · How they actually work

Capstone: three practices to adopt

The three T3 Code practices that would help your projects most, with the evidence for each and a step-by-step plan for one of your repos, plus the practices to skip while you work alone.

12 min read

Reading a large codebase leaves you with a long list of things it does. Most of them exist because of that team’s size, users, or history, and copying them into a solo project adds work without adding safety. This chapter makes the list short. It picks three T3 Code practices that fix real gaps in your projects as they are today, shows the T3 Code code or pull request each one comes from, and gives you a plan to adopt each in a named repo.

You do not need any other chapter to follow this one. The links to other chapters are further reading.

Where your projects stand

TL;DR

Fud is already close to how T3 Code ships. Antre deploys without running its tests. None of your apps would tell you why a production request failed. Fud runs production migrations, one of which deletes rows, without a restore point.

The choice of three comes from comparing what T3 Code does on every release with what your repos do at the commits this book pins.

Practice T3 Code fud antre monosodiums
Deploy only after every check passes Yes Yes No No
Errors say what failed and how to recover Yes For users, not in logs Partly Not applicable
Restore point before a schema change Yes No Once, in the cutover plan No database
Test a migration against old data Yes Yes Yes, for the cutover No database
Outcome-first commit subjects Yes Since Oct 28 Mixed Mostly

Some of this you already do well. fud’s Workers Builds build command is pnpm build:cloudflare, which runs lint, type checks, tests, the build, and dry-run deploy checks before anything ships. fud’s ledger.test.ts seeds old rows and then runs the 0003 migration on them, which is the same idea as T3 Code’s old database fixture. antre’s public workspaces rollout told you to export a Convex backup before its one big migration. Your pull request descriptions in monosodiums and fud have Why, Verification, and Scope sections with measured numbers. These are strengths, so the three practices below build on them instead of repeating them.

The gaps are in antre’s deploy, in what fud logs when something breaks, and in how fud changes its production database.

Ship nothing until the checks pass

TL;DR

T3 Code’s release job lists every test and every build it waits for, and it also opens the real app before publishing. Antre has tests and a type check, but its deploy runs neither. Put them in front of the build command so a red test stops the deploy.

The evidence

The job that publishes a T3 Code release names everything it depends on. GitHub Actions skips a job when one of its needs fails, so a single failing check stops the whole release:

t3code/.github/workflows/release.yml:794-810
release:
name: Publish GitHub Release
needs:
[
preflight,
relay_public_config,
quality,
test,
test_web,
test_server,
desktop_mac_arm64,
desktop_mac_x64,
desktop_linux_x64,
desktop_linux_arm64,
desktop_win_x64,
desktop_win_arm64,
]

The checklist adds one more gate after the tests. It opens the packaged app against a seeded database and opens a thread, and “A build whose backend or bundle cannot start fails here, so nothing is published” (release.md). Tests prove the parts work. A launch check proves that the thing you are about to ship starts.

The gap in antre

antre has a type check and a test suite. Its package.json defines check and test. Vercel runs only the command in vercel.json, and that command does not run them:

antre/vercel.json:3
"buildCommand": "if [ \"$VERCEL_ENV\" = production ]; then npx convex deploy --cmd 'npm run build' --cmd-url-env-var-name CONVEX_URL; else npm run build; fi"

On a production build, npx convex deploy pushes the backend functions and schema first, then builds the site. A change that breaks a test still reaches your Convex production deployment. For an app that publishes posts on a schedule, that is the deploy you most want to stop.

Adopt it in antre

  1. Run npm run check && npm test locally on a clean checkout and confirm both pass. If a test needs a variable from .env.example, add it under Vercel’s build environment variables now, not after the gate fails.
  2. In vercel.json, put npm run check && npm test && at the start of buildCommand, before the if. The && means the shell stops at the first failing command, so Convex never deploys after a red test.
  3. Push the change on a branch. Open the Vercel preview build log and confirm that svelte-check and Vitest run before vite build.
  4. On a second branch, change one assertion in src/lib/calendar.test.ts so it fails, and push. The preview build should fail at the test step, with no deployment created. Delete that branch.
  5. Merge the first branch. The next production build log should show the tests running before npx convex deploy.

monosodiums has the same gap and a ready-made launch check, scripts/check-world.mjs, which serves the build, opens it in Chromium, and fails on page errors. Wire it in the same way once antre is done. For more on release gates, read Releases and observability and CI and pull request hygiene.

Make errors say what broke and what to do

TL;DR

When T3 Code fails, the message names the thing that failed and the step that fixes it, and the original error stays attached. Fud’s user messages already do this. Its logs say only that something failed. Log a few fixed, safe fields so you can find the cause later without logging anything private.

The evidence

PR #35057, opened from Julius’s account, fixed a startup crash that printed Error: file is not a database and nothing else. The fix replaces that error with one that names the file and the recovery steps:

t3code/apps/server/src/persistence/Sqlite.ts:43-52
// Keeps the SqlError type so callers' error channels do not change; the original stays as the cause.
const damagedDatabaseError = (dbPath: string, cause: SqlError.SqlError) =>
new SqlError.SqlError({
reason: new SqlError.UnknownError({
cause,
operation: "open",
message: `The T3 Code database at ${dbPath} is damaged or is not a SQLite database, so T3 Code cannot start. To recover, quit T3 Code, then move that file and any -wal and -shm files next to it out of the folder, or replace them with a backup. T3 Code creates a new database on the next start.`,
}),
});

The message answers three questions in order: what failed, what it means, and what to do. The cause field keeps the original error, so nothing is lost for whoever debugs it next. The pull request body adds a reproduction, before and after output, and a test that “failed before the fix”.

For logs, T3 Code’s observability doc separates fields by how many values they can take. Labels such as “method name” or “outcome” come from a small fixed set. Thread IDs, file paths, and prompts never become labels. The product analytics doc names what must never be sent, starting with “prompts, authentication material, raw provider payloads, user-assigned device names, or conversation identifiers.”

The gap in fud

fud’s API Worker already gives users actionable errors, such as “Reconnect Gmail to continue importing.” The catch-all path is where the trail goes cold:

fud/worker/index.ts:248-253
// Do not put provider errors, OAuth URLs, email content or credentials in logs/responses.
console.error("fud_request_failed");
return Response.json(
{ error: "Your request could not be completed. Try again." },
{ status: 503, headers },
);

The comment is the right rule. fud handles bank emails and Gmail tokens, and those must stay out of logs. The fixed string follows that rule by logging nothing at all. When this fires in Cloudflare’s logs, you will know a request failed, but not which route, which method, or what kind of error. The scheduled handler at line 271 has the same problem with fud_schedule_failed. It does not say whether Gmail import or Alert delivery failed.

Adopt it in fud

  1. In worker/index.ts, list the fixed route strings that route() checks, such as /push/subscribe and /gmail/import. Put them in one const array.
  2. In the catch-all, compute route as the request path if it is in that array, and "other" if it is not. A raw path could carry an ID, and a fixed list cannot.
  3. Replace the log line with console.error({ event: "fud_request_failed", method: request.method, route, error: error instanceof Error ? error.name : "unknown" }). Log the error’s name, never its message, because a provider’s message can quote a token or an email.
  4. In scheduled, log which of resumeGmail and resumeAlerts rejected, by index, with the same error.name rule.
  5. Add a Vitest case in tests/ that makes route() throw an Error whose message contains a fake token. Spy on console.error and assert that the logged object has route and name, and that no logged value contains the token.
  6. Deploy, trigger a failure on a test account, and find the entry in Workers observability. It should name the route and error class and nothing else.

For how T3 Code watches production more broadly, read Releases and observability.

Take a restore point before every production migration

TL;DR

Before T3 Code changes a database schema on update, it copies the database, and it restores the copy if the new version fails to start. Fud applies production migrations by hand with no copy, and one of its migrations deletes rows. Record a D1 Time Travel bookmark before each migration and write the restore command next to it.

The evidence

T3 Code’s background server updates itself while nobody watches. The server updates doc describes what the launcher does first:

Before stopping the old child, the launcher refuses an update whose snapshot will not fit on disk, so a full disk does not cause an outage. After the old child exits, the launcher snapshots SQLite’s main file, WAL, and shared-memory file. This makes trial migrations reversible without down migrations.

The new version then runs as a trial. It must finish its migrations and bind its port before the launcher commits to it. If the trial fails, the launcher restores the snapshot and restarts the old version (server-updates.md). The lesson for a solo developer is the first step. Take a copy you know how to restore, and you never need to write a reverse migration.

The gap in fud

The fud README applies the schema with pnpm exec wrangler d1 migrations apply fud --env production --remote. Nothing before it records where to go back to. That matters for this migration in particular:

fud/migrations/0003_push_ownership.sql:1-5
-- Conflicted endpoints have no known consenting Owner and must be enabled again.
DELETE FROM pushSubscriptions WHERE endpoint IN (
SELECT endpoint FROM pushSubscriptions GROUP BY endpoint HAVING COUNT(*) > 1
);
CREATE UNIQUE INDEX pushSubscriptions_endpoint ON pushSubscriptions(endpoint);

The deletion is deliberate, and ledger.test.ts checks it against seeded rows. A future migration with a wrong WHERE clause would delete rows you meant to keep, and there would be no down migration to bring them back.

D1 has a built-in way back called Time Travel. The Wrangler version fud pins, 4.152.0, includes wrangler d1 time-travel info and wrangler d1 time-travel restore with a --bookmark option.

Adopt it in fud

  1. Add a db:bookmark script to package.json that runs wrangler d1 time-travel info fud --env production. Run it once and confirm it prints a bookmark.
  2. Add a db:migrate script that runs pnpm db:bookmark && wrangler d1 migrations apply fud --env production --remote. The && means no migration runs if Wrangler cannot read the bookmark.
  3. In README.md, replace step 6 of “Deploy the backend” with pnpm db:migrate. Below it, add the restore command, pnpm exec wrangler d1 time-travel restore fud --env production --bookmark=<bookmark>, and one sentence on when to use it.
  4. For each new migration, paste the printed bookmark into the pull request that adds it, under Verification. The pull request then records where to restore to.
  5. Before you rely on this, rehearse it on a separate test database. Create one with wrangler d1 create fud-restore-test, add a row, record a bookmark, delete the row, restore to the bookmark, and confirm the row is back.

Keep the habit you already have, too. Every migration that changes or deletes rows gets a test like the 0003 test in ledger.test.ts, which seeds old rows before it runs the migration. For more on T3 Code’s tests, read How they test.

What to skip for now

TL;DR

Most of what makes T3 Code’s release process big exists because people install it and keep old versions. Your apps are websites, so skip release channels, signed builds, self-updating servers, your own analytics pipeline, and review bots until a real problem asks for them.

Theo’s note at the top of T3 Code’s agent instructions applies here as much as to the agents it was written for:

t3code/AGENTS.md:35-37
I like ambitious ideas, simple systems, and software that feels obvious. Do not preserve complexity just because it already exists. Do not introduce machinery because it looks architecturally impressive. Understand the real constraint, then fight for the smallest model that makes the correct behavior unsurprising.

Channel both "measure twice, cut once" and "yagni". Fight scope creep. Try to honor the dev's intent in both a minimal and realistic fashion.

“YAGNI” stands for “you aren’t gonna need it”. It means you build a thing when a real need shows up, not before. By that rule, skip these:

  • Release channels and a nightly train. T3 Code ships stable from the last nightly because, in Theo’s words in PR #10410, “We merge hundreds of PRs a day.” You merge a few a day, and every visitor gets your latest deploy on the next page load. fud’s branch previews already give you a place to try a build first.
  • Signed builds and a self-updating launcher. These exist because T3 Code runs on users’ machines across three operating systems. A web app has no installed copies to update.
  • Your own analytics pipeline. T3 Code runs its own ingest endpoint and forwards to PostHog because its server runs on thousands of laptops. Cloudflare Web Analytics, already on monosodiums, answers a portfolio’s questions.
  • Review bots and automated backlog sweeps. T3 Code’s maintainers run paid review bots with a $10 per-review cost limit, as comments on PR #5362 show, and they used agents to close stale outside pull requests. Your review agent’s verdicts on fud pull requests already cover the useful part.
  • A full tracing stack. Spans, OTLP export, and dashboards pay off when many requests interleave and you cannot reproduce them. The few fixed log fields from the second practice in Cloudflare’s own logs are enough until they are not.

Revisit this list when something changes. That might be a second person committing, users who install something, or a bug you could not diagnose from the logs you had.

Try it

TL;DR

Do the three plans in order, each in its own pull request. Each one is finished when you have watched it catch the failure it exists for.

  1. Gate Antre’s deploy. Follow the steps in the first practice in antre/vercel.json. It is done when a branch with a failing test produces a failed Vercel preview build and no deployment.
  2. Make Fud’s failure log useful. Follow the steps in the second practice in fud/worker/index.ts and a new test in fud/tests/. It is done when the test proves a fake token never reaches console.error, and a real failure in Workers observability names its route.
  3. Add a restore point to Fud’s migrations. Follow the steps in the third practice in fud/package.json and fud/README.md. It is done when the rehearsal on fud-restore-test brings a deleted row back from a bookmark.

Text size

Theme