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, theApiservice container, the authorization helpers (require_*,get_*_or_404,is_admin), and theAuthedPubkeyNIP-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 ondb::pool(), while multi-step writes run insidedb::with_tx.db— the globalSqlitePool, the activity broadcast channel, andwith_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:
dotenvy::dotenv().ok()- tracing setup
env::init()— loads and validates all config, panicking on any missing vardb::init().await— normalizes the sqlite URL, opens the pool, sets WAL, runs migrations, and installs the broadcast channelRobot::new().await— builds the Nostr identity (and publishes it; see the integrations reference)Stripe::new()Billing::new(robot.clone())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_tenantcalls norequire_*helper (only theAuthedPubkeyextractor); it callsquery::get_tenant, thenapi.robot.fetch_nostr_nameandapi.stripe.create_customerdirectly (two integration leaves, not viabilling), thencommand::create_tenant(routes/tenants.rs:76-109).reconcile_invoicecallsquery::get_invoice, thenbilling.ensure_bolt11_for_invoiceandbilling.attempt_payment— here the handler delegates the multi-integration orchestration tobillingrather 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