A regulated retail-investing platform — USD, US-listed equities and bonds. Customers pass identity checks, deposit from a linked bank account, buy into one of four model portfolios, and are rebalanced monthly. Positions are units valued daily; every movement is an immutable double-entry entry across cash and assets; trades settle T+1; every buy opens a tax lot. History is never rewritten — a late dividend, a split, or a corrected price restates the affected period's return while the as-published figure stays queryable.
Full brief: docs/requirements/project-description.md.
Every slot is labelled honestly, per
docs/requirements/real-vs-simulated-rules.md.
| Slot | Provider | Status |
|---|---|---|
| Brokerage / custody | Alpaca Paper Trading API | Planned live — order placement, fills, and positions are real against Alpaca's paper infrastructure. The per-customer account-approval verdict is simulated, because this product has no such verdict to be live against (ADR 21). |
| Market data | Alpaca | Planned live |
| Trading calendar | Alpaca | Planned live |
| Identity / KYC | Stripe Identity | Planned live (test mode) |
| Bank linking / ACH | Plaid | Planned live (sandbox) |
| Fee billing | Stripe Billing | Live (test mode) — POST /payment-methods genuinely calls Stripe's API; verified both via the real-adapter contract test (requires_credentials) and a live request against the deployed MVP |
| Custodian file | Built by us | Simulated — a deliberate simulator that generates the awkward cases (FR-33) |
"Planned live" means the adapter and its port exist and the contract suite runs against both the real adapter and its fake, but the sandbox credentials are not yet issued. Each row flips to live once its keys are in place and its contract tests run green against the real sandbox. Nothing is presented as live before that.
An MVP is deployed to Azure Container Apps (trueup-mvp-rg, centralus):
- Frontend:
https://frontend.proudmeadow-36949862.centralus.azurecontainerapps.io - Backend API:
https://backend.proudmeadow-36949862.centralus.azurecontainerapps.io(external ingress kept for webhook reachability; the frontend's own nginx proxies/apiand/healthto it over the environment's internal network, not the public FQDN — seefrontend/nginx.conf.template)
Resources: Postgres Flexible Server (trueup-mvp-pg, Burstable B1ms), Azure Container Registry
(trueupmvpacr), a Key Vault (trueup-mvp-kv) backing real envelope encryption via the backend's
system-assigned managed identity (no LOCAL_CIPHER_KEY in this environment), and Redis running as
an internal-only Container App (ADR 13/15: sessions and rate limits only, never financial state).
Deliberate MVP-speed shortcuts, not silent gaps — tracked here until hardened:
- Postgres allows public access from any IP (
--public-access 0.0.0.0-255.255.255.255) rather than VNet-integrated private access. - Three domains are still mocked client-side, honestly labelled "Simulated" in the UI: tax lots,
the admin customer directory/detail/KYC-override, and statement export. The backend has since
caught up —
/lots,/admin/customers*,/admin/kyc-overrides/<id>, and/statements/<period>/exportare all real, registered, tested routes — the frontend just hasn't been wired to them yet (docs/superpowers/specs/2026-09-05-frontend-completion-design.mdhas the full design for closing this). - No real-time SSE push (ADR 20/S12 §6) — every screen refetches on mount instead, the documented fallback.
- No new automated test coverage was added for this pass (explicit scope cut — ship fast, harden after); the exhaustive frontend test suite is tracked in the same design doc above.
- Alpaca and Plaid stay "Planned live" (unchanged this pass) — only Stripe Billing's
POST /payment-methodswas verified genuinely live this deployment, via Stripe's fixed test-mode payment-method tokens (pm_card_visaetc.), confirmed against the real Stripe test API both by therequires_credentialscontract test and a live request against the deployed URL.
- Design specs:
docs/specs/— S0 (foundation) through S12 (production operations). - Decisions:
docs/decisions/— one ADR per material architectural choice. - Requirements:
docs/requirements/requirements.md— 48 FRs, 18 NFRs, each traceable to the brief. - Build plan:
docs/delivery/backend-build-plan.md— how S0–S4 gets built, in what order, and what must be green before each step. - Decision log:
DECISION-LOG.md— timestamped record of what was decided, assumed, and cut. Distinct from the ADRs: those say why the architecture is shaped this way, this says what happened and when.
| Layer | Technology |
|---|---|
| Backend | Python 3.12 + Flask |
| ORM | SQLAlchemy 2 (no Flask-SQLAlchemy — the session belongs to the unit of work) |
| Database | PostgreSQL 16 |
| Migrations | Alembic |
| Frontend | React |
| Containers | Docker / Docker Compose |
| CI/CD | GitHub Actions |
| Cloud | Microsoft Azure |
Requires uv and Docker.
make up # PostgreSQL on :5433, Redis on :6380
make env # create .env from .env.example
make install # install backend dependencies
make migrate # apply migrations
make dev # run the API at http://localhost:5000Then fill in SECRET_KEY and LOCAL_CIPHER_KEY in .env:
python3 -c "import secrets; print(secrets.token_urlsafe(48))" # SECRET_KEY
python3 -c "import base64,os; print(base64.b64encode(os.urandom(32)).decode())" # LOCAL_CIPHER_KEYProvider credentials can stay blank — the app starts without them, and each real adapter refuses
to operate rather than silently substituting a fake. See
docs/project-helpers/service-accounts.md for what to
register and where each key comes from.
Ports 5433 and 6380 are deliberate, not typos: the defaults are avoided so Trueup cannot collide with another project's database on the same machine.
make check # everything CI runs: lint, types, import rules, tests
make test # the whole suite
make test-unit # value objects, cash policy, TWR math — no database
make test-integration # repositories, append-only guard, RLS policies
make test-api # controllers via the Flask test client
make test-contract # each port's real adapter against its fake, same suite
make migration M="..." # autogenerate a migrationmake help lists every target.
Tests run against real PostgreSQL, never SQLite. The design depends on CHECK constraint
semantics, revoked UPDATE/DELETE grants, Row-Level Security, NUMERIC precision,
FOR UPDATE SKIP LOCKED, and advisory locks — none of which SQLite has, so a suite passing
against SQLite would leave every one of those invariants unverified.
Three, not one, because S0 §7.3 makes bypassing Row-Level Security a credential boundary rather than application discipline:
| Role | Used by | BYPASSRLS |
|---|---|---|
trueup_owner |
Migrations only | no |
trueup_app |
The web API | no — structurally incapable of cross-tenant reads |
trueup_worker |
Jobs and the outbox worker | yes, by design |
trueup_chat_readonly |
S11's chat assistant tool calls only | no — and granted no table access at all except SELECT on the curated chat views (ADR 19) |
Read CLAUDE.md and backend/CLAUDE.md first. Work on a
feature_*, bugfix_*, or patch_* branch, and ship tests with every behaviour change.
Update CHANGELOG.md and DECISION-LOG.md in the same commit
as the work they describe. A decision log assembled retrospectively is worth nothing.