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¶
- Coding patterns
- Source: Seed_Data_Spec.md