feat(server): usage metering ledger (Phase 7 slice 1)#74
Merged
Conversation
A durable per-organization, per-UTC-calendar-month ledger of identity resolutions — the billable unit — replacing the ephemeral Redis rate-limit windows as the source of truth. Measurement only: Stripe, public signup, and hard enforcement are deferred follow-ups (ADR-0007). - migration 015: organizations.plan + monthly_resolution_limit (NULL = unlimited); usage_counters(organization_id, period_start, resolution_count, warned_80, warned_100). - lib/usage.ts: incrementUsage() — atomic UPSERT inside the resolution transaction in resolveSnapshot, so counting is exactly-once (the event_id dedup short-circuits before the tx; rollback un-counts). /v1/resolve is a non-persisting preview and intentionally does not count. - checkAndWarnThreshold() — soft limits: crossing 80%/100% flips a once-per- period guard and emits logger.warn + Sentry.captureMessage (reusing the shipped error tracking; no-op without SENTRY_DSN). Never blocks traffic. NULL limit = no-op, so self-host is metered but never warned. - GET /admin/usage (org-scoped) + Observatory Usage page (count vs limit, history); set-org-plan CLI for thin/assisted onboarding. - docs: ADR-0007, OpenAPI /admin/usage, ROADMAP, CHANGELOG. - tests: usage.integration (exactly-once, dedup, multi-project rollup, org isolation, threshold guards) + GET /admin/usage coverage. 148 server tests.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The first slice of Phase 7 billing: a durable per-organization, per-UTC-calendar-month ledger of identity resolutions — the billable unit. Replaces the ephemeral Redis rate-limit windows as the metering source of truth. Measurement only — Stripe, public self-serve signup, and hard enforcement are deferred follow-ups (ADR-0007).
Builds on the organizations tenant layer (ADR-0005) — orgs are the billing anchor.
Billable unit & exactly-once counting
One billable resolution = one committed snapshot.
incrementUsage()records it via an atomic UPSERT inside the existing resolution transaction inresolveSnapshot, so counting is exactly-once with no extra coordination:event_iddedup short-circuits before the transaction → retries/duplicates don't count;POST /v1/resolveis a non-persisting preview (login-flow confidence/risk check) and intentionally does not count — you bill for committed observations, not previews.Soft limits (measure + warn, never block)
organizationsgainsplan+monthly_resolution_limit(NULL = unlimited, so self-host is metered but never warned).checkAndWarnThreshold()runs after commit (fire-and-forget; metering never delays/fails a resolution). Crossing 80% / 100% flips a once-per-period guard and emitslogger.warn+Sentry.captureMessage— reusing the shipped error tracking (no-op withoutSENTRY_DSN), no new infra. Exceeding the limit never blocks traffic.Surfacing & provisioning
GET /admin/usage(org-scoped) → current-period count, limit, %, recent history.set-org-plan <orgName> <plan> [limit|unlimited]CLI — thin/assisted onboarding until an org-management UI exists.Schema (migration 015)
organizations.plan(defaultfree) +monthly_resolution_limit(nullable);usage_counters(organization_id, period_start, resolution_count, warned_80, warned_100, updated_at), PK(organization_id, period_start). No backfill; self-host unchanged.Tests
usage.integration.test.ts— exactly-once counting, dedup-doesn't-count, multi-project rollup, per-org isolation, threshold-guard flips (idempotent). PlusGET /admin/usagecoverage inadmin.integration. 148 server tests pass; type-check, lint,openapi:lint, andpnpm audit --audit-level=highall green.Scaling note
One counter row per org/month is a documented write hotspot under high concurrency. Fine at current scale (single box, one design partner); deferred mitigation: Redis hot counter flushed periodically, or sharded counters.
Deferred follow-ups
Stripe (subscription/checkout/portal/webhooks + usage reporting); public self-serve signup; hard quota enforcement (402/429); per-project usage breakdown; Redis hot-counter optimization; Observatory org-management UI.