Skip to content

docs: CLAUDE.md — commands, architecture, and the production-secrets rule - #26

Merged
kunjhirapara merged 2 commits into
mainfrom
docs/env-secret-policy
Sep 13, 2026
Merged

kunjhirapara merged 2 commits into
mainfrom
docs/env-secret-policy

Conversation

@kunjhirapara

@kunjhirapara kunjhirapara commented Sep 13, 2026

Copy link
Copy Markdown
Owner

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 lint is tsc --noEmit. There is no ESLint config in this repo. Someone expecting lint to catch style will be disappointed, and someone adding a lint rule will be confused about where it goes.
  • The test runner is bare node --test with type stripping — no bundler, no path mapping. It resolves neither @/… aliases nor extensionless specifiers, so tests import with relative paths and explicit .ts extensions.

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.ts hand-rolls a constant-time compare instead of using node:crypto.

The Auth.js ↔ Convex seam, and the four ways to break it that have all now happened or nearly happened: an issuer differing by a trailing slash; an env var Convex requires statically because auth.config.ts names it; AUTH_URL set to the empty string (?? falls through only on null/undefined, so "" disables trustHost and 500s every route); and the pairing between allowDangerousEmailAccountLinking and the signIn callback 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 through resolveStreamUserId. 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 .env reach 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.example is the reference otherwise. Forbidden and allowed forms are both written out, with the two incidents that prompted the rule — convex env list --prod printing 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 in CLAUDE.md rather than rewritten here.

Docs only — no code, no tests affected.

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.
Copilot AI lite review requested due to automatic review settings September 13, 2026 16:08

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

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.
@kunjhirapara kunjhirapara changed the title docs: never read the values of production secrets docs: CLAUDE.md — commands, architecture, and the production-secrets rule Sep 13, 2026
@kunjhirapara
kunjhirapara merged commit d000b36 into main Sep 13, 2026
1 check passed
@kunjhirapara
kunjhirapara deleted the docs/env-secret-policy branch September 13, 2026 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants