driptab/Docs

Events and metering

Driptab's usage-based billing is driven by events. Your backend sends an event every time a billable action occurs. At period close, those events are aggregated into invoice line items.


What is an event?

An event is a single billable occurrence: an email sent, a gigabyte stored, an API call made. Events have:

Field Description
transaction_id Your stable, unique ID for this event. Must be unique per subscription per organization.
subscription_id Which subscription to attribute the usage to (or external_id prefixed with ext:).
code Which billable metric this event contributes to (e.g. mail_out).
value Numeric value as a string (e.g. "1", "24576"). Used by sum, max, latest aggregations.
properties Optional JSON object with additional context.
timestamp When the event occurred (ISO-8601 UTC).

Sending an event

dtab ev ingest \
  --transaction-id msg_01HX4K2MNPQRSTUVWXYZ000001 \
  --subscription ext:your-sub-id \
  --code mail_out \
  --prop count=1
POST /v1/events
Authorization: Bearer dtab_…
X-Project-Id: <project_id>

{
  "transaction_id": "msg_01HX4K2MNPQRSTUVWXYZ000001",
  "subscription_id": "ext:your-sub-id",
  "code": "mail_out",
  "value": "1",
  "timestamp": "2026-06-08T14:23:00Z"
}

Response:

{ "accepted": true, "duplicate": false }

If the same transaction_id is sent again:

{ "accepted": true, "duplicate": true }

The event is acknowledged but not counted again. Your backend can retry event ingestion safely.


Idempotency

Idempotency for events is enforced at two levels:

  1. The Durable Object — the SubscriptionMeter DO maintains a set of seen transaction_ids for the current period. Concurrent or retried events are serialized by the DO; a duplicate is never double-counted.

  2. D1 unique constraintUNIQUE(organization_id, transaction_id) on the events table acts as a second safety net, catching any path that bypasses the DO.

The dedup set in the DO is reset when the period closes, so it does not grow without bound. transaction_id uniqueness is per-period per-subscription.


How aggregation works

Each billable metric has an aggregation_type that determines how raw event values roll up into the period total:

Aggregation How it works Example use
sum Add all value values in the period Email volume, bytes stored
count Count the number of events API calls (ignoring value)
count_unique Count distinct values of field_name Unique active users
max Peak value in the period High-water-mark storage billing
latest Most recent value in the period Current seat count

Aggregation does not happen in SQL. It happens in the SubscriptionMeter Durable Object. The DO maintains a running counter per metric for the current period and updates it on each accepted event.


The Durable Object architecture

Each active subscription has its own SubscriptionMeter Durable Object named {organization_id}:{subscription_id}. The DO is:

  • Single-threaded: all events to the same subscription are processed one at a time. No concurrent counter updates, no locking.
  • Serialized: the DO's inbox queues concurrent requests rather than running them in parallel.
  • Strongly consistent: the DO's SQLite storage is local to the DO; reads and writes are not eventually consistent.

This means you can fire thousands of events at the same subscription concurrently and the final counter will be exactly correct. There is no need for an external queue or distributed lock at current event volumes.

At extremely high event rates for a single subscription, a Queue can be placed in front of the DO with no change to callers (see ARCHITECTURE.md).


Raw event storage

Every accepted event is also appended to the D1 events table for:

  • Audit: who sent what, when, with what properties.
  • Recompute: if a DO is lost or corrupted, the raw events can be replayed to reconstruct counters.
  • Analytics: query patterns like "top 10 customers by mail_out this month".

The events table is append-only. At high scale, it migrates to R2 + Analytics Engine.


Period close

When the DO's alarm() fires at current_period_end:

  1. Final per-metric counters are read from the DO.
  2. Fees are computed for each charge:
    • standard charges: units × properties.amount, rounded to cents once.
    • flat_fee charges: the catalog item's price (or override).
  3. An invoice row and its fees rows are written to D1 in one atomic transaction.
  4. Counters and the dedup set are reset.
  5. current_period_start/end advances by one interval.
  6. The next alarm is scheduled.
  7. An invoice.created webhook fires.

Late events

An event whose timestamp falls before current_period_start arrives after the relevant period has already closed. Driptab's MVP behavior is to reject late events with an error. Do not silently roll them into the current period — that would misrepresent when the usage occurred. An explicit adjustment workflow will be added in a future release.