Skip to content

Policy keys

Implemented

Source: scripts/seed.sql · doc/architecture/Seed_Data_Spec.md · packages/shared/policy

All runtime business-policy values come from policy_values via PolicyClient. No hardcoded constants in service code. Test-only constants in _test.go are allowed.

Scope resolution

flowchart TB
    REQ[PolicyClient.Get key] --> S1{user_id scope}
    S1 -- hit --> H1[return user-scoped value]
    S1 -- miss --> S2{project_id scope}
    S2 -- hit --> H2[return project-scoped value]
    S2 -- miss --> S3{org_id scope}
    S3 -- hit --> H3[return org-scoped value]
    S3 -- miss --> S4{plan scope}
    S4 -- hit --> H4[return plan-scoped value]
    S4 -- miss --> H5[return global default]

Most-specific wins. Every level has bounded min, max, or enum.

Catalog

Billing

Key Type Default Meaning
billing.low_balance_threshold_minor int 500 Cents below which warning fires
billing.window_seconds int 60 Billing accrual interval
billing.minimum_deposit_minor int 1000 Minimum Stripe checkout (cents)
billing.maximum_deposit_minor int 100000 Maximum Stripe checkout (cents)

Allocation

Key Type Default Meaning
allocation.max_concurrent_per_user int 50 Max active allocations per user
allocation.refund_window_days int 30 Refund eligibility window
allocation.isolation_model string user-revoke Node isolation between allocations: user-revoke or full-reimage

Rate limits

Key Type Default Meaning
rate_limit.api_requests_per_minute int 120 Default per-user
rate_limit.terminal_token_requests_per_minute int 10 Terminal token mint
rate_limit.financial_requests_per_minute int 30 Payments / refunds / balance
rate_limit.admin_overview_requests_per_minute int 600 Admin overview polling

Terminal

Key Type Default Meaning
terminal.session_max_ttl_seconds int 14400 Max active session lifetime (4 h). Gateway + node-agent enforce.

Notification

Key Type Default Meaning
notification.low_balance_enabled bool true Enable low-balance alerts
notification.balance_depleted_enabled bool true Enable depletion alerts

Auth

Key Type Default Meaning
auth.service_account_token_ttl_seconds int 900 Service-account access token TTL

MAAS

Key Type Default Meaning
maas.enabled bool false Enable MAAS bare-metal integration

How keys are read

// service.go
limit, err := s.policy.GetInt(ctx, policy.Scope{UserID: userID}, "allocation.max_concurrent_per_user")
if err != nil { return err }

if active >= limit {
    return ErrAllocationConcurrencyLimit
}

How keys are written

Admin API mutation routed through packages/services/admin/:

POST /api/v1/admin/policy-values
{
  "key":          "allocation.refund_window_days",
  "scope":        {"org_id": "..."},
  "value":        45,
  "effective_at": "2026-06-01T00:00:00Z",
  "reason":       "Customer success request",
  "old_value":    30
}

Writes: - policy_values row - audit_logs row with metadata.policy_key, metadata.old_value, metadata.new_value, metadata.reason

Bounds validation

Every key carries min/max/enum bounds. Admin API rejects out-of-bound values before the write.

Why this matters

  • Operational agility: change behavior without redeploying code.
  • Auditability: every change has a who/what/before/after/when/reason.
  • Testability: tests inject their own values without touching globals.
  • Multi-tenant flexibility: enterprise overrides land via scope, not branching code.

CI gate

Lint-level review catches hardcoded numeric/string constants in handler/service code that match a policy key name. The deeper rule (no hardcoded business constants anywhere) is reviewer-enforced.

Where to look next