Every word, explained once

Tenant, feature, period, plan, and the five kinds of thing you can put on a plan. Ten minutes here saves you from the one mistake everybody makes: picking the wrong kind.

Free core 10 min Beginner

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:

  1. How much did this customer take? That is metering.
  2. Are they allowed to take this? That is entitlements.
  3. 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:

  1. Does using it cost you money every time? If not, you want feature or limit.
  2. Should the customer be stopped at a line? Use limit ... :hard.
  3. Should they be allowed past the line and charged? Use metered.
  4. 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 nil as “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/2 followed by track/3 to 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/2 rounds 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. Use format_compact/1 for 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)

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.