Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

<!-- What changed and why. -->

Spec: specs/<file>
<!-- The spec this implements (specs/YYYY-MM-DD-<feature>.md) — or "N/A" with a one-line reason. -->
Spec: specs/<feature>
<!-- The spec this implements (specs/<feature> — see specs/README.md for the convention) — or "N/A" with a one-line reason. -->

## Test plan

Expand Down
5 changes: 4 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ on:
jobs:
deploy:
runs-on: ubuntu-latest
# Only on a successful CI run, and only for main.
# Only on a successful CI run of a push to main. The `event == 'push'` guard is
# load-bearing: without it, a fork PR opened from a branch named "main" satisfies
# `head_branch == 'main'` and would deploy the fork's commit with this repo's secrets.
if: >-
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.head_branch == 'main'
steps:
# When filling in: check out the exact commit CI tested — a `workflow_run`
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ dist/
build/
.output/
.cache/
coverage/
*.tgz
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# Environment
.env
Expand Down
102 changes: 41 additions & 61 deletions CLAUDE.md

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Cavalry

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
62 changes: 37 additions & 25 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion add-ons/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ A directory `add-ons/<name>/` with a `README.md` of agnostic guidance. `<name>`

## Opt in — adoption is keeping the directory

Keep the add-ons you want under `add-ons/`, delete the directories you don't. Opting out *is* deleting the directory; every directory still present is adopted. The Day-1 checklist (root `README.md`) is where a fresh project chooses.
Keep the add-ons you want under `add-ons/`; opting out *is* deleting the directory every directory still present is adopted. The Day-1 checklist (root `README.md`) is where a fresh project chooses.

Activation is by instruction: the root `CLAUDE.md` tells agents to read every kept add-on's `README.md` and follow it when touching the capability it covers. Add-ons are cross-cutting (backend + frontend + db at once), so the pointer lives in the always-loaded root file rather than a per-area one. The README under `add-ons/` is the single source of truth — edit it in place; there is no generated copy.

Expand Down
4 changes: 4 additions & 0 deletions add-ons/otp-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ One-time-code auth: a user proves control of a phone or email by entering a code

## Make it robust

- **Generate codes with a CSPRNG, minimum 6 digits** — never `Math.random()` or anything timestamp-derived.
- **A code is single-use** — consume it on a successful verify; a consumed code never verifies again.
- **TTL is minutes, not hours.**
- **Cap failed verify attempts per challenge** (e.g. 5); past the cap, invalidate the challenge and require a fresh send.
- **Idempotent verify.** A retry or double-submit must never create a second account or double-consume. Put a unique constraint on the natural key (target + purpose) so the race resolves to `409`, and have the client treat `409` as "already done, proceed".
- **A knowable test code.** Gate a knowable code behind **test mode** (a logged real code, or a fixed code valid *only* in test mode) so the flow is walkable without a live provider. The verify path still runs — only delivery is stubbed.
- **Log every send and verify** with `{purpose, masked target, test-mode, provider status, correlation id}` — never the code or full contact.
Expand Down
124 changes: 59 additions & 65 deletions apps/backend/CLAUDE.md

Large diffs are not rendered by default.

264 changes: 127 additions & 137 deletions apps/frontend/CLAUDE.md

Large diffs are not rendered by default.

28 changes: 15 additions & 13 deletions db/CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
# Database

The database contract — read before touching anything under `db/`. Repo-wide rules (principles, workflow, cross-app standards) live in the root `CLAUDE.md`. This file governs migrations and the seed/reset scripts that share this folder.
The database contract — read before touching anything under `db/`: the migrations in `db/migrations/` (its `README.md` marks the folder; they run via the root `migrate` command) and the seed/reset scripts beside them. Stack pack adopted? Read its `db.md` appendix first — precedence rules in `stacks/README.md`.

Migrations are one of the highest-risk surfaces in any project — irreversible data loss, table locks, ordering collisions, and prod/dev divergence all originate here. The rules below are checkable and client-agnostic (no specific migration tool is assumed). **If a stack pack is adopted (a single directory kept under `stacks/`), also read its `db.md` appendix before working here** — it binds these rules to the concrete tool, and its conflict register resolves any disagreement with this file, for that stack only.
## Migration rules

`db/` is the shared home for **migrations** (under `db/migrations/`) and related **seed/reset** scripts. The backend's repo-ring adapters read from the database at runtime (see `apps/backend/CLAUDE.md`); the migrations here are applied via the root `migrate` command (see root `CLAUDE.md`).
- **Ordered, timestamp-prefixed naming.** Name each file with a timestamp prefix and a short description (e.g. `20260601120000_add_orders_table`), monotonic and never reused — a timestamp, never a hand-incremented sequence, which parallel branches will both claim. Before merging, rebase onto trunk and confirm your migration still sorts after every migration already there.
- **Reversible, or justified.** Every migration pairs an `up` with a `down` OR carries an explicit irreversible-change justification comment. Never neither.
- **Never edit an applied migration.** Once merged or applied anywhere, a migration is immutable — fix forward with a new one.
- **Separate schema from data.** Keep schema migrations apart from data backfills; make backfills batched, idempotent, and resumable so an interrupted run never half-applies.
- **Expand → migrate → contract.** Split non-additive or destructive changes (DROP COLUMN/TABLE, NOT NULL on a populated table, type narrowing) across separate migrations/releases so a rollback never loses data: add the new shape, migrate onto it, remove the old shape once nothing reads it.
- **Prove the down path.** Before merging, run the migration up, then down, then up again on a throwaway scratch DB and confirm a clean round-trip — a down script present but untested proves nothing. State the evidence you observed.
- **Transactional where supported.** Run each migration in a transaction where the engine supports it, so a failure rolls back instead of leaving the schema half-changed.
- **Seed/reset are non-production only.** Seed and reset scripts are idempotent and run only against local/throwaway databases. Seed realistic, named accounts and content (not `user1`/`user2`) so manual and e2e testing exercises lifelike data; if the **test-mode** add-on is adopted, those accounts back its test-user picker (`add-ons/test-mode/`).

## Migration rules
## Schema rules

- **Ordered, timestamp-prefixed naming.** Name each file with a timestamp/sequence prefix and a short description (e.g. `20260601120000_add_orders_table`), monotonic and never reused. Use a timestamp, never a hand-incremented sequence — parallel branches must never both claim the same number. Before merging, rebase onto trunk and re-check that your migration still sorts after every migration already on trunk.
- **Reversible, or justified.** Every migration is reversible — an `up` paired with a `down` — OR carries an explicit irreversible-change justification comment. Never neither.
- **Never edit an applied migration.** Once a migration is merged or applied anywhere, treat it as immutable — fix forward with a new migration. Editing an applied migration is the single most common way an agent corrupts a shared or production database.
- **Separate schema from data.** Keep schema migrations apart from data backfills. Make backfills batched, idempotent, and resumable, so a large or interrupted run never half-applies.
- **Expand → migrate → contract for destructive changes.** For non-additive or destructive changes (DROP COLUMN/TABLE, NOT NULL on a populated table, type narrowing), split the work across separate migrations/releases so a rollback never loses data: add the new shape, migrate onto it, then remove the old shape once nothing reads it.
- **Prove the down path, not just the up.** Before merging, run the migration up, then down, then up again on a throwaway scratch DB and confirm a clean round-trip — don't rely on the down script being present but untested. State the evidence you observed.
- **Transactional where supported.** Run each migration in a transaction where the engine supports it, so a failed migration rolls back instead of leaving the schema half-changed.
- **Seed/reset are non-production only.** Seed and reset scripts are idempotent and run only against local/throwaway databases — never against a shared or production database. Prefer **realistic, named** seed accounts and content (not `user1` / `user2`) so manual and e2e testing exercises lifelike data. (If the project adopts the **test-mode** add-on, those seeded accounts also back its test-user picker — see `add-ons/test-mode/`.)
- **Money and quantities are exact types** — integer minor units or fixed-precision decimal, never floats.
- **Every table carries created/updated timestamps** in UTC-aware types.
- **Unique constraints encode business invariants.** A uniqueness violation maps to the domain conflict error — HTTP 409 at the edge.

## Shared DB across worktrees

The local DB is **global state** shared across worktrees by a fixed name (see *Working in a git worktree* in root `CLAUDE.md`). A migration, reset, or seed run in one worktree changes the schema every other worktree's app depends on. Run round-trip and destructive checks against a throwaway DB, never the shared one, while a parallel worktree depends on the current schema.
The local DB is global state, shared across worktrees by a fixed name (worktree mechanics: root `CLAUDE.md`). A migration, reset, or seed run in one worktree changes the schema every other worktree's app depends on. Never run a reset or destructive check against the shared DB while a parallel worktree depends on the current schema — use a throwaway DB.
2 changes: 1 addition & 1 deletion db/migrations/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# migrations

Migrations live here — rules in `../CLAUDE.md`.
Migration files land here — empty until the first migration (see the Day-1 checklist in the root `README.md`). Rules: `../CLAUDE.md`.
15 changes: 7 additions & 8 deletions design/README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# design — UI mockup / design reference

**Reference only — not part of the buildable workspace.** Drop generated or
hand-made design mockups here and use them as the source for visual design, screen
inventory, copy, and flows. **Do not copy the mockup code** into the apps (its
framework is usually not the app stack). See the *UI mockup / design reference*
section in the root `CLAUDE.md`.
hand-made design mockups here; they are the source for visual design, screen
inventory, copy, and flows. **Never copy mockup code** into the apps (its
framework is usually not the app stack).

This folder also holds the **design guide** — `design-guide.html` + `tokens.css`,
the visual keystone confirmed before any UI work (`apps/frontend/CLAUDE.md` →
Expand All @@ -14,7 +13,7 @@ the visual keystone confirmed before any UI work (`apps/frontend/CLAUDE.md` →

Keep mockups findable so "point the relevant mockup at the spec" is mechanical, not a hunt.

- **Inventory table (below) is the index — keep it current** as screens are added. Columns: **screen** (semantic name) · **mockup file/folder** · **owning spec**. The *screen* name must match its name in the central route registry (`apps/frontend/CLAUDE.md`), so a screen, its mockup, and its URL cross-reference through the registry — which stays the **only** route→URL surface (the frontend file forbids a second list, so this table carries no route column).
- **The inventory table (below) is the index — keep it current** as screens are added. Columns: **screen** (semantic name) · **mockup file/folder** · **owning spec**. The *screen* name must match its name in the central route registry (`apps/frontend/CLAUDE.md`), so a screen, its mockup, and its URL cross-reference through the registry — which stays the **only** route→URL surface, so this table carries no route column.
- **One file or folder per screen, named by the screen's semantic name** (matching the route registry) — never by tool export names like `screen-3-final-v2`.
- **Flows** are shown by mockup ordering/links or an optional flow file — don't mandate separate flow diagrams.

Expand All @@ -24,7 +23,7 @@ Keep mockups findable so "point the relevant mockup at the spec" is mechanical,

## Building from a mockup

A screen's **initial build** is verified against its design reference — never declared done from reading the code. Mockups guide that first build only: once a screen is iterated and improved it drifts from the mockup by design, so **later changes are verified against the running app, not re-checked against the mockup**.
Mockups guide a screen's **initial build only**. Verify that build against the mockup by rendering and looking — never declare it done from reading the code. After the first build the screen drifts from the mockup by design: **later changes are verified against the running app, not the mockup.**

- **With a mockup — verify, don't assume.** After first building a screen that has a mockup, run the app and view the built screen at the project's declared primary form factor plus the responsive baseline's other end (e.g. mobile + desktop), compare against the mockup, and iterate until layout, spacing, visual hierarchy, and copy match. **Capture and actually look at the rendered output** (a screenshot or equivalent) — don't reason about the code and declare it done. This is the success marker for the screen's **initial build** (ties to root `CLAUDE.md` *Goal-driven execution*). The capture/visual-diff tool is a per-project choice; the view-and-compare-against-reference step is mandatory regardless of tool.
- **Without a mockup — don't invent silently.** For a non-trivial **new** screen with no mockup, sketch the screens/copy/flow in the feature's spec under `specs/` and get it approved there before building — reuse the existing spec gate, don't create a second approval process. For minor changes to an existing screen, build to the conventions in `apps/frontend/CLAUDE.md` and note in the PR that it was "built to convention, no mockup." Never improvise UI for a non-trivial new screen with no reference.
- **With a mockup — verify, don't assume.** After first building the screen, run the app and view it at the project's declared primary form factor plus the responsive baseline's other end (e.g. mobile + desktop); compare against the mockup and iterate until layout, spacing, visual hierarchy, and copy match. **Capture and actually look at the rendered output** (a screenshot or equivalent). The capture/visual-diff tool is a per-project choice; the view-and-compare step is mandatory.
- **Without a mockup — don't invent silently.** For a non-trivial **new** screen, sketch the screens/copy/flow in the feature's spec under `specs/` and get it approved there — reuse the spec gate, no second approval process. Never improvise UI for a non-trivial new screen with no reference. For minor changes to an existing screen, build to the conventions in `apps/frontend/CLAUDE.md` and note in the PR that it was "built to convention, no mockup."
Binary file added design/brand/cavalry-lockup-dark-1600.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added design/brand/cavalry-lockup-light-1600.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions design/brand/cavalry-mark-dark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions design/brand/cavalry-mark-light.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion design/design-guide.html
Original file line number Diff line number Diff line change
Expand Up @@ -342,11 +342,14 @@
<main>

<section id="intro">
<p class="kicker">Keystone · design guide</p>
<img src="brand/cavalry-mark-dark.svg" alt="Cavalry" width="44" height="44">
<p class="kicker">Keystone · design guide · by Cavalry</p>
<h1>Many apps. One design philosophy.</h1>
<p class="lede">The foundation for every product built from this template — the principles,
tokens, and rules that make ten different apps feel like one practice built them.
Components stay flexible per app; everything they're made of starts here.</p>
<p class="lede">Keystone is <strong>Cavalry</strong>'s design foundation, shipped as this
template's default system. Rebranding it is Day-1 work; the bones stay.</p>
<div class="chips">
<span class="chip">Cavalry palette</span>
<span class="chip">Token-driven</span>
Expand Down
Loading
Loading