docs: CLAUDE.md — commands, architecture, and the production-secrets rule - #26
Merged
Merged
Conversation
Two disclosures happened in one session, both while answering the question "is this variable set?", and both avoidable. `npx convex env list --prod` was run to check for one variable and printed every production value, including AUTH_JWT_PRIVATE_KEY -- a key that mints a valid Convex token for any user. Five secrets needed rotating. A per-variable existence check was then handed over as a multi-line command. It wrapped when pasted, which split `[ -n` from its `]`; the test became a syntax error and the shell executed each value as a command, printing the Google and GitHub OAuth client secrets. Two more rotations. So the rule is that values are never read, existence is checked only when it is genuinely necessary, and .env.example is the reference for names and shapes. The allowed forms are written down alongside the forbidden ones, because "be careful" is not a procedure -- the second incident happened while being careful. Includes the one-line constraint, which is not a style preference: it is the difference between a check that prints names and the same check printing secrets.
Extends the secrets policy into the full onboarding file, covering the things that take reading several files to work out and are expensive to learn by being bitten. Commands, including the two surprises: `npm run lint` is tsc, not ESLint -- there is no ESLint config in this repo -- and the test runner is bare `node --test` with type stripping, so it resolves neither `@/` aliases nor extensionless specifiers. Tests import with relative paths and explicit .ts extensions, which is also why convex/lib holds import-free modules; that pattern looks arbitrary until you know what the runner cannot do. Architecture, limited to what is not discoverable from one file: The three runtimes and what each forbids -- Node, Edge middleware, and Convex's V8 isolate, which is why adapterAuth hand-rolls a constant-time compare. The Auth.js/Convex seam, and the four ways to break it: an issuer that differs by a trailing slash, an env var Convex requires statically because auth.config names it, an AUTH_URL set to the empty string, and the pairing between allowDangerousEmailAccountLinking and the signIn callback that makes it safe. The three ids for one person. Passing a session id where Stream expects streamUserId does not error -- it mints a valid token for a user Stream has never seen, and their recordings are simply gone. Deployment, where publishing is not shipping and Portainer keeps its own copy of docker-compose.yml, so repo changes to that file do not reach the VM and env vars in the VM's .env reach nothing unless named in Portainer's copy. Also notes that README's auth section still describes Clerk.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Creates the project
CLAUDE.md. Two parts: the guidance that makes someone productive here quickly, and the rule about production secrets that was asked for separately.Commands
Two surprises worth having written down:
npm run lintistsc --noEmit. There is no ESLint config in this repo. Someone expectinglintto catch style will be disappointed, and someone adding a lint rule will be confused about where it goes.node --testwith type stripping — no bundler, no path mapping. It resolves neither@/…aliases nor extensionless specifiers, so tests import with relative paths and explicit.tsextensions.That second one explains a pattern that otherwise looks arbitrary:
convex/lib/*holds modules with zero imports (retention,owner,permissions,subjectResolution,integrityModes— all verified at 0). A module there that imports a sibling becomes untestable.Architecture
Deliberately limited to what takes several files to work out:
Three runtimes, three sets of constraints. Node, Edge middleware, and Convex's V8 isolate — which is why
convex/lib/adapterAuth.tshand-rolls a constant-time compare instead of usingnode:crypto.The Auth.js ↔ Convex seam, and the four ways to break it that have all now happened or nearly happened: an
issuerdiffering by a trailing slash; an env var Convex requires statically becauseauth.config.tsnames it;AUTH_URLset to the empty string (??falls through only on null/undefined, so""disablestrustHostand 500s every route); and the pairing betweenallowDangerousEmailAccountLinkingand thesignIncallback that is the only thing making it safe.Three ids for the same person —
_id,clerkId,streamUserId— and the rule that anything touching Stream goes throughresolveStreamUserId. Getting this wrong does not error: it mints a valid token for a user Stream has never seen, and their recordings are simply absent.Deployment, where publishing is not shipping. Convex deploys before the image so the backend is never behind the frontend; nothing pushes to the VM. And Portainer keeps its own copy of
docker-compose.yml, so repo changes to that file never reach the VM, and env vars in the VM's.envreach nothing unless named in Portainer's copy. Both cost real downtime to discover.Production secrets
Values are never read. Existence may be checked, only when genuinely necessary, and only in a form that cannot print the value;
.env.exampleis the reference otherwise. Forbidden and allowed forms are both written out, with the two incidents that prompted the rule —convex env list --prodprinting every value, and a multi-line existence check that wrapped on paste and executed the values.The one-line constraint is stated as a rule rather than a style note, because it is the entire difference between the two: the second leak happened while being careful.
Also noted
README.md's auth section still describes Clerk and is out of date after the migration. Flagged inCLAUDE.mdrather than rewritten here.Docs only — no code, no tests affected.