From the blog

Aurora Meter 1.0: Stable Usage Metering and Billing for Phoenix

By Liam Killingback ·

Aurora Meter 1.0: Stable Usage Metering and Billing for Phoenix

Aurora Meter 1.0.0 is out, and so is Aurora Meter Pro 1.0.0. Both were released on 22 September 2026.

The free MIT core, aurora_meter, is on Hex. Pro is in the private phxtemplates Hex organisation.

Until now Aurora Meter has been a 0.x library: useful, tested, and free to change its mind. From 1.0 it makes a promise instead. Every supported function, config key, telemetry event, PubSub message and migration is written down with a stability class. Nothing on that list breaks within 1.x.

This post covers four things:

  • what 1.0 means;
  • what is new in the core;
  • what is new in Pro;
  • how to upgrade from the versions you can actually install today (core 0.4.0 and Pro 0.3.0).
def deps do
  [
    {:aurora_meter, "~> 1.0"},
    # optional, commercial:
    {:aurora_meter_pro, "~> 1.0", organization: "phxtemplates"}
  ]
end

What “1.0” promises

A version number is only worth what it commits to, so here is the commitment in concrete terms.

An API inventory. Each package ships a docs/api.md that lists every supported entry point. Each entry carries one of four stability classes:

  • stable
  • optional-dep
  • deprecated
  • internal

If something is not on that page, it is not supported, even if the code happens to be public. A test checks the page against the code, so the inventory cannot quietly drift.

What SemVer covers within 1.x:

  • function names, arities, arguments and return shapes;
  • error tuples (not error message strings);
  • ledger semantics;
  • config key names, types and defaults;
  • telemetry event names and measurement keys;
  • PubSub message tags;
  • migration entry points.

Deprecated APIs warn, and they are not removed before 2.0.

Schema changes are additive. Every 1.x migration only adds, and every version can be rerun safely. A destructive down needs confirm_data_loss: true, so nobody drops a ledger by accident.

A guarantee page. The core’s docs/guarantees.md states fifteen guarantees. Each row gives the condition that makes it true, what takes it away, and the test that proves it. It is deliberately modest. Delivery is never claimed to be stronger than at-least-once, and no fixed data-loss window is promised for buffered counters. If you are going to put money through a library, that page is the one to read first.

Supported runtimes. CI runs these exact pairs on every push:

Elixir Erlang/OTP Notes
Floor 1.15.8 25.3.2.21 Pro needs OTP 26.2.5.21, because its Stripe HTTP stack requires it
Supported 1.18.5 27.3.4.17
Current 1.20.4 29.0.6

Postgres 16.13 is the tested version and 13 is the floor. The optional LiveView integration requires phoenix_live_view ~> 1.0.

What is new in the core (MIT)

The 0.x core counted, gated and held prepaid credit. 1.0 adds the parts you need once real invoices depend on those numbers.

Durable, idempotent usage events with corrections

AuroraMeter.record/4 writes a durable usage event. It requires an :id and an :occurred_at. A retry with the same :id is recognised as a duplicate, never counted as a second charge. record_batch/2 writes up to 500 events in one transaction.

When a recorded fact turns out to be wrong, AuroraMeter.correct/4 and replace/4 add a correction next to it. A correction never edits the original fact, so the audit trail stays intact.

Each event is stamped with the plan and plan version that applied when it was recorded. A later plan change therefore cannot reprice usage that has already happened. The durable events guide walks through it.

Versioned plans and scheduled transitions

Plans are now keyed by {id, version}, and a published version is immutable:

plan :pro, version: "2", effective_at: ~U[2026-10-01 00:00:00Z] do
  ...
end

Editing a published version in place raises AuroraMeter.PlanVersionConflictError. Changing price or limits means adding a new version, which is what you want when existing customers are on the old one.

AuroraMeter.Subscriptions can schedule a move between plans and apply it when it falls due:

  • schedule_transition/3
  • preview_transition/3
  • cancel_transition/3
  • apply_due_transitions/1

See changing a plan without repricing anybody.

Credit lots, debt, refunds and recurring allowances

The prepaid credit ledger now tracks individual lots (AuroraMeter.Credits.Lots). That makes it clear which grant paid for which spend.

  • Refunds reverse a specific lot (reverse_lot/4) and never touch promotional credit.
  • Debt is explicit: {:error, :debt_outstanding} is a real answer.
  • Balances now report spendable, debt and expired alongside available.

AuroraMeter.Plans.recurring_credits/2 gives a plan a monthly credit allowance, with capped rollover and expiry. The lots and recurring allowances guide covers the details.

Scheduling without lock-in

Scheduled work (credit expiry, hold reconciliation, recurring grants, plan transitions and retention) runs through AuroraMeter.Operations. It supports pause, resume and resumable cursors, and it does not depend on Oban.

If you do use Oban, optional AuroraMeter.Oban.* workers wrap each job. AuroraMeter.Oban.cron_entries/1 prints the crontab, and mix aurora_meter.install --oban sets it up.

Retention, observability and benchmarks

Retention. AuroraMeter.Retention prunes bookkeeping rows from a fixed list of tables. You can dry run it with plan/1 before calling prune/1.

Observability:

  • a LiveDashboard page that requires :authorized_by and shows no tenant data;
  • AuroraMeter.OpenTelemetry.attach/1, which adds no span to the hot path;
  • AuroraMeter.Telemetry.Metrics.metrics/1, ready for Telemetry.Metrics.

Benchmarks. mix aurora_meter.bench <mode> runs the benchmark suite and exits non-zero if a correctness check fails during a run. Measured on one machine, median of five runs:

Load shape Kind Median throughput
8 processes, distinct counters micro (in memory) 3,418,407 increments/s
8 processes, with_quota/4 micro (in memory) 423,658 ops/s
8 processes, durable record/4 end to end (Postgres) 1,804 events/s

“Micro” and “end to end” differ by three orders of magnitude, so read the kind before the number. The core README gives the hardware, and the evidence file is linked from there. Benchmark figures are not covered by SemVer.

What is new in Pro

Pro is the part that takes the money: Stripe subscriptions and checkout, usage reported to Stripe Billing Meters, credit top-ups, dashboards and alerts. For 1.0 the work went into making the path to Stripe something you can check, rather than something you have to trust.

An export outbox

Usage headed for Stripe is now staged in an outbox before AuroraMeter.Pro.Outbox.Deliverer sends it, with per-item retry and quarantine.

  • HTTP 429 is a retry, never a rejection.
  • No answer from Stripe marks the item uncertain. It is never resent under a new id, because a resend can double-charge.

Corrections that reach the invoice

A core correction flows through to Stripe as a cancellation followed by a re-send of the net amount. Pro never sends Stripe a negative meter event. A correction that arrives too late for that becomes a manual_adjustment item for a human to handle.

Reconciliation you can read

AuroraMeter.Pro.Outbox.Reconciler compares what Pro sent with Stripe’s own summaries, error reports and invoices. When they disagree, it raises a typed item, such as:

  • summary_shortfall
  • summary_excess
  • meter_error_report
  • unconfirmed_item
  • invoice_status

AuroraMeter.Pro.Reconcile.list_items/1 and resolve_item/3 give you a queue of disagreements to work through, rather than a number you hope is right. The outbox and reconciliation guide shows the workflow.

Guarded recovery

AuroraMeter.Pro.Recovery and mix aurora_meter_pro.recovery handle the rare manual fix. Every mutation takes three arguments:

  • expected:, what you believe the current state is;
  • reason:
  • evidence:

There is no force: option. A fix made at 2am should still say what it expected to find.

Plans that follow Stripe

Stripe price mappings are keyed by plan version. AuroraMeter.Pro.Transitions.schedule/3 changes a customer’s plan and records the change locally only once Stripe’s own state shows it. Proration defaults to :none, so no invoice line appears that you did not ask for.

Hardening

  • Every received webhook is recorded.
  • The Stripe mode and account are checked at boot, so test keys can’t meet a live webhook secret unnoticed.
  • The Stripe API version is pinned.
  • Configuration is validated against a schema, and errors show only a key prefix.
  • The customer usage dashboard now takes the tenant only from the server session.

That last one is a security fix, and it is the most important change in Pro. In 0.3 the mountable dashboard read the tenant from the URL, so a signed-in customer could read another tenant’s usage. If you mount AuroraMeter.Pro.Live.Dashboard, upgrade.

Upgrading

If you installed from Hex, you are on core 0.4.0 and Pro 0.3.0. A 0.5.0 core was prepared but never published to Hex, so you go straight to 1.0, and its behaviour changes all arrive at once. The upgrading to 1.0 guide is the full procedure. Here is the shape of it.

1. Pin the old defaults while you migrate. Two defaults changed and can be set back:

config :aurora_meter,
  undeclared_feature_policy: :allow,   # 1.0 default is :deny
  plan_version_conflict: :warn         # 1.0 default is :raise

The following now fail loudly, and cannot be switched back:

  • unknown config keys stop the app booting;
  • binary feature names raise;
  • an empty tenant key raises;
  • subscribing to an unknown plan returns {:error, changeset}.

Search your code for those before you deploy.

2. Migrate the core schema, in order. The core goes from schema version 6 to 10.

mix aurora_meter.gen.migration --upgrade -r MyApp.Repo
mix aurora_meter.events.backfill
mix aurora_meter.credits.migrate_lots

The guide says exactly where each command falls between the v7 to v10 migrations, and which writers to stop first. Two warnings from it:

  • Lot migration can’t be undone. Every node must be on 1.0 before you run migrate_lots, and there is no rollback after it.
  • Lock time and disk space. On a million events, the v7 migration blocked readers for several seconds. Budget accordingly, and have roughly three and a half times the size of your events table free on disk.

3. Then Pro. Pro 1.0 requires core 1.0. Once the core is on schema 10, run:

mix aurora_meter_pro.gen.migration --upgrade -r MyApp.Repo

That takes Pro’s schema from 9 to 11. Stop every 0.3 usage reporter before applying Pro’s v10. Also move the credit expiry cron entry from AuroraMeter.Pro.Credits.Expirer to AuroraMeter.Oban.CreditExpiry, and don’t register both.

4. Check your Oban config. The 1.0 workers use finite uniqueness periods, and AuroraMeter.Oban.validate!/1 warns if Oban.Plugins.Lifeline is missing.

This fixes a failure we found in soak testing. A worker that died holding an “infinite” uniqueness lock blocked every later run, and usage piled up unsent.

What stays the same

The core is still MIT, still a dependency rather than a hosted service, and still keeps every row in your own Postgres. The hot path is still an ETS increment, with no database write and no GenServer per tenant. track/2, with_quota/3 and the LiveView components still work the way the real-time metering post describes.

Pro is still licensed per app, from $99/mo, with a 14-day free trial (one trial per customer).

Where to start

If you have been waiting for 1.0 before putting billing on it, this is the release.