Contributing
Repository layout, the CI gates a change must pass, and how to verify work locally before opening a pull request.
Repository layout #
| Path | What it is |
|---|---|
server.mjs | The entire server. ~10,000 lines: config, script injection, serve-time bundle patching, every route, startup backfills. |
index.html | The SPA shell. At the repository root, not in public/. |
public/assets/ | Pre-built minified Vite output. Patched at request time, never modified on disk. |
public/sync/ | Sync engine modules, including backup-normalizer.js. |
public/*.js | Runtime glue: auth bridge, boot recovery, restore-and-launch, update checker. |
bin/isotope | The CLI, plus .bat and .ps1 equivalents. |
scripts/*.mjs | Operational scripts. No test framework; each is standalone Node. |
sql/ | Numbered migrations and the portable schema dump. |
docs/ | This site. Plain HTML and CSS, no build step. |
src/ | Vite/React scaffold. Not what is served — editing it has no effect on users. |
src/ is not the running appThe React source in src/ is a shadcn scaffold. What users receive is the pre-built bundle set in public/assets/, rewritten by server.mjs at request time. Changing a feature usually means adding a serve-time patch, not editing src/.
Zero dependencies is a constraint, not an accident #
package.json lists no dependencies and no devDependencies. Please keep it that way. It is why install works on a phone with no build toolchain, and why there is no transitive supply chain to audit.
Working with serve-time patches #
Each patched bundle has a getPatched*Bundle() function that reads the file, performs exact string replacements against minified code, memoises the result and returns a Buffer.
const ANCHOR_FROM = 'exact minified substring from the built bundle';
const ANCHOR_TO = 'replacement';
if (raw.includes(ANCHOR_FROM)) {
raw = raw.replace(ANCHOR_FROM, ANCHOR_TO);
console.log('[MyPatch] applied');
} else {
console.warn('[MyPatch] anchor not found');
_criticalPatchFailures.push('my-patch'); // surfaces as a UI banner
}
Three rules when adding one:
- Anchor against the minified file, not pretty-printed source. Two patches were silently dead for weeks because their anchors were written from formatted code.
- Always log both outcomes and push to
_criticalPatchFailureson miss. A silent failure is the worst case. - Add the asset to
RUNTIME_PATCHED_ASSET_PATHSin bothserver.mjsandpublic/sw.js. Miss this and the file shipsimmutable, pinning the unpatched body in browsers for a year.
CI gates #
13 workflows. These are the ones that will block a pull request:
| Workflow | Gate |
|---|---|
ci.yml | bash -n on 9 shell scripts, node --check on 20 JS files including sw.js both raw and with placeholders substituted, a backup-normalizer assertion, required-files presence, a secret scan, and a server smoke test on port 3099. |
schema-lint.yml | Executes isotope-complete.sql against Postgres 16 with a Supabase compatibility shim. Zero ERROR lines permitted. Also validates root-level patch SQL and dollar-quote balance. |
codeql.yml | Static analysis on JavaScript and TypeScript. |
pages.yml | Deploys docs/ to GitHub Pages. |
release.yml | Builds and verifies the release archive. |
The two drift checkers #
Both exist because a real bug got past review, and both fail loudly rather than warning.
# does isotope-complete.sql still match the live schema? npm run schema:drift # are all dollar-quoted SQL bodies balanced? npm run sql:quotes
Why the dollar-quote checker existsA stray $$; in isotope-complete.sql silently swallowed the next CREATE FUNCTION header. Postgres then reported a confusing syntax error thousands of lines later. The checker matches tags properly — $$, $fn$, $iso_fn$ — and balances each independently.
Verifying a change locally #
# 1 — syntax node --check server.mjs node --check public/sw.js bash -n bin/isotope # 2 — restart and confirm every patch anchor still matches bash bin/isotope restart && sleep 6 grep -iE 'anchor not found|String not found' ~/.isotope/logs/server.log # want no output # 3 — every patched bundle must return 200 and parse for f in Community-CEnEgsrd useAuthStore-Aw1au7RF index-D1Y5F8Lk; do curl -s "http://127.0.0.1:3000/assets/$f.js" -o /tmp/b.js node --check /tmp/b.js || echo "PARSE FAIL $f" done # 4 — test suite npm run test:auth-bridge npm run test:runtime-glue npm run docs:validate
Editing these docs #
Plain HTML in docs/, one stylesheet, one script, no build step. Pages share the chrome verbatim — if you change the topbar, drawer, sidebar or footer, change it in every page or navigation drifts.
Conventions:
<h2 id="x">is required — the table-of-contents rail is generated from those ids at runtime.- Wrap code in
<div class="codeblock" data-lang="bash">and a copy button is attached automatically. - Callouts use an icon plus a label, never colour alone.
- Use semantic classes, not inline styles. Colour lives in the token block at the top of
site.css.
npm run docs:validate
Commits #
Conventional prefixes — fix:, feat:, chore:, docs:. The body matters more than the subject: state what was wrong, why it was wrong, and how the fix addresses it. A future reader needs the reasoning, not a restatement of the diff.
- Never commit
.env— CI fails if it becomes tracked. - Commit before running
isotope update; it stashes a dirty tree. - Do not force-push shared branches.
Known open items #
| Item | Detail |
|---|---|
community_bootstrap_profile cannot run | Its UPDATE assigns profile_data four times in one statement, which PostgreSQL rejects with 42601: multiple assignments to same column. It is the only writer to user_profiles, so that table stays empty and every buddy feature fails downstream. |
| Buddy handle is written and read in different places | Bootstrap writes the handle into profile_data->>'community_handle'; community_request_buddy reads the user_profiles.handle column. Even with 42601 fixed, buddy requests would still raise user_not_found. |
Two search_path conventions | Some definer functions pin the empty string, the rest pin public. Both are safe; the inconsistency is not ideal. |