> ## Documentation Index
> Fetch the complete documentation index at: https://internal.softcrum.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Usage Snapshots and Metering

> After this ships, a tenant can see what they have consumed against what they contracted, updated every 30 minutes, and we can bill from the same numbers they are looking at.

## Context

DEC-G3 committed to Vercel-style pricing: published unit prices, included allowances, measured
consumption, a projected-spend panel and tenant-configurable spend caps. All of that rests on a
measurement that is frequent enough to be actionable and cheap enough to run for every tenant
forever.

DEC-G1 settled it at **every 30 minutes with deltas**. Computing consumption on demand would put an
aggregate query on a dashboard path; daily snapshots would let a tenant blow through a quota for
23 hours before anyone noticed.

The measurement has contractual weight: the ToS names these snapshots as the evidentiary basis for
billing. That is why they are append-only and why a correction is a new row, never an edit.

## Scope *(normative)*

* `core.usage_snapshots`, append-only, monthly partitioned, every 30 minutes per tenant.
* `core.billable_metrics`: parametric — marketable contacts, tracked events, messages by channel,
  storage.
* Delta computation between t1 and t2 so consumption never needs a scan.
* The marketable-contact count, using the predicate defined in FS-CORE-0004.
* Threshold alerts at 80 / 90 / 100% through the platform's own notification system.
* Enforcement hooks: grace, hard stop, or overage per metric, configurable per tenant from Ops.

## Non-scope *(normative)*

* The rating engine — it consumes these snapshots; it is its own epic (ADR-016).
* Invoicing and charging — Fintoc, Paddle, LibreDTE.
* The consumption dashboard UI — `frontend/console`.

## Behaviour *(normative)*

1. Snapshots run **every 30 minutes for every tenant**, on cron fan-out. Not tiered — the cost is
   low and a tenant discovering an overage a day late is a support problem for us.
2. Each row stores the **absolute value and the delta** since the previous snapshot. Deltas make
   billing a sum instead of a scan.
3. The table is **append-only**. A correction is a new row with a `correction_of` reference; editing
   a snapshot would destroy the evidentiary value the ToS relies on.
4. Marketable contacts use the predicate from FS-CORE-0004 — one definition, shared, so metering
   and messaging can never disagree about who is billable.
5. Alerts at 80, 90 and 100% are sent through the platform's own notification cascade. Building a
   second delivery path for our own alerts would put them outside the rules everything else obeys.
6. Enforcement per metric is **configurable per tenant from Softcrum Ops** (DEC-G2). Defaults:
   contacts grace + notice, messages hard at 110%, events soft with overage.
7. A spend cap is the tenant's self-service face of enforcement: reaching it applies the same stop
   the operator would.
8. A missed snapshot window is **recorded as missing, never interpolated**. An interpolated number
   in a billing basis is a number we cannot defend.
9. Partitioned monthly; retention follows the plan, with the legal minimum applying independently.

## Data *(normative)*

| Table                   | Key invariants                                                                                                                                                                                            |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `core.usage_snapshots`  | append-only; `tenant_id`, `metric_code` FK, `absolute_value`, `delta`, `window_start`, `window_end`, `correction_of` nullable; unique (`tenant_id`, `metric_code`, `window_end`); monthly RANGE partition |
| `core.billable_metrics` | parametric; `is_system` seeds `marketable_contacts`, `tracked_events`, `messages_email`, `messages_push`, `storage_bytes`; `unit`; not tenant-extensible                                                  |

## API *(normative)*

| Endpoint                        | Class      | Permission             | Budget    |
| ------------------------------- | ---------- | ---------------------- | --------- |
| `GET /v1/core/usage`            | Management | `core.usage.read`      | p95 \<1 s |
| `GET /v1/core/usage/projection` | Management | `core.usage.read`      | p95 \<1 s |
| `PUT /v1/core/usage/spend-cap`  | Management | `core.usage.configure` | p95 \<1 s |

## Events *(normative)*

`core.usage.threshold_crossed` on the outbox at 80, 90 and 100%, which is what drives the alert
campaign through `messaging`.

## Acceptance criteria *(normative)*

1. Snapshots land every 30 minutes for every tenant, verified over a 24-hour synthetic run.
2. The sum of deltas over a cycle equals the difference of absolutes at its endpoints.
3. The marketable-contact count matches the messaging module's own view for the same tenant and
   instant.
4. Crossing 80% emits once and does not re-emit while it stays above.
5. A missed window is recorded as missing and is visibly absent, not interpolated.
6. A spend cap applies the configured enforcement within one snapshot window.
7. Corrections append with `correction_of` and leave the original readable.
8. **Negative:** no code path updates or deletes a snapshot row.

## Execution

Asynchronous pipeline. Cron fan-out in `backend/scheduler`, computation in `backend/workers`,
read endpoints in `backend/api`.

## Open questions

| # | Question                                                                                     | Decides | By              |
| - | -------------------------------------------------------------------------------------------- | ------- | --------------- |
| 1 | Is storage metered by database bytes, by exported archive bytes, or both?                    | Daniel  | before approval |
| 2 | Does a spend cap stop the Runtime API (rejecting `track`) or only the billable side effects? | Daniel  | before approval |

## Changelog

| Version | Date       | Change        | Why | Author                 |
| ------- | ---------- | ------------- | --- | ---------------------- |
| 0.1.0   | 2026-08-17 | Initial draft | —   | daniel + claude-opus-5 |

## Delivery record

*Not implemented yet.*
