Files
caravel/.agents/skills/backend/references/module-map-and-layering.md
T
2026-06-02 15:11:19 -07:00

5.1 KiB

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