forked from coracle/caravel
67 lines
5.1 KiB
Markdown
67 lines
5.1 KiB
Markdown
# Module map and layering (deep detail)
|
|
|
|
This is the lookup-depth companion to the SKILL.md "module map" section: the full per-module responsibility map, the exact `main()` bootstrap and spawn order, the layering direction with concrete traces, and the test-target note. It exists so the SKILL.md "where things live" section can stay prose-only.
|
|
|
|
## The flat module map
|
|
|
|
Everything lives flat under `backend/src`, one job per module. `backend` is a dual library+binary crate, so this same set of modules is declared in two roots: `lib.rs` declares them as `pub mod` (the library root, the public/canonical declaration) and `main.rs` re-declares them as private `mod` for the binary entry point:
|
|
|
|
- **`api`** — the router, the `Api` service container, the authorization helpers (`require_*`, `get_*_or_404`, `is_admin`), and the `AuthedPubkey` NIP-98 extractor.
|
|
- **`billing`** — the orchestrator: it composes the integration leaves against the DB to reconcile activity into invoice items, renew subscriptions, and collect payment.
|
|
- **`bitcoin`** — the Coinbase fiat↔msats conversion leaf.
|
|
- **`command`** — all writes, as free async fns: single-statement writes run directly on `db::pool()`, while multi-step writes run inside `db::with_tx`.
|
|
- **`db`** — the global `SqlitePool`, the activity broadcast channel, and `with_tx`.
|
|
- **`env`** — the config singleton.
|
|
- **`infra`** — the relay-sync reactor plus the zooid client.
|
|
- **`models`** — the domain and sqlite-row structs plus the relay status string constants.
|
|
- **`query`** — all reads, plus the hardcoded plans.
|
|
- **`robot`** — the service's Nostr identity and DM sender.
|
|
- **`routes/*`** — the HTTP handlers, grouped by resource (`identity`, `plans`, `tenants`, `relays`, `invoices`).
|
|
- **`stripe`** — the Stripe HTTP leaf.
|
|
- **`wallet`** — the NWC (Nostr Wallet Connect) leaf.
|
|
- **`web`** — the response envelope and its success/error builders.
|
|
|
|
Source: `lib.rs:1-14`, `main.rs:1-14`, and each module head.
|
|
|
|
## The `main()` bootstrap order
|
|
|
|
The order is strict, and each service is built from the ones before it:
|
|
|
|
1. `dotenvy::dotenv().ok()`
|
|
2. tracing setup
|
|
3. `env::init()` — loads and validates all config, panicking on any missing var
|
|
4. `db::init().await` — normalizes the sqlite URL, opens the pool, sets WAL, runs migrations, and installs the broadcast channel
|
|
5. `Robot::new().await` — builds the Nostr identity (and publishes it; see the integrations reference)
|
|
6. `Stripe::new()`
|
|
7. `Billing::new(robot.clone())`
|
|
8. `Api::new(billing, stripe, robot)`
|
|
|
|
Then it builds the router (`api.router().layer(cors)`), spawns `infra::start()` and `billing.start()` as detached tokio tasks, and only then binds `127.0.0.1:{SERVER_PORT}` and calls `axum::serve`. The HTTP server and the two workers run concurrently for the life of the process.
|
|
|
|
Source: `main.rs:27-67`.
|
|
|
|
## The layering direction
|
|
|
|
The call direction is: route handler → (when needed) `Api` authorization helpers (`require_admin` / `require_admin_or_tenant` / `require_tenant`) → `query` (reads) / `command` (writes) / `billing` (orchestration) → `db`. There is no single strict linear layer: a handler may skip the authz helpers entirely, and it may call integration leaves directly rather than going through `billing`. The integration leaves (`stripe`/`wallet`/`bitcoin`/`robot`) are composed in **two** places — `billing.rs` holds its own `stripe`/`wallet`/`robot` for the reconciliation loop, while `Api` holds `stripe` and `robot` that route handlers invoke directly — so the leaves are *not* composed exclusively by `billing.rs`.
|
|
|
|
Two concrete traces:
|
|
|
|
- **`create_tenant`** calls no `require_*` helper (only the `AuthedPubkey` extractor); it calls `query::get_tenant`, then `api.robot.fetch_nostr_name` and `api.stripe.create_customer` directly (two integration leaves, not via `billing`), then `command::create_tenant` (`routes/tenants.rs:76-109`).
|
|
- **`reconcile_invoice`** calls `query::get_invoice`, then `billing.ensure_bolt11_for_invoice` and `billing.attempt_payment` — here the handler delegates the multi-integration orchestration to `billing` rather than calling the leaves itself (`routes/invoices.rs:35-60`).
|
|
|
|
## Router assembly
|
|
|
|
`Api::router()` is the single place every route string is wired to a handler fn imported from `routes/*`, after which `Arc<Api>` is attached via `.with_state`. An endpoint therefore needs *both* a handler fn (in the matching `routes/*.rs`) and a `.route(...)` line (in `api.rs`) — two different files, both required (`api.rs:66-99`).
|
|
|
|
## The test-target note
|
|
|
|
`lib.rs` re-exports every module `pub` solely for an integration-test target that does not exist yet — there is no `backend/tests` dir — so nothing currently consumes those re-exports. If you add an integration-test crate, this is the surface it reads (`lib.rs:1-14`).
|
|
|
|
## Sources
|
|
|
|
- flat module list — `backend/src/lib.rs:1-14`, `backend/src/main.rs:1-14`, and each module head
|
|
- `main()` bootstrap and spawn order — `backend/src/main.rs:27-67`
|
|
- layering traces — `backend/src/routes/tenants.rs:76-109`, `backend/src/routes/invoices.rs:35-60`
|
|
- router assembly — `backend/src/api.rs:66-99`
|
|
- test-target note — `backend/src/lib.rs:1-14`
|