This page assumes you know Elixir and nothing else. Every word that matters is defined here, and every other guide uses these words in exactly this sense.
#The problem, in one picture
You run a shop. Customers come in and take things. Sooner or later you have to answer three questions, and they are genuinely different questions:
- How much did this customer take? That is metering.
- Are they allowed to take this? That is entitlements.
- Who pays, and how much? That is billing.
Most billing libraries answer the third one and leave you to invent the first two. Aurora Meter answers all three and keeps them apart, so you can use only the parts you need.
#The four nouns
Everything in the library is about these four things. Learn them and the rest is detail.
#Tenant: who
The customer being counted. Usually a company or an account, not a person: five colleagues in one company share one allowance.
A tenant can be any Elixir term, because it gets turned into a short text key:
AuroraMeter.track("org_42", :api_calls) # a string, used as it is
AuroraMeter.track(42, :api_calls) # a number, stored as "42"
AuroraMeter.track(org, :api_calls) # your own struct, see below
To pass your own struct, teach the library how to make a key from it:
defmodule MyApp.MeterTenant do
@behaviour AuroraMeter.Tenant
@impl true
def to_key(%MyApp.Org{id: id}), do: "org:#{id}"
def to_key(key) when is_binary(key), do: key
end
config :aurora_meter, tenant: MyApp.MeterTenant
The key must never change. It is the name of a counter, a database row and a PubSub topic. If it changes, that customer looks like a brand new one with an empty allowance and no history. Use the database id, never the name.
#Feature: what
The thing being counted or gated, named by an atom: :api_calls,
:ai_generations, :seats, :export_to_pdf.
You choose these names. They are yours. The library never invents one.
#Period: when
Allowances reset. A period is the window they reset in.
By default a period is a calendar month: the 1st of March to the 31st of March, then the counter starts again at zero. With the Pro package it becomes the customer’s own billing cycle instead, so somebody who subscribed on the 20th gets their reset on the 20th.
AuroraMeter.period(org)
# %{start: ~U[2026-03-01 00:00:00Z], end: ~U[2026-04-01 00:00:00Z],
# source: :calendar}
#Plan: the rules
What a tenant is allowed, written once in a module and checked when your app compiles.
defmodule MyApp.Plans do
use AuroraMeter.Plans
plan :free do
price 0
limit :ai_generations, 50, :hard
end
end
A tenant is put on a plan with one call:
AuroraMeter.subscribe(org, :free)
That call is local. It has nothing to do with Stripe, and it takes effect immediately.
#The five kinds of feature
This is the most important table on the site. Every metering surface in the library is one of these, and picking the wrong one is the mistake that costs the most to undo.
| You write | It means |
check/2 answers |
Money |
|---|---|---|---|
feature :pdf_export, true |
a switch that is on |
:ok |
none |
feature :pdf_export, false |
a switch that is off |
{:error, :not_entitled} |
none |
feature :seats, 5 |
a number the plan carries |
:ok |
none |
limit :runs, 50, :hard |
a wall |
:ok until 50, then {:error, :limit_exceeded} |
none |
metered :runs, included: 1_000, unit_price: 2 |
an allowance you may pass |
always :ok |
billed after 1,000 |
counter :runs |
just counting |
always :ok |
never |
That is six rows and five kinds, because feature does two jobs: a true or
false switch, and a number the plan carries which you read yourself rather than
counting against.
#How to choose
Four questions, in order:
-
Does using it cost you money every time? If not, you want
featureorlimit. -
Should the customer be stopped at a line? Use
limit ... :hard. -
Should they be allowed past the line and charged? Use
metered. -
Is the money already handled somewhere else, like a prepaid balance? Use
counter.
#Why counter exists
This one catches people out, so it gets its own section.
Say your product is prepaid: customers buy credit up front and every request spends some. Nothing is billed at the end of the month, because the money left when the request ran.
If you model that with metered ... included: 0, then every single request
reads as overage against an allowance of zero, and your dashboard tells the
customer they are six over their limit and will be invoiced. Both halves of
that sentence are false.
counter measures and says nothing about money. Its quota/2 reports
limit: nil, included: nil and percent: nil deliberately, because there is
no denominator.
Anything drawing a progress bar must treat
nilas “no bar”, never as zero. “0% of 0” is exactly the reading this kind exists to prevent.
#The two questions, in code
#How much have they used?
AuroraMeter.track(org, :api_calls) # add one
AuroraMeter.track(org, :api_calls, 10) # add ten
AuroraMeter.usage(org, :api_calls) # => 37
AuroraMeter.usage_all(org) # => %{api_calls: 37, ai_generations: 4}
track/3 writes to an in-memory counter and returns immediately. A background
worker writes the totals to Postgres every five seconds. That is why it is fast
enough for every request, and it is also why a hard crash can lose up to five
seconds of counts. If one feature must never lose a count, list it in
:durable_features and it is written straight through.
#Are they allowed?
AuroraMeter.check(org, :ai_generations)
# :ok | {:error, :limit_exceeded} | {:error, :not_entitled}
Never use
check/2followed bytrack/3to enforce a wall. Two requests arrive together, both read 49 of 50, both pass, both write, and you have 51.
Use the one call that reserves the count and the permission together:
case AuroraMeter.with_quota(org, :ai_generations, fn -> generate() end) do
{:ok, result} -> result
{:error, :limit_exceeded} -> upgrade_prompt()
{:error, :not_entitled} -> upgrade_prompt()
end
Under load, a hard limit of 50 admits exactly 50. If your function raises, the reservation is handed back before the error travels on.
There are two more question shapes worth knowing:
AuroraMeter.entitled?(org, :file_uploads) # does the plan grant this at all?
AuroraMeter.allowed?(org, :file_uploads) # and is there room right now?
A team on Free that has used all 100 uploads is entitled (their plan gives them uploads) but not allowed (they are out). Use the first to decide whether to show a feature at all, and the second to decide whether this click works.
#Money: the two shapes
There are exactly two ways money works here. Use either, or both.
#Shape one: a subscription with overage
The customer pays $20 a month, which includes 1,000 generations. The 1,001st still works, costs two cents, and lands on the invoice at the end of the month.
metered :ai_generations, included: 1_000, unit_price: 2
The free core counts. Pro reports the overage to Stripe. Full walkthrough: Give an allowance, charge for going over.
#Shape two: prepaid credit
The customer buys $25 of credit up front. Every request spends some. When the balance runs low they top up, or their card is charged automatically.
AuroraMeter.Credits.debit(org, 1_500, "req:abc") # spend $0.0015
AuroraMeter.Credits.available(org) # => 24_998_500
The free core holds the ledger. Pro fills it from Stripe. Full walkthrough: Sell credit up front.
#Micro-dollars
Credit amounts are whole numbers called micro-dollars. One dollar is 1,000,000 of them.
$1.00 = 1_000_000
$0.01 = 10_000
$0.000001 = 1
Why not floating point? Because 0.1 + 0.2 is not 0.3, and a ledger that
drifts is worse than no ledger. Why not cents? Because an API call that costs a
fortieth of a cent rounds to zero, and then a million of them are free.
Do not write the zeroes yourself:
alias AuroraMeter.Credits.Money
Money.from_cents(2_500) # => 25_000_000 ($25.00)
Money.to_cents(1_500_000) # => 150 ($1.50)
Money.format(1_500_000) # => "$1.50"
Money.format(1_500) # => "$0.00" careful
Money.format(1_500, precision: 6) # => "$0.001500"
Money.format_compact(1_500) # => "$0.0015"
format/2rounds to two decimal places unless you ask for more. That is right for a balance and wrong for a unit price. A page that costs 1,500 micro-dollars renders as “$0.00”, which is the misreading the unit was chosen to avoid. Useformat_compact/1for unit prices and chart labels.
#Where everything lives
| You want to | Call |
|---|---|
| count something |
AuroraMeter.track/4 |
| read a count |
AuroraMeter.usage/2, usage_all/1, history/3 |
| gate an action |
AuroraMeter.check/2, with_quota/4 |
| fill a dashboard card |
AuroraMeter.quota/2 |
| put a tenant on a plan |
AuroraMeter.subscribe/2 |
| hold prepaid money |
AuroraMeter.Credits |
| take money from a card |
AuroraMeter.Pro.Credits (Pro) |
| bill overage to Stripe |
AuroraMeter.Pro.UsageReporter (Pro) |
#Which guide to read next
- The whole thing at once: Build a metered SaaS, end to end.
- Selling tiers with switches, seats and caps: Write your plans down, then enforce them.
- Selling an allowance and charging for what goes over: Give an allowance, charge for going over.
- Selling credit up front: Sell credit up front, spend it per request.
- Putting any of it on a screen: Put the numbers on screen.
Keep going
The core is free. Pro is the Stripe half.
Everything the free MIT package does is yours with no account and no expiry. Aurora Meter Pro adds Stripe checkout, metered usage reporting, prepaid top-ups, dashboards and alerts, and every plan starts with a free trial.