# Seed Data Specification

## Purpose

Define the minimum bootstrap data required for a greenfield launch of `db_schema_v1.sql`.
This is static reference data — SKU catalog, policy definitions, global policy values, the
default pricing plan, and role baseline metadata.

The seed is idempotent. Running it multiple times on the same database is safe.

## How to Run

```bash
# Apply schema first, then seed:
scripts/db-init.sh && psql $DATABASE_URL -f scripts/seed.sql
```

Or via the Makefile target:
```bash
make db-init && make seed
```

---

## What Gets Seeded

### 1. SKU Catalog (`sku_catalog`)

Compute SKU definitions used as the reference for node assignments, marketplace
display, allocation snapshots, and billing rate lookup. Active code uses the
resource-model fields (`resource_class`, `billing_unit`, `scheduling_unit`,
`resources`, `unit_price`). Legacy GPU-shaped SKU columns remain only for
placement substrate and deployed-row continuity.

| SKU | Vendor | Resource class | Scheduling unit | Billing unit | Shape | Rate |
|---|---|---|---|---|---|---:|
| `mi300x` | AMD | `gpu_baremetal` | `node` | `gpu_hour` | 8 MI300X | $4.00/GPU-hr |
| `compute-vm-small` | Local | `cpu_vm` | `vm` | `node_hour` | 2 vCPU, 8 GiB | $0.25/node-hr |
| `compute-vm-medium` | Local | `cpu_vm` | `vm` | `node_hour` | 4 vCPU, 16 GiB | $0.50/node-hr |
| `h200` | NVIDIA | `gpu_baremetal` | `node` | `gpu_hour` | 8 H200 | $4.00/GPU-hr |
| `h200-sxm-slice` | NVIDIA | `gpu_slice` | `slice` | `gpu_hour` | 1/2/4 H200 slices | $4.00/GPU-hr |
| `h100` | NVIDIA | `gpu_baremetal` | `node` | `gpu_hour` | 8 H100 | $2.00/GPU-hr |
| `mi210x` | AMD | `gpu_baremetal` | `node` | `gpu_hour` | 8 MI210X | $2.00/GPU-hr |
| `cs-3` | Cerebras | `gpu_baremetal` | `node` | `gpu_hour` | 1 CS-3 | $5.00/GPU-hr |
| `cs-4` | Cerebras | `gpu_baremetal` | `node` | `gpu_hour` | 1 CS-4 | $5.00/GPU-hr |

Billing calculation: `total_cost = unit_count_snapshot × billing_unit_price_minor × hours`.

`compute-vm-small` is the initial Proxmox-backed compute VM SKU for demo/user
app allocation testing. Its SKU identity is CPU and memory; storage remains an
attachable option outside the SKU, matching cloud-provider instance-type
models. Future control-plane VM SKUs such as `kube-master-vm` or
`slurm-controller-vm` need separate lifecycle contracts and must not be
overloaded onto compute VM SKUs.

Slice SKU resource profiles:

`h200-sxm-slice` stores mutable lab/runtime shape choices in
`sku_catalog.resource_profile` until they stabilize into a first-class table.
The current seed includes named `slice_vm_profiles` and a
`default_slice_vm_profile` rather than a hidden CPU/memory constant:

| Profile | GPU count | vCPU | Memory |
|---|---:|---:|---:|
| `h200_1g_12c_64g` | 1 | 12 | 64 GiB |
| `h200_1g_24c_64g` | 1 | 24 | 64 GiB |
| `h200_2g_48c_128g` | 2 | 48 | 128 GiB |
| `h200_4g_96c_256g` | 4 | 96 | 256 GiB |
| `h200_8g_192c_512g` | 8 | 192 | 512 GiB |

The default is `h200_1g_24c_64g`, matching the latest tested prototype. Future
implementation must snapshot the selected profile name and resolved CPU/memory
values onto the allocation placement so later seed updates do not change running
allocations.

### 2. OS Image Catalog (`os_images`)

Bootstrap OS image records used by launch prechecks and provisioning. The seed
keeps image compatibility explicit by SKU and target so compute VM, GPU slice,
and future bare-metal flows do not infer image compatibility from SKU names.

| Image | Target | Format | Compatible SKUs | Status |
|---|---|---|---|---|
| `ubuntu-24.04-h200-base` | `vm_slice` | `raw` | `h200-sxm-slice` | active |
| `ubuntu-24.04-h200-cuda` | `vm_slice` | `raw` | `h200-sxm-slice` | disabled placeholder |
| `ubuntu-noble-compute-vm` | `compute_vm` | `qcow2` | `compute-vm-small`, `compute-vm-medium` | active |
| `ubuntu-noble-maas-lxd-arm64` | `compute_vm` | `maas` | `compute-vm-tiny` | active |

`ubuntu-noble-compute-vm` is the demo Proxmox-backed cloud-init image used by
the compute VM warm-pool refill path. It is compatible only with the Proxmox
compute VM SKUs seeded today.

`ubuntu-noble-maas-lxd-arm64` is the Mac/UTM MAAS-LXD readiness image profile
for `compute-vm-tiny`. It intentionally stays separate from the Proxmox image
record because the provider source, architecture, and validation path differ.
It should remain provider-readiness capacity until image/artifact compatibility
and node-agent/app E2E are proven.

### 3. Policy Definitions (`platform_policy_definitions`)

Defines every valid policy key with its type, description, and allowed bounds. Must be
present before any `platform_policy_values` row can be inserted (FK constraint).

| Policy Key | Type | Default | Bounds | Purpose |
|---|---|---|---|---|
| `billing.low_balance_threshold_minor` | integer | 500 | min 1, max 100000 | Cents below which `platform.billing.low_balance_warning` is emitted |
| `billing.prepaid_runway_warning_hours` | integer | 24 | min 0, max 720 | Projected prepaid hours remaining below which active allocations emit `platform.billing.auto_release_pending`; tenant-admin configurable later |
| `billing.prepaid_depleted_action` | string | `restrict` | `restrict`, `force_release` | Action when prepaid balance is depleted: apply a financial restriction by default, or explicitly force-release active allocations |
| `billing.window_seconds` | integer | 60 | min 30, max 3600 | Billing accrual interval |
| `billing.minimum_deposit_minor` | integer | 1000 | min 100, max 1000000 | Minimum Stripe checkout amount (cents) |
| `billing.maximum_deposit_minor` | integer | 100000 | min 1000, max 10000000 | Maximum Stripe checkout amount (cents) |
| `allocation.max_concurrent_per_user` | integer | 50 | min 1, max 100 | Max active allocations per user |
| `allocation.refund_window_days` | integer | 30 | min 0, max 365 | Days after credit posting for refund eligibility |
| `rate_limit.api_requests_per_minute` | integer | 120 | min 1, max 10000 | API rate limit per authenticated user |
| `rate_limit.platform_proxy_requests_per_minute` | integer | 1200 | min 1, max 10000 | Browser-facing platform proxy traffic rate limit per authenticated user |
| `rate_limit.v3_read_model_requests_per_minute` | integer | 2000 | min 1, max 10000 | V3 read-model GET rate limit for dense UI pages and route-smoke navigation |
| `rate_limit.terminal_token_requests_per_minute` | integer | 10 | min 1, max 1000 | Terminal token minting rate limit |
| `rate_limit.financial_requests_per_minute` | integer | 30 | min 1, max 1000 | Financial endpoint rate limit |
| `rate_limit.admin_overview_requests_per_minute` | integer | 600 | min 1, max 5000 | Admin overview polling rate limit |
| `terminal.session_max_ttl_seconds` | integer | 14400 | min 300, max 86400 | Maximum lifetime of an active terminal session (independent of token TTL) |
| `auth.service_account_token_ttl_seconds` | integer | 900 | min 60, max 3600 | Service-account access token lifetime in seconds |
| `auth.federation_state_ttl_seconds` | integer | 600 | min 60, max 3600 | OIDC/SAML enterprise sign-in state lifetime in seconds |
| `audit.dedupe_window_seconds` | integer | 300 | min 30, max 3600 | Window for coalescing repeated identical controller/token retry audit outcomes into structured logs and metrics |
| `notification.low_balance_enabled` | boolean | true | — | Enable low-balance warning notifications |
| `notification.balance_depleted_enabled` | boolean | true | — | Enable balance-depleted notifications |
| `allocation.isolation_model` | string | `user-revoke` | `user-revoke`, `full-reimage` | Node isolation strategy between allocations: `user-revoke` revokes the OS user and SSH key (fast, ~10 s); `full-reimage` triggers a MAAS re-deploy (full OS wipe, ~5–15 min). Requires `maas.enabled=true` for `full-reimage`. |
| `maas.enabled` | boolean | false | — | Enable Canonical MAAS bare-metal integration. When true, the provisioning worker uses `packages/services/maas.MAASClient` for machine deploy/release; cloud-init userdata is delivered via the MAAS deploy API. |

Rate-limit strategy:
- `rate_limit.api_requests_per_minute` is the global default.
- Middleware resolves route-class overrides (`platform_proxy`, `v3_read_model`, `terminal_token`, `financial`, `admin_overview`) and falls back to global default.

### 4. Global Policy Values (`platform_policy_values`)

One `scope_type = 'global'` row per policy key, setting the system-wide default.
Per-org or per-user overrides can be added via the Admin API post-launch without
touching the seed.

The global values match the `default_value` column in `platform_policy_definitions`. They are
inserted conditionally — if a global value already exists for a key the row is skipped
(idempotent by design).

### 5. Default Pricing Plan (`pricing_plans`)

A single `standard` pay-as-you-go plan is seeded with a stable UUID so it can be
referenced safely in subsequent scripts. New plans can be added via the Admin API.

Seed UUID: `10000000-0000-4000-8000-000000000001`

### 6. Dev User Tenant/Project Bootstrap (conditional)

For each existing user row, seed ensures:
- a personal tenant exists,
- a default project exists,
- active tenant/project membership rows exist.

This is idempotent and only normalizes already-existing users; it does not create new users.

### 7. Platform Role Baseline

Seed inserts built-in platform role definitions and v1 role versions:
- `platform_superadmin`
- `platform_ops`
- `platform_user`

For existing users with `users.role='admin'`, seed inserts active `platform_role_bindings`
to `platform_superadmin`. This preserves admin reachability when
`PLATFORM_ROLE_SOURCE=bindings` is enabled.

### 8. App Catalog Baseline

Seed inserts the baseline app catalog plus one active version for each seeded app.

Current seeded catalog entries:
- `ollama` -> runtime backend `bare_metal`
- `meta-llama-3-8b` -> runtime backend `bare_metal`
- `mlflow` -> runtime backend `bare_metal`
- `open-webui` -> runtime backend `bare_metal`
- `nginx` -> runtime backend `bare_metal`
- `postgresql` -> runtime backend `bare_metal`
- `slurm-reference` -> runtime backend `slurm`
- `rke2-self-managed` -> runtime backend `rke2`
- `jupyterlab` -> runtime backend `bare_metal`
- `vllm-openai` -> runtime backend `bare_metal`
- `code-server` -> runtime backend `bare_metal`
- `openclaw` -> runtime backend `bare_metal`

The `rke2-self-managed` entry is the first-slice self-managed Kubernetes validation
offering. It is intentionally narrow and is not the managed-Kubernetes product
contract.

`code-server` and `openclaw` are developer workspace app entries seeded through
the same launchable OCI manifest path. They validate the compute-VM direction
without adding app-specific API routes.

Seed also registers digest-pinned local/dev/demo OCI artifacts for these
developer workspace apps in every default project:
- `code-server` -> `ghcr.io/coder/code-server:4.101.2` linux/amd64 and
  linux/arm64 manifests
- `openclaw` -> `ghcr.io/openclaw/openclaw:latest` linux/amd64 and linux/arm64
  manifests

These artifact rows are scoped to local/dev/demo bootstrap. Production curated
app artifacts should still flow through the app artifact publish/verify path.

---

## What Does NOT Get Seeded

| Data | Reason |
|---|---|
| Users | No user data in seed — including no admin user (see Admin Bootstrap below) |
| User SSH public keys (`user_ssh_public_keys`) | User-provided runtime credentials; never seeded |
| User POSIX identities (`platform_iam_user_posix_identities`) | Created lazily on first provisioning request per user; not seeded |
| Nodes | Nodes are registered post-launch via `POST /api/v1/admin/nodes` |
| Organizations / Projects | Not pre-created globally; created on demand and conditionally bootstrapped for existing users |
| Ledger entries / usage records | Transactional data — never seeded |
| Stripe events | Live data — never seeded |
| Audit logs | Immutable live data — never seeded |

---

## Admin Bootstrap Procedure

The first admin account is **not created in the seed SQL** — admin credentials in SQL
files are a security risk.

### Development (local)

The Keycloak dev realm includes a `dev-admin` user (`admin123`) with the `admin` realm
role. On first OIDC login via the app, the auth service creates the user record in
`users` with `role = 'admin'` (mapped from Keycloak's `realm_access.roles` claim).

No manual DB intervention required for local dev.

Local reset convenience:
- `make db-reset-fast` and `make dev-reset-full-web` run
  `scripts/seed-local-dev-identities.sh` after schema + seed.
- This script bootstraps local-only identities aligned to current Keycloak `sub`
  claims for the V3 persona set: `dev-user`, `dev-project-admin`,
  `dev-tenant-admin`, `dev-platform-admin`, and `dev-ops`.
- The legacy `dev-admin` smoke account remains available for scripts and broad
  admin testing.
- `scripts/seed.sql` still does not seed users.

### API-backed local bootstrap helpers

Use SQL seed for foundational reference data only. Use API-backed helpers for
runtime/product-managed objects once the owning domain API exists, so local and
kind setup exercises the same validation, idempotency, audit, and authz paths
as real operators.

Current classification:

| Bootstrap data | Owner | Mechanism |
|---|---|---|
| Policy definitions/values | Platform policy | `scripts/seed.sql` |
| Static catalog baselines | Inventory/app platform | `scripts/seed.sql` until CRUD ownership is complete |
| Local identity subject alignment | Auth/dev harness | `scripts/seed-local-dev-identities.sh` because it must align Keycloak `sub` claims with DB users |
| MAAS local site/profile | Provisioning/MAAS | `make seed-local-maas` through admin APIs |
| Persona platform roles | IAM/platform roles | `make seed-local-platform-roles-api` through admin APIs |

`make seed-local-platform-roles-api` binds `dev-admin`,
`dev-platform-admin`, and `dev-ops` through
`POST /api/v1/admin/users/{user_id}/platform-roles` and verifies the result
through `GET /api/v1/admin/users/{user_id}/platform-roles`. Run it after the API
is up. It is intentionally separate from `db-reset-fast`, because `db-reset-fast`
must remain usable before API containers are started.

### Production

1. Create the first admin account in Keycloak admin console (`http://<keycloak>/admin`).
2. Assign the `admin` realm role.
3. Log in via the app OIDC flow — the user record is created automatically with `role = admin`.
4. All subsequent admin operations are available via `POST /api/v1/admin/*` endpoints.

If OIDC is not yet configured, use the `POST /api/v1/auth/personal/login` endpoint
(requires `password_hash` set directly in the DB via a one-time bootstrap script — not
the seed — and must be removed after OIDC is operational).

---

## Idempotency Guarantee

All inserts in `seed.sql` use either:
- `ON CONFLICT (pk) DO NOTHING` for tables with stable primary keys
- A `WHERE NOT EXISTS` guard for `platform_policy_values` (global scope uniqueness is enforced
  by a partial unique index, not the PK)

Running the seed after a partial failure or re-deployment is safe.

## SSH Key Selection Policy (runtime, not seed)

- Users upload keys via `POST /api/v1/ssh-keys`.
- Exactly one active key per user may be marked default (`is_default = true`).
- New allocations use the user's default active key unless the request sets `ssh_key_ids`.
- `PUT /api/v1/allocations/{allocation_id}/ssh-keys` supports per-allocation multi-key override and rotation.
