Aurora Meter Pro README

Latest release v1.0.0 · 2026-09-22 · this is the file that ships inside the package

Aurora Meter Pro

Commercial Pro tier for Aurora Meter: Stripe checkout + webhook sync, prepaid credit top-ups and auto-recharge, metered usage reporting, credit-aware usage dashboards, a per-request audit log, quota alerts, and historical rollups.

Requires the free aurora_meter core and a valid commercial license. The supported runtime starts at Elixir 1.15 with Erlang/OTP 26; the Stripe client’s current HTTP dependency does not compile on OTP 25. That floor is enforced by the dependency itself: the Stripe client’s HTTP stack pulls in quic, whose rebar.config declares {minimum_otp_vsn, "26"}.

Supported versions

Every pair below is exercised by CI on every push, at the exact patch shown, and the whole test suite runs on each of them.

Elixir Erlang/OTP
Floor 1.15.8 26.2.5.21
Supported 1.18.5 27.3.4.17
Current 1.20.4 29.0.6

Elixir 1.15 supports Erlang/OTP 24 to 26 and no further, so Elixir 1.15 with Erlang/OTP 26 is the only pair available at the floor. Elixir 1.15.8 is also the last release of the 1.15 line, so it will not receive further security patches, and Erlang/OTP 26 is past its upstream maintenance window; a host that wants a patched toolchain wants Elixir 1.20.4 with Erlang/OTP 29. The floor is a compatibility guarantee, not a recommendation.

Postgres: 16.13 is the tested version and 13 is the floor. A second CI lane runs the suite on 15.6 as well.

A further CI lane resolves the core from Hex rather than from a sibling checkout, and runs the whole suite against the published aurora_meter artifact, so an install from the registry is verified rather than assumed.

Install

def deps do
  [
    {:aurora_meter, "~> 1.0"},
    {:aurora_meter_pro, "~> 1.0", organization: "phxtemplates"}
  ]
end

Pro 1.0 requires core 1.0. It declares aurora_meter as >= 1.0.0-rc.1 and < 2.0.0, so a host that upgrades Pro while its core is still on 0.4.x or 0.5.x fails to resolve rather than compiling into something that does not work. Upgrade core first.

Then configure the Stripe provider:

config :aurora_meter,
  provider: AuroraMeter.Pro.Stripe,
  period_source: AuroraMeter.Pro.Period

config :aurora_meter_pro,
  stripe_prices: %{pro: "price_flat_pro", scale: "price_flat_scale"},
  # usage-based prices on a Billing Meter, attached at checkout so overage is invoiced
  stripe_metered_prices: %{scale: ["price_scale_overage"]},
  stripe_meters: %{ai_generations: "ai_generations"},
  webhook_secret: {:system, "STRIPE_WEBHOOK_SECRET"},
  stripe_portal_configuration: {:system, "STRIPE_PORTAL_CONFIGURATION"}, # optional
  alert_handler: &MyApp.Billing.quota_alert/1

Mount the webhook before Plug.Parsers and schedule the workers with Oban:

# endpoint.ex
plug AuroraMeter.Pro.Webhook  # or forward "/webhooks/stripe" from a raw-body route

# config
config :my_app, Oban,
  queues: [aurora_meter: 5],
  plugins: [{Oban.Plugins.Cron, crontab: [
    {"*/5 * * * *", AuroraMeter.Pro.UsageReporter},
    {"15 2 * * *", AuroraMeter.Pro.Rollup},
    {"*/10 * * * *", AuroraMeter.Pro.Alerts}
  ]}]

In Stripe, give each metered price a graduated tier with the included allowance at $0 and the overage unit price above it; the reporter sends total usage deltas and Stripe applies the tiers.

What it adds

  • Billing: AuroraMeter.Billing.checkout/3 and portal_url/2 via Stripe, plus a webhook plug that keeps subscriptions in sync.
  • Prepaid credit top-ups & auto-recharge: see below.
  • Usage reporting: completed, persisted metered usage pushed to Stripe Billing Meters with immutable retries and bounded provider idempotency.
  • Dashboards: a styled, themeable usage_dashboard/1 component and a mountable LiveView, both live. A tenant with a prepaid ledger gets a money section first (balance, spend this period, runway, spend chart); then quota cards, SVG daily charts and monthly history.
  • Request audit log: one row per API call with the actor, route, status, duration and cost, filtered and cursor-paged; see below.
  • Alerts: quota-threshold telemetry and a handler callback, for hard caps and metered allowances.
  • Rollups: daily/monthly historical aggregates and CSV export.

Dashboards and counters

AuroraMeter.Pro.Dashboard.load/2 returns everything usage_dashboard/1 renders, including :credits, :spend and :spend_months from the core credit ledger. The money section only appears when the tenant actually has a ledger, so a subscription-billed product renders exactly as it did in 0.3.

A counter(:requests) feature (core 0.4, ADR 0006) renders as a bare count with no cap, no progress bar and the status line "6 this period · paid from credits". It is deliberately not described as overage: for a prepaid product nothing is billed at period end, because the money left the ledger when the request ran. Metered features with a unit price keep their overage wording.

<AuroraMeter.Pro.Components.usage_dashboard
  tenant={@org} data={@dashboard} money={true} />

Request audit log

# router
pipeline :api do
  plug :accepts, ["json"]
  plug MyAppWeb.Authenticate
  plug AuroraMeter.Pro.Plugs.AuditLog,
    tenant: &MyAppWeb.Audit.tenant/1,     # conn -> tenant | nil (nil skips the write)
    actor: &MyAppWeb.Audit.actor/1,       # conn -> %{id:, label:, mode:} | nil
    metadata: &MyAppWeb.Audit.metadata/1  # conn -> %{amount_micro:, metadata:, route:}
end

The three host-shaped values arrive as resolver functions, so the package never guesses at your schema, and nil from the :tenant resolver is how unauthenticated traffic stays out of the log. The row is written after the response is sent, and a failure to write it is a Logger.warning, because the audit log never breaks the request it is auditing. See ADR 0003.

AuroraMeter.Pro.AuditLog.list(org, status_class: :server_error, limit: 50)
#=> %{data: [%AuroraMeter.Pro.Schema.AuditEvent{}, ...], has_more: true,
#=>   next_cursor: "..."}   # keyset cursor, stable under concurrent writes

AuroraMeter.Pro.AuditLog.stats(org, from: DateTime.add(DateTime.utc_now(), -86_400))
#=> %{total: 1_284, by_status_class: %{success: 1_270, client_error: 12,
#=>   server_error: 2}, p50_ms: 180, p95_ms: 940}
config :aurora_meter_pro,
  audit_log_retention_days: 30,
  audit_log_writer: :async   # :sync in tests

# application.ex, the supervisor the async writer uses
children = [{Task.Supervisor, name: AuroraMeter.Pro.TaskSupervisor}, ...]

config :my_app, Oban,
  queues: [aurora_meter: 5],
  plugins: [{Oban.Plugins.Cron, crontab: [
    {"30 3 * * *", AuroraMeter.Pro.AuditLog.Pruner}
  ]}]

The table is aurora_meter_audit_events, Pro schema version 3 (mix aurora_meter_pro.gen.migration -r MyApp.Repo --from 3 on an existing install).

Top-ups and auto-recharge

The cent-denominated top-up API requires matching Stripe and ledger currencies. Zero- and three-decimal Stripe currencies are rejected before charging; ISK and UGX use Stripe’s compatible two-decimal representation and its whole-unit charge restrictions. This API does not perform currency conversion.

The core’s AuroraMeter.Credits is a prepaid ledger; Pro fills it from Stripe. A one-off top-up is a payment-mode Checkout Session; auto top-up charges the card saved by the last top-up, off-session, when the balance crosses the tenant’s threshold. Stripe is only the payment rail. The ledger is the source of truth and is credited from the webhook, keyed on the PaymentIntent id, so a redelivered event never funds an account twice (see ADR 0002).

# One-off top-up: send the customer to Stripe
{:ok, url} = AuroraMeter.Pro.Credits.checkout(org, 2_500,
  success_url: url(~p"/billing?topped_up=1"), cancel_url: url(~p"/billing"))

# Auto top-up: $25 whenever the balance drops below $5
{:ok, _account} = AuroraMeter.Pro.Credits.update_auto_top_up(org, %{
  auto_top_up_enabled: true, threshold_micro: 5_000_000, amount_cents: 2_500})

Configuration (all optional):

config :aurora_meter,
  # let Pro react to the ledger's low-balance crossings
  credits_low_balance_handler: &AuroraMeter.Pro.Credits.on_low_balance/1

config :aurora_meter_pro,
  credits_product_id: {:system, "STRIPE_CREDITS_PRODUCT"}, # else found/created by metadata
  credits_presets_cents: [1_000, 2_500, 5_000, 10_000],
  credits_min_cents: 500,
  credits_max_cents: 100_000,
  auto_top_up_cooldown_seconds: 600,
  auto_top_up_max_failures: 3,
  # {:granted, txn} | {:low_balance, event} | {:auto_top_up_failed, key, reason, n}
  # | {:auto_top_up_disabled, key, reason}
  credits_handler: &MyApp.Billing.credits_event/1

config :my_app, Oban,
  queues: [aurora_meter: 5],
  plugins: [{Oban.Plugins.Cron, crontab: [
    {"*/30 * * * *", AuroraMeter.Pro.Credits.Expirer} # expire promotional grants
  ]}]

Subscribe the webhook endpoint to checkout.session.completed, payment_intent.succeeded and payment_intent.payment_failed (in addition to the customer.subscription.* events). Also subscribe to charge.refunded, refund.created, refund.updated, refund.failed, charge.dispute.funds_withdrawn, and charge.dispute.funds_reinstated. This release requires core schema 6 and Pro schema 9. Generate upgrade migrations from the first version not yet installed; see the changelog.

Pending recharge attempts retain their original parameters and key. Known PaymentIntents are retrieved rather than recreated. Unknown outcomes at least 23 hours old require reconciliation; they are never automatically recharged outside Stripe’s minimum idempotency window. Monitor worker errors and pending state, including when recharge is disabled. See Top-ups.

In tests, config :aurora_meter_pro, credits_stripe_client: AuroraMeter.Pro.Credits.StripeClient.Fake records every call and never touches the network.

Guides

  • Recovery: inspect and resolve uncertain payments and meter reports using provider evidence and guarded database edits

  • Getting started: install, what to create in Stripe, config, the webhook and the workers

  • Subscriptions: checkout, the portal, and what each webhook does

  • Top-ups: prepaid credit, auto-recharge, refunds and disputes

  • Usage reporting: getting metered overage onto the invoice

  • Operations: dashboard, audit log, alerts, rollups and the reconciler

Before you depend on it

  • Architecture: what Pro owns, which direction the dependency runs, and the export and payment paths end to end.
  • Correctness: per invariant, with the prerequisites, the known limits and the evidence.
  • Troubleshooting: symptom first, for when something is visibly not happening.
  • SECURITY.md: what Pro stores, how key material is handled, what the host is responsible for, and how to report a vulnerability.

The contract for everything below Pro is the free core’s guarantee page.

Reference

What is supported, and for how long:

  • API inventory: every supported function, behaviour, struct, configuration key, telemetry event and Mix task, each with a stability class, plus the core version Pro requires and the core entries it relies on. If it is not on that page, it is not supported, even where the code happens to be public.
  • Support policy: what SemVer covers here, how schema changes work in 1.x, how long a deprecated API lives, which Elixir and Erlang/OTP pairs are tested, and what working against a payment provider does and does not let this package promise.
  • Guarantees and limits, in the core package: one row per guarantee, each with the exact conditions that make it true, what voids it and the test that proves it. It covers Pro as well, because the rows that matter here (usage export is at-least-once with provider-side idempotency, scheduled work is at-least-once with idempotent effects, a provider invoice is never reported as finalised or paid) are properties of the whole product. It is stated in one place on purpose: two copies of a contract drift, and the copy a customer reads is always the stale one.

License

Commercial: see LICENSE.commercial. Plans, the free trial and your package key: https://aurorameter.com/pricing. Licence terms and refunds: https://www.phxtemplates.com/licensing. Aurora Meter is built by the team behind PhxTemplates.