Skip to content
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ server/
app.ts Hono factory
worker.ts CF Workers entry (prod)
index.ts Legacy Express entry (standalone dev — kept for reference)
routes/ 22 resource-per-file modules. OpenAPI at /api/v1/documentation
routes/ 22 resource-per-file modules. Resource routes are mounted at /api/<resource> (no /v1 prefix). Only operational/meta routes (status, metrics, documentation) live under /api/v1/. OpenAPI spec at /api/v1/documentation is partial (covers a subset — see `server/routes/docs.ts`); treat `server/app.ts` route mounts as the source of truth until the spec is completed.
middleware/ auth (hybridAuth), tenant, error
storage/ SystemStorage — single source of DB access
db/ Neon HTTP connection
Expand Down
84 changes: 84 additions & 0 deletions docs/contracts/chittybooks-chittyfinance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# ChittyBooks ↔ ChittyFinance Contract

> Engine / UI boundary. Not canon yet — proposal for the pentad.

## Status (verified 2026-05-27)

| Surface | Repo | Deploy | Health |
|---|---|---|---|
| ChittyFinance (engine) | `CHITTYAPPS/chittyfinance` | Hono on CF Workers at `finance.chitty.cc` | `200 ok` |
| ChittyBooks (UI/app) | `CHITTYAPPS/chittybooks` | Python Flask, `main.py`, Dockerfile, `.replit` → `cloudrun` | **Not deployed**. `books.chitty.cc` does not resolve. |
| ChittyLedger (substrate) | `CHITTYFOUNDATION/chittyledger` | Worker at `ledger.chitty.cc` | `200 ok` |
| ChittyLedger (legacy fork) | `CHITTYOS/chittyledger` | Express + React, in-memory, not deployed | n/a — DUPLICATE, candidate for retirement or repurpose as ChittyLedger-Evidence seed (see `docs/proposals/chittyledger-naming-plan.md`) |

`CHITTYAPPS/chittybooks/CHARTER.md` claims a Cloudflare Worker at `books.chitty.cc`. The repository is a Python Flask application with `deploymentTarget = "cloudrun"`. Charter and code disagree.

## Boundary

- **ChittyFinance is the finance engine.** It owns: tenants, properties, accounts, transactions, allocations, classification (COA L0–L4), reports, valuation, AI summarization, OAuth connectors (Wave, Stripe, Google, Mercury via proxy), and webhooks. It writes evidence-grade entries to ChittyLedger.
- **ChittyBooks is a bookkeeping UI/app.** It does **not** own bookkeeping records. It is a thin surface that reads ChittyFinance and (where applicable) ChittyLedger-Finance projections.
- **ChittyLedger is the substrate.** ChittyLedger-Finance and ChittyLedger-Evidence are projections of it. See `docs/chittyledger-finance-design.md` for the canonical projection design.

## Source of Truth (no competing writers)

| Resource | Writer | Reader |
|---|---|---|
| `tenants`, `properties`, `accounts`, `transactions`, `allocations`, `classifications` | ChittyFinance | ChittyBooks (read-only), ChittyCommand, exports |
| `financial_documents`, `financial_facts`, `reconciliation_conflicts` | ChittyFinance writes into the ChittyLedger-Finance projection via `POST https://ledger.chitty.cc/api/entries` (matches `CHITTY.md` and `docs/proposals/chittyledger-naming-plan.md`). ChittyTrace ingest is an upstream feeder that hands material to ChittyFinance — it is not a second writer to the projection. | ChittyFinance, ChittyBooks |
| Mercury/Wave/Stripe/Plaid raw events | external | ChittyFinance webhooks |

ChittyBooks MUST NOT write transactions, allocations, or COA classifications directly. All mutations route to ChittyFinance.

## API Surface ChittyBooks consumes

All paths are under `https://finance.chitty.cc`. Auth (per `server/middleware/auth.ts` hybridAuth): either a service `Authorization: Bearer <CHITTY_AUTH_SERVICE_TOKEN>` header (with `X-Chitty-User-Id`) **or** a ChittyAuth-issued JWT delivered as the session cookie (`jwt:` prefix) — Bearer is service-to-service only; user/browser callers use the cookie path. Tenant scoping is resolved server-side from session/JWT claims (`c.var.tenantId`); path params are not trusted.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Document the tenant selector protected routes require

For protected resource routes, server/app.ts runs tenantMiddleware after hybridAuth, and tenantMiddleware returns 400 unless the request includes X-Tenant-ID or tenantId query; the JWT/session path only resolves userId, not c.var.tenantId. A ChittyBooks caller following this auth paragraph with only the service token + X-Chitty-User-Id or a session cookie will still fail every /api/accounts, /api/transactions, etc. request until it supplies the tenant selector the middleware actually requires.

Useful? React with 👍 / 👎.


| Path (verified mounted) | Purpose for ChittyBooks |
|---|---|
| `/api/tenants` | List tenants the caller can read |
| `/api/properties` | Property list |
| `/api/accounts` | Account list + balances |
| `/api/transactions` | Transaction feed for caller's tenant. Today the mounted handler accepts only `?limit=`; `?accountId=` and `?since=` are accepted on `/api/transactions/export`. Any source/date filter parity is a follow-up, not a current guarantee. |
| `/api/allocations/rules`, `/api/allocations/preview`, `/api/allocations/execute`, `/api/allocations/run` | Allocation rule CRUD + dry-run + execution (per `server/accounting/allocations.ts`). There is no umbrella `/api/allocations` index route. |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the mounted allocations runs path

Fresh evidence after the prior thread: server/accounting/allocations.ts and CHARTER.md both expose the run-list/update surface as /api/allocations/runs, but this contract still advertises singular /api/allocations/run. ChittyBooks clients using the verified-mounted table will call a 404 for allocation run history/status operations.

Useful? React with 👍 / 👎.

| `/api/classification` | COA assignments + audit trail |
| `/api/reports/consolidated`, `/api/workflows/close-tax-automation` | Pre-aggregated bookkeeping/close views currently mounted under `/api/reports/*` and `/api/workflows/*` (per `server/accounting/reports.ts`). A `/api/reports/reconciliation` endpoint is **proposed**, not yet mounted. |
| `/api/integrations/status` | Per-provider `configured` boolean derived from env vars (see `server/routes/integrations.ts`). Last-sync timestamps are a proposed extension, not part of the current response. |
| `/api/v1/documentation` | OpenAPI 3.0 spec (note: only the docs route is under `/api/v1`; data routes are under `/api`. See `docs/proposals/api-v1-prefix-fix.md`.) |

## ChittyLedger projection paths (read-only)

ChittyBooks reads ChittyLedger-Finance projection tables via `ChittyFinance` aggregator endpoints — it does not query ChittyLedger directly. This preserves the substrate boundary: ChittyLedger does not know about ChittyBooks. The aggregator endpoints (e.g. `/api/ledger/finance/*`) are **proposed** — none are mounted yet. They must be defined and shipped in ChittyFinance before ChittyBooks relies on them. Until then, ChittyBooks reads projection data only through the existing mounted routes above.

## Deploy decision (2026-05-27)

**Retired: the assumption that `books.chitty.cc` is a live API.** The domain does not resolve and no Worker exists. Code paths that target it MUST be disabled or guarded (see `docs/proposals/ch1tty-connector-revision.md`).

**Not yet decided: the actual ChittyBooks deploy path.** That decision is an explicit operator gate. Until the operator chooses container / worker-port / merged-into-finance, ChittyBooks stays as repo-only — a candidate UI/workflow surface, not a runtime.

Proof from repo files for the fake-domain retirement (no inference):

| Evidence | Source | What it proves |
|---|---|---|
| Repo is Python Flask, not Worker | `CHITTYAPPS/chittybooks/main.py` (43 KB), `Dockerfile`, `pyproject.toml`, `wsgi.py` | Charter ↔ code mismatch |
| Replit config targets Cloud Run, not CF | `CHITTYAPPS/chittybooks/.replit` lines `deploymentTarget = "cloudrun"` and `[[ports]] localPort = 5000 externalPort = 80` | Never built as a Worker |
| No `wrangler.toml`/`wrangler.jsonc` in repo | `find CHITTYAPPS/chittybooks -maxdepth 2 -name 'wrangler*'` returns empty | No CF deploy path exists |
| No DNS for the claimed domain | `dig books.chitty.cc` → NXDOMAIN (verified via `curl: Could not resolve host`) | Charter URL is aspirational |
| Bookkeeping engine already complete in ChittyFinance | `CHITTYAPPS/chittyfinance/server/routes/{allocations,classification,reports,tax,portfolio,charges}.ts` | No engine gap requires a second service |
| Per-tenant `tenantId NOT NULL` at schema | `database/system.schema.ts:103` | Multi-tenant boundary is in ChittyFinance, not in ChittyBooks |

Options preserved for the operator (deploy gate — not decided here):
- **Option A — Cloud Run container.** Matches existing `.replit` config. Justification gap: ChittyFinance already owns bookkeeping engine; risk of competing source of truth.
- **Option B — Worker rewrite.** Same competition risk + significant rewrite cost.
- **Option C — Merged UI surface served by ChittyFinance.** Lowest cost, no new runtime. Default if no operator decision is made.

## Followup actions (operator-gated, not auto-merged)

1. Pick A/B/C above. Each requires explicit approval.
2. Reconcile `CHITTYAPPS/chittybooks/CHARTER.md` and `CHITTY.md` once the choice is made (currently both claim a Worker at `books.chitty.cc`, which is false).
3. Patch `CHITTYOS/chittycommand` so its `booksClient` does not call the dead `CHITTYBOOKS_URL` (see `docs/proposals/ch1tty-connector-revision.md`). This is the only auto-mergeable action in this branch.

## Deploy gates

- [ ] PR approval before any `books.chitty.cc` DNS or Worker creation.
- [ ] CHARTER.md in `CHITTYAPPS/chittybooks` must be reconciled with whichever option is chosen (currently aspirational).
- [ ] `CHITTYOS/chittybooks` legacy fork is renamed, archived, or repurposed as ChittyLedger-Evidence — do not let it shadow the canonical surface.
68 changes: 68 additions & 0 deletions docs/contracts/mercury-multitenant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Mercury Multitenant Model

> How Mercury bank data flows through ChittyFinance with tenant + Legal Person isolation.

## Requirements (non-negotiable)

1. **`tenant_id` required on every Mercury-derived record.** No row, webhook event, or queue message lacks it. Cross-tenant reads are server-side blocked, not client-trusted.
2. **Legal Person ChittyID binding required.** Every Mercury account maps to exactly one Legal Person (`P-Legal` entity), recorded as `legal_person_chittyid` on the account row. Account ↔ Legal Person is many-to-one (one LLC can have many accounts).
3. **Wave business mapping required.** Each `(tenant_id, legal_person_chittyid)` pair has at most one Wave business. The intended persistence point is a `integration_account_links` table (source = `wave`, target = `mercury`) — **this table does not yet exist in `database/system.schema.ts`**, so the mapping is contract-only today. Interim implementations may store the link under `integrations.metadata` until the table lands via a coordinated schema cutover (see `CLAUDE.md` → "Schema Changes"). Unmapped Mercury accounts produce reconciliation conflicts once the surface is live, not silent omission.
4. **ChittyBooks reconciles over Mercury + Wave + Stripe** by reading ChittyFinance's reconciliation views. ChittyBooks never reaches into Mercury directly.

## Identity model

```text
Person (P-Legal, e.g. "IT CAN BE LLC")
│ legal_person_chittyid
└── Account (Mercury account #1, #2, ...)
│ tenant_id
└── Transaction (mercury txn rows)
│ metadata.mercury_kind, mercury_id
└── Allocation → Property/Lease (Business surface)
```

- **Business / Legalink separation**: business operations (property, lease, allocation) live on the Business surface; the Legal Person binding lives on the Authority/Person surface. They join through `legal_person_chittyid`, not by sharing schemas.

## Data sources

| Source | Path | Tenant binding |
|---|---|---|
| Mercury read API | `https://api.mercury.com/api/v1` (direct, OAuth tokens scoped per tenant) | Token issued per `(tenant_id, legal_person_chittyid)` via ChittyConnect |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Mercury reads behind ChittyConnect

This multitenant contract still names direct api.mercury.com reads, while the repo-level agent boundary says every Mercury Bank API call routes through ChittyConnect and server/routes/mercury.ts implements account reads via ${CHITTYCONNECT_API_BASE}/mercury/accounts. If implementers use this table for the Mercury read path, they bypass the static-egress/credential boundary and reintroduce direct Mercury token handling despite the connector doc being corrected elsewhere.

Useful? React with 👍 / 👎.

| Mercury write API | `mercury-proxy` on `chittyserv-dev` (IP-allowlisted by Mercury) | `X-Mercury-Token` per request; proxy is stateless re tenancy |
| Mercury webhooks | `POST /api/webhooks/mercury/:tenantId` on `finance.chitty.cc` (native Mercury receiver, per `server/books/webhooks.ts`; PR #113). Per-tenant HMAC secret keyed at `webhook:mercury:secret:<tenantId>` in KV. The legacy `POST /api/webhooks/mercury` path was the ChittyConnect-normalized shim and is not the native receiver. | `tenant_id` is the path param and is verified by HMAC; `legal_person_chittyid` resolution is **not** performed inside the webhook handler today — it joins downstream via the local account row. |

The write proxy at `CHITTYOS/mercury-proxy` is **not** a tenant boundary — it is a network egress shim. Tenancy is enforced by the caller (ChittyFinance) before reaching it.

## Reconciliation surface

ChittyBooks consumes these ChittyFinance endpoints, not raw Mercury. **Endpoints marked _(proposed)_ are not yet mounted** — they are the target surface for the reconciliation contract and must ship before ChittyBooks relies on them:

- `GET /api/transactions` — tenant-scoped transaction feed. Today the handler accepts only `?limit=`; `?source=mercury|wave|stripe` filtering is _(proposed)_ and must be added to `server/books/transactions.ts` before this contract is satisfied.
- `GET /api/reports/reconciliation?tenant_id=...&period=...` _(proposed)_ — three-way diff (Mercury ↔ Wave ↔ Stripe). Not mounted; only `/api/reports/consolidated` exists today.
- `GET /api/integrations/status` — currently returns per-provider `configured` booleans from env vars (see `server/routes/integrations.ts`). Last-sync timestamps and per-source health _(proposed)_ require extending the handler to read sync state.

Conflicts surface as `reconciliation_conflicts` in ChittyLedger-Finance (see `docs/chittyledger-finance-design.md`).

## What is explicitly out of scope

- ChittyBooks may **not** call Mercury directly.
- ChittyBooks may **not** create Wave invoices/sales directly — those route through ChittyFinance.
- Credential rotation is owned by ChittyConnect concierge. ChittyBooks never sees a Mercury or Wave token.

## Adversarial findings (2026-05-27 verification)

**CRITICAL — schema gap.** `legal_person_chittyid` column is **NOT present** on `accounts` in `database/system.schema.ts` (verified at lines 101-103; only `id`, `tenantId`, and downstream columns exist). The Legal Person binding is therefore not enforceable at the database level today. Two remediation paths:

- **Path A (schema migration)** — add `legal_person_chittyid TEXT NOT NULL` to `accounts` with a backfill plan. Coordinated cutover required (no migrations in this repo per CLAUDE.md "Schema Changes"; `drizzle-kit push` is destructive).
- **Path B (interim metadata)** — store the binding in `accounts.metadata->>'legal_person_chittyid'` until Path A is ready. Adds a runtime check at the storage layer.

Until one of these lands, the Mercury multitenant contract is **partially enforced** (tenant_id yes, legal_person no). Reconciliation reports MUST flag this in their output until the gap closes.

**Verified OK.** `tenant_id` is `NOT NULL` on `accounts`, `transactions`, `properties`, `integrations` (`database/system.schema.ts`), so any insert missing `tenantId` fails at the database. The 18 inserts in `server/storage/system.ts` rely on the caller's `data` containing `tenantId`; the schema constraint provides defense-in-depth. No write path can bypass it.

## Deploy gates

- [ ] **BLOCKER**: `legal_person_chittyid` column or metadata interim must exist before reconciliation reports reference it. Until then, the column is contract-only.
- [ ] `legal_person_chittyid` column present on `accounts` (verify in `database/system.schema.ts` before reconciliation reports go live).
- [ ] Per-business Mercury webhook secret in place (already shipped — PR #113).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require Mercury webhook secrets before going live

The native webhook handler skips HMAC verification entirely when webhook:mercury:secret:<tenantId> is missing, but this deploy gate marks the per-business secret as already shipped instead of making secret presence a blocker. In any tenant where the KV secret was not actually stored, operators following this checklist can expose a native Mercury endpoint that accepts unsigned payloads, so the gate should require verifying the secret exists before enabling the webhook.

Useful? React with 👍 / 👎.

- [ ] No code path can write a Mercury-derived row with `tenant_id = NULL`. Enforce at the SystemStorage layer, not at the route layer.
32 changes: 32 additions & 0 deletions docs/proposals/api-v1-prefix-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# API path prefix — RESOLVED as non-issue

## Original report

> `https://finance.chitty.cc/api/v1/entities` → `404`. Docs imply `/api/v1/*` everywhere.

## Investigation (2026-05-27)

Two route families exist by design:

| Family | Mount | Examples | Verified |
|---|---|---|---|
| **Operational/meta** | `/api/v1/*` | `/api/v1/status`, `/api/v1/metrics`, `/api/v1/documentation` | `server/routes/health.ts:21,38`; `server/routes/docs.ts:7`. All return `200`. |
| **Resource data** | `/api/*` | `/api/accounts`, `/api/transactions`, `/api/properties`, `/api/tenants`, `/api/integrations/*`, `/api/reports/*`, etc. | `server/app.ts:74-118`; each route module under `server/routes/`. All return `401` (auth) — routes exist. |

`/api/v1/entities` returns `404` because **there is no `entities` resource**, not because of a path mismatch. The original concern came from assuming the `/api/v1` prefix applied to data routes; it does not.

## Decision: no code change

- The split (meta under `/v1`, resources unprefixed) is intentional and consistent with the OpenAPI spec at `/api/v1/documentation` which lists `paths` correctly.
- All 5 in-repo references to `/api/v1` (`.github/workflows/register.yml`, `deploy/registration/chittyfinance.registration.json`, `client/src/pages/Landing.tsx:342`, `.claude/commands/quick-deploy.md:28`, plans) point at real endpoints. **Nothing to fix.**
- All 2 cross-org consumer references (`chittycommand/src/lib/integrations.ts:249`, `chittyregistry/src/routes/notion-webhooks.ts`) target `/api/<resource>` — correct.

## Documentation patch (the only action)

Add a single sentence to `CLAUDE.md` under "Where Things Live" → routes section:

> Resource routes are mounted at `/api/<resource>` (no `/v1` prefix). Only operational/meta routes (`status`, `metrics`, `documentation`) live under `/api/v1/`. The OpenAPI spec at `/api/v1/documentation` is authoritative.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Stop calling the partial OpenAPI spec authoritative

Fresh evidence beyond the earlier CLAUDE.md finding: this proposal still instructs maintainers to add a sentence saying /api/v1/documentation is authoritative, but server/routes/docs.ts only lists a small subset of the mounted routes and the updated CLAUDE.md now correctly says to treat server/app.ts as the source of truth. Anyone following this proposal later can reintroduce the same bad guidance and cause consumers to omit supported APIs such as reports, imports, allocations, and classification.

Useful? React with 👍 / 👎.


## Deploy gates

- None. No routing or worker change.
Loading
Loading