Skip to content

Contributing

Branch naming

All feature and bug work happens on short-lived branches cut from origin/development (the integration base for ftl-backend and ftl-frontend; ftl-docs uses origin/main).

git checkout -b feat/<slug> origin/development
git checkout -b bug/<slug> origin/development

Rules:

  • The slug must describe the work. No dates, no session IDs, no person names. A teammate reading git branch -r should understand the scope from the name alone.
  • feat/ — new user-visible behavior. Examples: feat/trade-window-gating, feat/reminder-bell.
  • bug/ — fixes to existing behavior. Examples: bug/squad-refresh-loss, bug/login-cookie-domain.
  • One branch = one scope. If a task expands into a separate concern, cut a new branch from development for the new concern.
  • Push to origin on your first meaningful commit. Work that only exists locally is invisible and unrecoverable.
  • Never commit directly to main, release/staging, or release/prod.
  • Never force-push a shared branch without explicit confirmation from the team.

Pull requests

PR description must include Author: @<github-handle> — the GitHub account of the human operator running the session, not Claude. This applies even when Claude wrote every line of code.

Example footer:

Author: @vaishnav-s-01

Container names must be prefixed with the branch slug. When starting any Docker container for work on this branch, use the slug as a name prefix:

# On branch feat/trade-window-gating:
docker run --name trade-window-gating-api ...

This keeps containers from parallel tasks isolated and makes ownership obvious in docker ps.

Pre-merge checklist:

  • [ ] make test passes with the -race flag (see Running tests).
  • [ ] If the PR changes behavior described in any Living Spec, the spec is updated in the same PR or a paired ftl-docs PR.
  • [ ] If the PR introduces an architectural decision, a new ADR is added.
  • [ ] PR description includes Author: @<github-handle>.
  • [ ] Container names in any run instructions use the branch slug as a prefix.

Living Spec system

FTL uses three complementary doc types. Knowing when to write each prevents stale docs.

Type Path Job
Living Spec ftl-docs/architecture/specs/<domain>.md Describes one domain as it is today. Rewritten in place when behavior changes. Never versioned.
ADR (Architecture Decision Record) ftl-docs/architecture/decisions/NNNN-<slug>.md Captures one decision — what was chosen, why, what was rejected. Append-only once accepted.
Per-commit walkthrough ftl-docs/pr-reports/<branch>-<sha7>.html Auto-generated HTML after every Claude-authored commit. Captures the what of the commit.

When to update a Living Spec

Update the affected spec whenever a code change alters behavior that the spec describes:

  1. Edit the relevant section of ftl-docs/architecture/specs/<domain>.md.
  2. Bump last-updated: in the YAML frontmatter to today's date.
  3. Verify the code-refs: paths still exist and update line ranges if they shifted.

Do not pre-write spec sections for not-yet-shipped behavior. That content belongs in an ADR until the code lands.

When to add an ADR

Add an ADR whenever you make a decision that: - changes behavior, schema, or a public contract, or - a future engineer would be confused about why the change was made if they only saw the diff.

Skip ADRs for pure refactors, typos, or dead-code removal with no behavior change.

To create a new ADR:

# Find the next number
ls ftl-docs/architecture/decisions/ | grep -E '^[0-9]{4}-' | sort | tail -1

# Copy the template
cp ftl-docs/architecture/decisions/_TEMPLATE.md \
   ftl-docs/architecture/decisions/NNNN-<slug>.md

Fill in every YAML field. affects-specs: and affects-code: are required. Add a row to ftl-docs/architecture/decisions/INDEX.md (newest first).

ADR lifecycle

ADRs carry two independent status fields:

Field Tracks Values
status: Decision lifecycle proposedacceptedsuperseded / rejected
implementation-status: Code lifecycle pendingin-progressimplemented

Once status: accepted, only status, superseded-by, and implementation-status may change. The content of an accepted ADR is append-only.

Common flow — design first, code later:

  1. Open a docs PR with the ADR at status: accepted, implementation-status: pending. The spec is not rewritten yet — it still describes the live system. The ADR may carry an implementation guide for the code author.
  2. Add a row to ftl-docs/PENDING.md for any ADR with implementation-status: pending.
  3. When the code lands, open a paired code PR that also: rewrites the affected spec section, flips the ADR to implementation-status: implemented, and moves the PENDING.md row to the "Recently shipped" section.

Superseding an older ADR:

  • Set supersedes: NNNN on the new ADR.
  • Open the older ADR and set status: superseded and superseded-by: <new number>.
  • Only supersede when the enacting code ships in the same PR. If the code hasn't landed yet, the older ADR still describes live behavior.

Per-commit HTML walkthrough

After every commit Claude makes in any ftl-* repo, Claude must also generate an HTML walkthrough and commit it separately.

Output path: ftl-docs/pr-reports/<branch-slug>-<sha7>.html

Audience: a 1st-year B.Tech IT intern in India — define every acronym on first use, lead with why the commit exists, and keep sentences under 18 words.

Required sections (in order): 1. Heading block (commit subject, SHA, branch, repo, author, date, files changed) 2. TL;DR for an intern 3. Background — what you need to know first 4. What changed, file by file (code commits only; skip for docs-only commits) 5. Documentation Changes (if any ftl-docs/ files were touched, excluding ftl-docs/pr-reports/) 6. How to verify locally (renamed "How to read this doc" for docs-only commits) 7. Glossary 8. Further reading

Follow-up commit: after generating the HTML, stage only ftl-docs/pr-reports/ and create a new commit:

docs(pr-reports): add walkthrough for <sha7>

Never amend the original commit.

Skip for: merge commits, no-op amends, and human-authored commits. Claude only documents its own commits.

See ftl-docs/PrInstructions.md for the full spec, including manifest.json schema, sidebar layout, and styling requirements.

Running tests

# Backend — from ftl-backend/
go test -race ./...

# Frontend E2E (Playwright) — requires the stack to be running
bash ftl.sh start
npx playwright test

Always run go test -race ./... before pushing a backend branch. The race detector catches concurrency bugs that are invisible under normal test runs.

Pre-launch security checklist (key items)

Work through the full checklist in CLAUDE.md before cutting over to production. The three highest-risk items:

  1. Rotate .env secrets before the first production deploy. The Google OAuth client secret, Sportmonks API key, JWT keypair, and REG_TICKET_SECRET may have been exposed during development. Regenerate JWT keys with ftl-backend/scripts/gen-dev-keys.sh.

  2. Verify DISABLE_RATE_LIMIT is unset in the production Container App environment. The local ftl.sh orchestrator sets it to 1 for load testing. Leaving it on in production disables all rate limiting on the auth surface and referral endpoints.

  3. Confirm activeRevisionsMode == "Single" for every Container App after any scale operation. Multiple-revision mode left 9 always-warm replicas running in staging, burning ₹1,234/day instead of the expected ₹150/day (see ftl-docs/reports/azure-cost-audit-2026-05-02.md). Check with:

    AZURE_CONFIG_DIR=~/.azure-ftl az containerapp show \
      -n ftl-stg-api -g ftl-stg-rg-cin \
      --query 'properties.configuration.activeRevisionsMode'