forked from coracle/caravel
3.7 KiB
3.7 KiB
Billing Architecture (Agreed)
This document summarizes the agreed billing architecture for Caravel backend.
Billing model
- Usage-based billing: sats/hour for relay operation.
- A relay is billable when it is provisioned/active in lifecycle terms.
- Billing is monthly, with a rolling cycle anchored to tenant signup.
- One consolidated invoice per tenant per billing period.
Metering and lifecycle
- Add an append-only lifecycle event table in the backend database.
- Events are the source for usage computation.
- Canonical event timestamp field name:
created_at(UTC). - Lifecycle behavior is treated as a state machine for billing math (idempotent outcomes for repeated/no-op transitions).
- Transition validation is permissive (any transition can be recorded); billing logic interprets sequences.
- Billable time behavior:
- Start on
provisioned - Pause on
suspended - Stop on
deactivated - Resume immediately on unsuspend
- Start on
Pricing
- Price is per relay plan/tier in sats/hour.
- Rates are stored in a mutable
planstable (current rate only). - Mid-cycle plan changes are billed by time spent in each plan.
- Plan rate changes are retroactive for un-invoiced usage in the current open period.
Rounding and minimums
- Round usage up to the next full hour.
- Minimum charge: 1 billable hour per relay per month.
Invoice generation
- A periodic worker creates invoices at billing boundaries.
- Existing relays at launch start billing from launch timestamp only (no historical backfill).
- Avoid duplicate invoices with a DB unique constraint on:
(tenant, period_start, period_end)
Invoice status and attempts
invoice_attemptsis the canonical history/state source.invoices.statusis a synchronous projection updated in the same transaction as attempt writes.- Each payment method attempt is its own row in
invoice_attempts. - Attempts in a single retry pass share a
run_idUUID.
Collection order and fallback
For each invoice collection run:
- Try NWC auto-pay
- If not paid, try Stripe auto-pay
- If still unpaid/unavailable, create Lightning invoice and show QR in-app
- If neither NWC nor Stripe is configured, send a one-time NIP-17 DM with invoice/subscription status
Notes:
- Retry cadence: every 24 hours (NWC/Stripe retries).
- Do not resend DMs on retries.
- Lightning invoice refresh is in-app only when prior invoice expires.
- DM send is recorded as an
invoice_attemptsrow (samerun_idas triggering run).
Due dates, grace, and enforcement
- Invoice due time is derived as:
invoice.created_at + 7 days. - Grace period: 7 days, relay service remains fully active during grace.
- If still unpaid after grace, billing flow marks tenant/account past due and performs billing-side handling.
- Full outstanding balance must be paid before billing status is considered clear.
Tenant and integration storage
- Store billing cycle anchor on
tenants(e.g.,billing_anchor_at). - Anchor can be reset when tenant goes from no non-free relays to having one again.
- Determine “has billable relays” by querying relays on demand (no counter cache).
- Keep NWC config in
tenants.nwc_url. - Store Stripe IDs directly on
tenants.
Worker and runtime model
- Scheduler runs inside backend service process.
- Multiple instances may run; correctness relies on DB idempotency and unique constraints.
Repository impact
- Add migration(s) for lifecycle events and billing-related schema changes.
- Add repository methods in
backend/src/repo.rsfor:- writing lifecycle events
- reading lifecycle events by relay/tenant/time window
- creating/fetching invoices with period boundaries
- writing invoice attempts and projecting invoice status atomically