Architecture
Extends the free core’s architecture page, which sets out the three regions of state, the process tree and the seams. Read that first; this page covers only what Pro adds.
For what Pro promises, read Correctness. This page is the map.
1. What Pro owns
Pro owns eight tables and reads none of core’s directly:
| Table | Holds | Written by |
|---|---|---|
aurora_meter_outbox_items |
one delivery intent per subject, with its state, lease and fencing token |
the enqueue hook (inside core’s record transaction) and Outbox.Deliverer |
aurora_meter_usage_reports |
per (tenant, feature, period) high-water mark of what has been reported |
UsageReporter |
aurora_meter_source_cutovers |
the watermark at which a feature changed reporting source |
Cutover |
aurora_meter_reconciliation_items |
a difference a person has to resolve |
Reconcile and Outbox.Reconciler |
aurora_meter_credit_accounts |
the provider customer, the saved card, auto top-up state and the pending payment machine |
Pro.Credits |
aurora_meter_rollups |
daily and monthly aggregates for reporting |
Rollup |
aurora_meter_alerts |
one dedup row per quota-threshold crossing |
Alerts |
aurora_meter_audit_events |
one row per audited request |
the Plugs.AuditLog before-send hook |
2. Which direction the dependency runs
Core never references Pro. Pro reaches core only through core’s public API:
the facade, the Storage callbacks, AuroraMeter.Credits and Credits.Lots,
Plans, Subscriptions, Period, Clock and Operations. It copies no core
schema and queries no core table directly.
Pro implements four core behaviours:
-
AuroraMeter.Billing.Provider, so core knows who the customer is at Stripe; -
AuroraMeter.Exporter, as the Stripe meter-event exporter; -
AuroraMeter.Events.Outbox, which is how a committed fact becomes a delivery intent inside the same transaction; -
AuroraMeter.Period, as the subscription-aligned period source.
That is the whole surface. A host can remove Pro and keep every free capability.
3. The export path, end to end
a fact is committed a buffered window is staged
(core record transaction) (UsageReporter, on its schedule)
| |
+----------------> outbox item <-----------+
(pending)
|
Outbox.Deliverer
leases (token), calls Exporter.deliver/2
|
+-----------+-----------+-----------+-----------+
| | | | |
accepted retry uncertain rejected quarantined
| (backoff) | (terminal, (mapping, mode
| pending | visible) or period problem;
Outbox.Reconciler Pro.Recovery operator requeues)
| |
confirmed accepted | pending | rejected
Three properties of that picture are worth stating plainly, because they are what the shape is for.
Delivery is at least once, and no stronger than that. Each send carries an identifier
Stripe deduplicates on, so a retry of the same window does not bill those units
twice. A send whose answer never arrives becomes uncertain rather than being
retried blindly or dropped. Correctness I15.
Every transition is conditional on the lease token. A worker whose lease expired writes zero rows and is logged, rather than overwriting a newer worker’s result. A lease timestamp answers “when does this expire”; the fencing token answers “whose write wins”, and only the second is a correctness property.
The payload is immutable once enqueued. The item carries the customer, the meter, the quantity, the timestamp, the identifier, the feature and the period as they were when the intent was created. A later change to the mapping does not rewrite an item in flight; it quarantines the ones that no longer fit.
Outbox has the states and the columns. Usage reporting has the reporter’s half.
4. The payment path
A top-up is a Stripe Checkout Session or an off-session charge against a saved card. The rule the machine is built around: Stripe is the payment rail and the ledger is the source of truth.
- The pending payment parameters are persisted before the POST, so an unknown outcome can be resumed with the same idempotency key rather than guessed at.
- Once the PaymentIntent id is known, resumption is GET only: the machine never re-POSTs a charge whose intent it can look up.
- The grant is keyed on the PaymentIntent id under the wallet’s balance row lock, so a redelivered webhook cannot fund an account twice. Correctness I14.
- An empty search result is not proof that no charge occurred. Recovery says what to do instead.
Top-ups has the customer-facing behaviour.
5. Transaction boundaries Pro adds
The enqueue. Pro.Outbox.enqueue/2 runs inside core’s record transaction.
If your host transaction wraps the record/4 call, the outbox item rolls back
with everything else. That is deliberate: an export intent that outlives the
fact it exports is worse than one that never existed.
The webhook handler. Each handler takes an advisory lock on the tenant key
for the duration of its work, so two deliveries of the same event serialise
rather than racing. AuroraMeter.Pro.Credits.with_lock/2 is the same lock, made
public so a host can serialise its own work on the same key.
The audit log write. Outside the request’s transaction, after the response
is sent, in a task. A failure to write it is a Logger.warning, because the
audit log never breaks the request it is auditing.
6. What Pro does not do
- It does not count. Metering, quotas, plans, events and the credit ledger are all in the free core.
- It does not guarantee that a Stripe invoice is finalised or paid. It records the state Stripe reports and opens a reconciliation item for every difference it finds. Correctness I13.
- It does not resolve every unknown provider outcome on its own. Past the configured uncertainty horizon an item stays uncertain and needs a person. Recovery.
-
It does not repair drift.
Reconcilereports differences; a human closes them. Reconciliation.
7. Further reading
- Correctness, per invariant, with prerequisites and limits.
- Outbox, Usage reporting, Cutover, Reconciliation.
- Recovery, for an operation whose outcome is unknown.
- Operations, for running it.
- The free core’s guarantee page, which is where the contract for everything below Pro lives.