Skip to main content
Available on request β€” The Autumn Lakehouse is provisioned per customer. Contact us at hey@useautumn.com to get access.
Every object is delivered as a table named v2_3_<object>, grouped below by lane. The one exception is events: the event log is immutable and unversioned, delivered as events (not v2_3_events) β€” see Events.

Type legend

Types below are engine-neutral. How each engine surfaces them:
  • Timestamps are epoch-milliseconds (number (epoch ms)), not native datetimes β€” convert before use. See Querying β†’ Timestamps. The one exception is events.timestamp, which is a real timestamp.
  • JSON columns are stored as strings β€” parse them with JSONExtract* (ClickHouse) or JSON_VALUE (BigQuery).

Keys

  • ⭐ marks the stable, immutable key for a table β€” join and filter on this.
  • πŸ”— marks a stable foreign key (internal_customer_id, internal_feature_id, internal_product_id, internal_entity_id, internal_reward_id) β€” join to the matching internal_id.
  • External ids (customer_id, plan_id, feature_id, entity_id, id, …) are mutable β€” convenient for display, but don’t rely on them as stable keys. See Querying β†’ Use internal ids.
  • org_id is your tenant id (constant across your tables); env is sandbox or live (not production β€” filtering env = 'production' silently returns zero rows).

Catalog

Your pricing model: features, plans, plan items, rewards, and referral programs.

Subjects

Who your plans apply to: customers and entities (sub-customers).

States

Active customer–product relationships: recurring subscriptions and one-off purchases.

Balances

Feature balances, their per-entitlement breakdowns, rollover grants, and boolean feature flags.
These tables do not behave like a clean relational schema. They are denormalized snapshots of Autumn’s read-time balance computation, and they include every customer_entitlement row β€” across superseded product versions and past reset cycles β€” with no active-status or current-cycle filter. A naive SELECT ... FROM v2_3_balances therefore sums many lifetimes of a customer’s history into one row. Before you query balances, breakdowns, or overage, read Working with balances β€” it explains what each column means, why usage/granted/remaining don’t form a closed arithmetic triple, where overage comes from (it is not stored), and the active + current-cycle filter you must apply.
Rollover balances are not in v2_3_balances or v2_3_breakdowns. They live in their own table, v2_3_rollovers (below), and remaining / usage on the other two tables exclude them. Any customer holding rollovers therefore reads low until you add the rollover balance in yourself β€” and rollovers expire, so you must filter expires_at when you do. See Working with balances β†’ Rollovers.
entity_id is the scope of the row: null means customer-scoped (pooled), non-null means the row belongs to that entity. In v2_3_balances and v2_3_breakdowns a given entitlement appears exactly once, under whichever scope it holds β€” the two kinds of row are disjoint, so filtering to coalesce(entity_id, '') = '' gives you customer-scoped rows only, and dropping entity-scoped ones. v2_3_flags is the exception: an entity-scoped flag is emitted twice, once at its own entity scope and once into the customer pool, so keep the pooled rows there to avoid double counting. (In ClickHouse use coalesce, not entity_id IS NULL β€” IS NULL throws on these Iceberg columns; BigQuery is unaffected. See Querying β†’ Pooled vs per-entity.)
granted / remaining / usage are not a closed arithmetic triple β€” remaining β‰  granted βˆ’ usage. Each is aggregated over every entitlement row for this customer Γ— feature Γ— scope, including superseded product versions and past cycles, so a single row routinely sums many lifetimes of usage. remaining is floored per-entitlement, manual β€œSet Balance” decouples balance from grant, and rollover balances are omitted entirely (add them from v2_3_rollovers). There is no overage column β€” it is derived. Always read Working with balances and apply the active + current-cycle filter before trusting these numbers.
One row per customer_entitlement β€” internal_id alone is the key. (It used to be (internal_id, entity_id), because one entitlement could emit a pooled row plus per-entity rows. That fan-out is gone; entity_id is now simply the scope this single row belongs to.)
plan_id is gone from this table. Breakdowns no longer join out to the plans table, so the external, human-readable plan id is not carried here β€” a query that selects plan_id from v2_3_breakdowns today will fail outright rather than return nulls. Get it by joining internal_product_id β†’ v2_3_plans.internal_id and reading plan_id there. (Prefer internal_product_id itself wherever you can: plan_id is mutable and shared across plan versions.)
v2_3_breakdowns has no status / is_active / is_current column and mixes rows from superseded/cancelled product versions, past reset cycles, and pre-seeded future cycles. To get the rows the API would use, join internal_customer_product_id β†’ v2_3_subscriptions.internal_id and keep the active ones, then pick the current cycle (earliest upcoming reset_resets_at); one_off rows are always live. This join is now direct: internal_customer_product_id is the customer’s own subscription row, so one equality gets you its status. (Don’t use internal_product_id for this β€” it points at the shared plan version, not at the customer’s own copy of it.) See Working with balances β†’ The active + current-cycle filter.
When a feature is configured to roll unused allowance into the next cycle, each carried-over grant lands here as its own row. Nothing in v2_3_balances or v2_3_breakdowns includes these amounts β€” you add them by joining internal_breakdown_id back to v2_3_breakdowns.internal_id.Rollovers are a separate table rather than extra columns on v2_3_breakdowns because they are sparse: roughly 6.8% of customer entitlements carry one (about 7.8M rollovers against 115M entitlements). Folding them in would put mostly-null columns on every breakdown row, so the ~93% of rows with no rollover would pay for the 7% that do. One entitlement can also hold several grants with different expiries, which a flat column set can’t represent at all.
Rollovers expire, and this table keeps the expired ones. Summing balance without filtering overstates what a customer actually holds. Always filter on expires_at: in ClickHouse write (coalesce(expires_at, 0) = 0 OR expires_at > <now epoch ms>), because expires_at IS NULL throws there, same as entity_id; in BigQuery plain IS NULL is fine. There is a worked query in Querying β†’ Balance including rollovers.
There is no entity_id column β€” per-entity rollover amounts are left packed in the entities JSON map rather than exploded into rows. Parse it if you need entity-level rollover detail.
plan_id, customer_plan_status, customer_plan_starts_at, and customer_plan_ended_at have been removed, leaving the 14 columns above. Flags now follow the same rule as v2_3_breakdowns: carry keys, never denormalized copies of data that lives on another table. Resolve each of the four by joining outward β€”
  • plan_id β†’ join internal_product_id to v2_3_plans and read plan_id there, exactly as you would from a breakdown.
  • the three customer_plan_* columns β†’ join internal_customer_product_id to v2_3_subscriptions.internal_id (or v2_3_purchases.internal_id) and read status, started_at, ended_at / expires_at from the plan row itself. There is a ready-made double-join in Querying β†’ The status of the plan behind a flag.
Dropping plan_id removed the plans join from the flags query outright, so the table is cheaper to keep in sync as well as simpler to reason about. If you previously filtered customer_plan_status = 'active' directly on this table, that query needs the join added.
The subscription_ids array was dropped β€” use subscription_ids_csv, which every engine handles identically; split on , if you need the individual ids.

Invoices

Invoices and their line items.

Events

The append-only usage event log. This is the highest-volume table and is append-only (no updates).
The events table is events, not v2_3_events. Events are immutable and unversioned β€” they are not subject to the v2_3 schema versioning that every other object uses. Address it as `<catalog>`.`<namespace>.events`. Querying v2_3_events returns Unknown table expression identifier because that table does not exist.
Unlike every other table, events.timestamp is a native timestamp β€” no epoch-ms conversion needed. created_at is still epoch-ms.