AI-agent invariants¶
This codebase is primarily developed by AI agents. Claude Code, Codex, Cursor,
Continue, Aider — whichever tool you drive, the same load-bearing rules apply.
This page surfaces the invariants encoded in the parent
CLAUDE.md
and each submodule's CLAUDE.md so an agent that has only read the public docs
still gets the destructive-action guardrails right.
If you only read one section, read TL;DR and the two !!! danger blocks.
TL;DR — the top five¶
- No PRs pre-launch. Commit directly to
cashless-v2on the parent repo and every submodule. Nofeat/*branches. - No Claude / Anthropic attribution in commits or PR bodies. Plain commit message only.
- No installs at the root. There is no root build system. Always
cdinto the submodule first. - DB is MySQL 8.0 on Cloud SQL. Not Postgres, not Drizzle — those defaults are aspirational for greenfield services.
- Mobile SDK is
cashless-core-rs(Rust + UniFFI) only. The KMPcashless-sdkwas decommissioned 2026-05-08.
Hard rules¶
No Claude / Anthropic attribution in commits or PR bodies
Do not add Co-Authored-By: Claude … (or any Claude / Anthropic
trailer) to commit messages, PR bodies, or generated docs. Plain commit
message only. This is a user-set global policy that overrides the default
Claude Code commit template — when dispatching subagents that will commit,
pass the instruction through in the subagent prompt.
No pull requests pre-launch
cashless-v2 has never shipped to production. Until launch, push commits
directly to cashless-v2 in whatever repo the change belongs to. Do not
open PRs, do not create feat/* / fix/* branches for intermediate work,
do not split work across review rounds. Feature branches and PR reviews
resume after launch. Codified in
CLAUDE.md.
| Rule | What it means |
|---|---|
Branch is cashless-v2 everywhere |
Parent monorepo + every submodule. If a submodule lacks the branch, create it from develop / master and push. |
| No root installs | npm install, pip install, gradle build at the repo root all fail. cd into the specific submodule first. |
| Submodule edits commit in-submodule | cd into the submodule, commit + push to its cashless-v2, then bump the pointer in the parent repo as a separate commit. |
| Submodule pointer bumps are atomic | Use git commit --only -- <paths> so a sibling agent's git add cannot leak files into your commit. |
| Pre-commit hooks are not optional | Don't pass --no-verify. If a hook fails, fix the underlying issue. |
The parent repo + deploy/ + services/ live directly in the tree (no
submodule), so commits there are normal commits. Everywhere else, the
submodule-first flow is mandatory.
Stack truths that contradict global defaults¶
The global stack defaults are aspirational
~/.claude/CLAUDE.md lists Drizzle + PostgreSQL + Bun as preferred stack
defaults. Those describe future-state preferences for new greenfield
services, not current production. The table below is what the cashless
platform actually runs on cashless-v2 today.
| What you'll see in defaults | What this codebase actually uses |
|---|---|
| Postgres / Drizzle | MySQL 8.0 on Cloud SQL (mycashless-pa-development:us-central1:cm1). Cloud SQL Proxy sidecar on :3306. |
| Generic Angular | Angular 21 + ESLint 9 + Jest + Playwright on cashless-v2 for all four web frontends. |
| KMP / multiplatform SDK | cashless-core-rs (Rust + UniFFI → Kotlin / Swift). KMP cashless-sdk was retired 2026-05-08 and is no longer a submodule. |
| Stripe + BrainTree + … | Stripe + Mercado Pago + Adyen + direct PayPal. BrainTree was dropped; legacy pa/v3/braintree.py survives only as a 30-day post-launch fallback. |
| Postgres syntax in SQL | MySQL 8.0 syntax — no CREATE INDEX IF NOT EXISTS, no RETURNING, no JSONB. Use information_schema.statistics to gate index creation. |
pg_dump in DR |
mysqldump + gcloud sql backups restore + gcloud sql instances clone --point-in-time. |
Do not trust frontend submodules' develop / master branches — those
still carry legacy Angular 7 + TSLint. The integration target is always
cashless-v2.
Per-submodule invariants¶
One row per submodule that ships its own CLAUDE.md. If you touch the
submodule, read its full CLAUDE.md before writing code — these are
1-line reminders, not substitutes.
| Submodule | "If you touch this, remember…" |
|---|---|
| parent monorepo | Branch is cashless-v2; no PRs, no Claude attribution, no root installs. See hard rules. |
sdk/cashless-core-rs |
No reqwest, no tokio::spawn, no unwrap() in library code. HttpClient is a trait — platform injects the impl. Amounts are i64 cents. Per-crate cargo llvm-cov gates in CI; ratchet via scripts/coverage-thresholds.json. (Lives in-tree, not a submodule.) |
apps/cashless-pos |
PCI scope lives here only. Stripe Terminal SDK + WisePOS E binding never appears in cashless-event-ops. Rust SDK is the only network entry point — no Retrofit / OkHttp from screens. Compose Material 3 + Hilt only. Receipts are ESC/POS — iText is forbidden. (Lives in-tree, not a submodule.) |
apps/cashless-event-ops |
Out of PCI scope — never bind Stripe Terminal here. Role-gated home (HomeScreen.selectRoleTiles(roles)) is the only access control; don't rip the filter out. All scanners (NFC, CameraX, Telpo) flow through CashlessScanner. (Lives in-tree, not a submodule.) |
backend/personalaccount-api / pa_v4 |
register_charge_result is the post-payment finaliser for every processor webhook (Stripe, MP, Adyen, PayPal). Wrap the call in a single transaction — never db.commit() inside services/reload/, never begin_nested(), never autocommit, never catch-and-drop the exception. Atomicity is what makes re-delivery dedup safe. |
Submodules without a CLAUDE.md (cashless-backend, cashless-core,
cashless-models, cashless-auth, the four Angular frontends, the legacy
Java mobile apps) inherit the parent monorepo's rules — branch policy, commit
style, no root installs.
Operational gotchas¶
These have all bitten before. Each row links to the memory note where the incident is recorded if you need the full forensics.
| Symptom | Root cause / fix |
|---|---|
ArgoCD says Synced but the live Deployment is stuck on 1.0.0 |
In a multi-source Application with a $values ref, spec.sources[N].helm.parameters does not override image.tag — the value-file's empty string wins. Pin image.tag in deploy/argocd/values/<svc>.yaml directly. |
| Per-service CI doesn't run after a submodule pointer-bump | paths: <sub>/** globs do not match the gitlink itself. Add the bare path as a second entry: paths: ["<sub>", "<sub>/**"]. Fixed across all 13 per-service workflows 2026-05-12; replicate the pattern for any new workflow. |
| Two agents racing on the parent monorepo land each other's files in their commits | The shared working tree means a sibling git add can land between yours. Use git commit --only -- <paths> (pathspec-atomic) and verify with git diff --cached --stat before committing. git commit -a / git add -A definitely picks up drift. |
git commit --only -- <paths> -m "msg" fails with "no such pathspec" |
Once -- appears every following token is a pathspec, so -m becomes a path. Put -m (or -F .git/msg.txt) before --. |
git push against github.com hangs forever, but nc github.com 22 succeeds |
Stale SSH ControlMaster socket. pkill -9 -f "ssh git@github.com" then rm -f ~/.ssh/sockets/git@github.com-22* and push again. Retrying without clearing the socket won't unstick it. |
| Parallel agents on the same submodule thrash the index / silently switch branches | Use per-task git worktrees: git -C <submodule> worktree add .claude/worktrees/<name> -b agent/<task> cashless-v2. The orchestrator FF-merges the branch into cashless-v2 and bumps the parent pointer. |
| You bumped the parent pointer and CI picked up unrelated changes | Always run git diff --cached --stat before a pointer-bump commit. If sibling files leaked, git reset --soft HEAD~1 and re-stage with git commit --only -- <submodule-path>. |
Service boundaries that AI keeps getting wrong¶
These are the drift hazards a freshly-spawned agent runs into most often.
cashless-coreandmodelsare libraries. Treat them as third-party packages — never reach into their internals from a service. New shared helpers go into the library and get released; new service code does not import from_internalpaths.cashless-core-uiis the shared Angular library. New shared components, pipes, services for the web frontends belong incore/cashless-core-ui/(published as@cashless-media/core), not inside any one of the four frontends. Copy-pasting between frontends is a smell.- No shared runtime between backend services.
cashless-backendandpersonalaccount-apiship as independent Helm releases. Cross-service calls go over HTTP through the cluster ingress and are authenticated viacashless-auth. There is no in-process SDK that lets you import handlers across services. - Inbound Stripe webhooks live in
pa_v4only.cm_v2(backend/cashless-backend/apiserver/cm_v2) has no inbound webhook endpoints by design — that's pinned bytest_no_stripe_webhook_endpoint.account.updated→Vendor.kyc_statusis already wired atpa_v4/routers/stripe_webhook.py. Don't add a duplicate handler incm_v2. - PCI scope is
cashless-posonly. Stripe Terminal SDK + WisePOS E binding never appears incashless-event-ops(or anywhere else). Putting it elsewhere pulls that app into PCI cert-recert. - Mobile SDK changes go through Rust. Modify
sdk/cashless-core-rs/crates/<…>, then regenerate UniFFI bindings viascripts/build-android.sh/scripts/build-ios.sh. Don't add new code to the archived KMPcashless-sdkrepo — it's no longer a submodule.
Where to look first¶
| If you want to… | Start at |
|---|---|
| Understand the monorepo + branching policy | Parent CLAUDE.md |
| Debug a deploy / understand the CI → GHCR → ArgoCD flow | OPERATIONS.md |
| Run end-to-end Playwright / integration suites locally | E2E.md |
| Modify a backend, mobile app, or the Rust SDK | The submodule's own CLAUDE.md (see per-submodule invariants) |
| Bootstrap a fresh cluster | services/cluster-bootstrap/README.md |
| Understand active phase work, payment-port briefs, migration handoff | .planning/ in the parent monorepo (not checked into the docs site — read it from the cloned repo) |
| Bring up a service locally | Getting started |
Find every public host on dev2.mycashless.com |
Services |
| Hit a backend HTTP surface | API reference |
When in doubt, the closest CLAUDE.md wins over the docs site, and the
parent CLAUDE.md wins over the global ~/.claude/CLAUDE.md defaults. Read
top-down: global → parent → submodule → this page.