Skip to content

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

  1. No PRs pre-launch. Commit directly to cashless-v2 on the parent repo and every submodule. No feat/* branches.
  2. No Claude / Anthropic attribution in commits or PR bodies. Plain commit message only.
  3. No installs at the root. There is no root build system. Always cd into the submodule first.
  4. DB is MySQL 8.0 on Cloud SQL. Not Postgres, not Drizzle — those defaults are aspirational for greenfield services.
  5. Mobile SDK is cashless-core-rs (Rust + UniFFI) only. The KMP cashless-sdk was 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-core and models are 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 _internal paths.
  • cashless-core-ui is the shared Angular library. New shared components, pipes, services for the web frontends belong in core/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-backend and personalaccount-api ship as independent Helm releases. Cross-service calls go over HTTP through the cluster ingress and are authenticated via cashless-auth. There is no in-process SDK that lets you import handlers across services.
  • Inbound Stripe webhooks live in pa_v4 only. cm_v2 (backend/cashless-backend/apiserver/cm_v2) has no inbound webhook endpoints by design — that's pinned by test_no_stripe_webhook_endpoint. account.updatedVendor.kyc_status is already wired at pa_v4/routers/stripe_webhook.py. Don't add a duplicate handler in cm_v2.
  • PCI scope is cashless-pos only. Stripe Terminal SDK + WisePOS E binding never appears in cashless-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 via scripts/build-android.sh / scripts/build-ios.sh. Don't add new code to the archived KMP cashless-sdk repo — 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.