Skip to content

feat(quotas+billing): implement the users/quota milestone (P9)#10

Merged
aarani merged 5 commits into
mainfrom
claude/users-quota-milestone-yylyfh
Jun 22, 2026
Merged

feat(quotas+billing): implement the users/quota milestone (P9)#10
aarani merged 5 commits into
mainfrom
claude/users-quota-milestone-yylyfh

Conversation

@aarani

@aarani aarani commented Jun 21, 2026

Copy link
Copy Markdown
Member

Per-user resource quotas with pay-as-you-go hourly billing.

Quotas:

  • user_quotas table (00006): admin-set per-user overrides of max
    servers / cpus / memory_mb, 0 = unlimited per axis; users without a
    row fall back to the configurable system default (QUOTA_MAX_*).
  • Enforced at server create: ServerHandler.Create checks the owner's
    current usage (GameServerRepository.OwnerUsage, counting every live
    server) against their effective quota (model.UserQuota.Allows) and
    returns 403 with the breached dimension before placement.
  • Endpoints: GET /quota (self), GET/PUT/DELETE /admin/users/:id/quota.

Billing (pay-as-you-go, hourly):

  • billing_usage table (00007): a metered ledger. The reconciler opens
    an interval on MarkRunning and closes it on stop/lost/delete; both
    idempotent (partial unique index = one open interval per server), so
    retries can't double-bill. internal/billing holds the pure pricing
    (per-vCPU-hour + per-GB-hour, BILLING_*); BillingRepository.OwnerLedger
    aggregates month-to-date with open intervals measured to now.
  • Endpoints: GET /billing (self), GET /admin/users/:id/billing — line
    items, period total, and live hourly burn.

Frontend: the Quotas & Users view (was a stub) is built — usage-vs-limit
meters, inline quota editing + reset-to-default, a pay-as-you-go cost
column with live burn, and fleet burn/spend tiles.

Verify: unit tests for UserQuota.Allows and billing pricing; e2e
(quota_test.go, billing_test.go) covering exceed-quota -> 403, admin
set/get/delete + authorization, and the meter opening/closing across a
server's run/stop lifecycle.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Per-user resource quotas with pay-as-you-go hourly billing.

Quotas:
- user_quotas table (00006): admin-set per-user overrides of max
  servers / cpus / memory_mb, 0 = unlimited per axis; users without a
  row fall back to the configurable system default (QUOTA_MAX_*).
- Enforced at server create: ServerHandler.Create checks the owner's
  current usage (GameServerRepository.OwnerUsage, counting every live
  server) against their effective quota (model.UserQuota.Allows) and
  returns 403 with the breached dimension before placement.
- Endpoints: GET /quota (self), GET/PUT/DELETE /admin/users/:id/quota.

Billing (pay-as-you-go, hourly):
- billing_usage table (00007): a metered ledger. The reconciler opens
  an interval on MarkRunning and closes it on stop/lost/delete; both
  idempotent (partial unique index = one open interval per server), so
  retries can't double-bill. internal/billing holds the pure pricing
  (per-vCPU-hour + per-GB-hour, BILLING_*); BillingRepository.OwnerLedger
  aggregates month-to-date with open intervals measured to now.
- Endpoints: GET /billing (self), GET /admin/users/:id/billing — line
  items, period total, and live hourly burn.

Frontend: the Quotas & Users view (was a stub) is built — usage-vs-limit
meters, inline quota editing + reset-to-default, a pay-as-you-go cost
column with live burn, and fleet burn/spend tiles.

Verify: unit tests for UserQuota.Allows and billing pricing; e2e
(quota_test.go, billing_test.go) covering exceed-quota -> 403, admin
set/get/delete + authorization, and the meter opening/closing across a
server's run/stop lifecycle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@aarani aarani force-pushed the claude/users-quota-milestone-yylyfh branch from fca698d to 4543a22 Compare June 21, 2026 21:16
claude and others added 4 commits June 21, 2026 21:28
Owner-scoped player management: a user maintains a roster of players (by
username) and chooses, per player, which of their own servers each may
use — check/uncheck access.

- Schema (migration 00008): players (unique per owner+username) and a
  player_servers many-to-many grant; grants onto soft-deleted servers are
  filtered out on read.
- model.Player + Minecraft-style username validation (3-16 chars,
  [A-Za-z0-9_]).
- PlayerRepository: transactional create/update (username + full grant-set
  replace), grants aggregated as a live-server-filtered array on read.
- Owner-scoped API /players (CRUD): grant sets are validated to be the
  caller's own live servers (cross-owner ids -> 400), duplicate username ->
  409, ownership-scoped reads (no existence leak).
- Frontend: Players view (new nav entry) — add/remove players, search, and
  a server checklist per player that toggles grants live.

Verify: unit test for username validation; e2e (players_test.go) covering
the roster + grant lifecycle, validation, duplicate/conflict, and
cross-owner isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The platform now owns each server's in-game whitelist: the reconciler
periodically feeds a running server's granted-player set to its workload
over RCON, so checking/unchecking a player converges on the live server.

Path (control plane -> agent -> guest), reusing the existing vsock
control channel snapshots and health probing already use — no protobuf
change, since the AgentLink Command is a generic op + JSON payload:
- reconciler: a decoupled 30s sync loop pushes each running server's
  desired whitelist (PlayerRepository.UsernamesForServer) via a new
  WhitelistSource seam; failures are logged, never block the core tick.
- provisioner/hub/agent: new SyncWhitelist op threaded through
  Provisioner, the Commander/Hub, and the agent Runtime (fake = no-op).
- firecracker driver: forwards the desired set down the VM's vsock UDS.
- guest init: a new WHITELIST control verb diffs the desired set against
  the live whitelist (read over RCON) and applies the minimal
  add/remove, enabling enforcement when non-empty and disabling it when
  empty — so a server with no granted players stays open.
- internal/minecraft: pure ParseWhitelist + WhitelistSyncCommands
  (case-insensitive diff, deterministic order) for the guest to apply.

Verify: unit tests for the parse/diff helpers and the agent op dispatch;
e2e pins the reconciler's data source (UsernamesForServer) after grants.
The live RCON application rides the KVM lane like the other vsock ops.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The e2e harness builds config.Config by hand rather than via config.Load,
so cfg.Quota and cfg.Billing were zero-valued: the billing endpoints
returned an empty currency and a 0 rate, and the default quota read as 0
(unlimited), failing TestBillingMetersRunningServer, TestBillingAdminView,
and TestQuotaDefaultSelfView.

Set both in the harness: a production-shaped billing price list (so a
running server accrues a non-zero, currency-tagged bill) and a generous
default quota — positive (so the default self-view reads as a real cap)
but high enough that the global per-user limit never interferes with the
capacity/placement/lifecycle suites; the quota-enforcement tests set their
own small per-user overrides.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
QuotaRow read billing.* in the cost cell but only destructured
user/quota/usage from row, breaking the frontend build with TS2304.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@aarani aarani merged commit 1d52823 into main Jun 22, 2026
3 checks passed
@aarani aarani deleted the claude/users-quota-milestone-yylyfh branch June 22, 2026 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants