Var Groups (Environment Variables & Secrets)¶
Var Groups are Starform's primitive for environment variables and secrets. They replace per-service env var tables with a reusable, attachable unit that can be shared across services within a project. This mirrors Render's "Environment Groups" pattern, which is the strongest UX in the PaaS market for this concern.
§38.1 Core Model¶
A Var Group is a named collection of key-value entries. Each entry is flagged as either a variable (plaintext, shown in UI) or a secret (masked in UI, plaintext only retrievable by users with appropriate permissions).
A Var Group is attached to one or more services. A service can attach multiple Var Groups. Attachment
is environment-scoped — the same Var Group can be attached to a service in staging without being
attached in production, or vice versa.
§38.2 Data Model (Postgres)¶
var_groups
id UUID PK
project_id UUID FK → projects
name TEXT (unique per project)
created_at, updated_at
var_group_entries
id UUID PK
var_group_id UUID FK → var_groups
key TEXT
value TEXT (encrypted at rest via pgcrypto or application-level AES)
is_secret BOOLEAN
created_at, updated_at
UNIQUE(var_group_id, key)
service_var_groups (attachment table)
service_id UUID FK → services
var_group_id UUID FK → var_groups
environment TEXT -- 'production', 'staging', 'development', custom env name
attach_order INT -- explicit ordering for precedence
attached_at
PRIMARY KEY(service_id, var_group_id, environment)
Values are encrypted at rest in Postgres. Plaintext values are only returned via the API to users with
at least Project Developer role in a non-protected environment, or Project Admin /
Workspace Admin in any environment (per RBAC §15).
§38.3 Attachment & Precedence¶
When Shuttle renders a service's pod spec, it walks all Var Groups attached to the service for the
current environment, in attach_order ascending. Later attachments override earlier ones on key
collision.
This matches Kubernetes' default envFrom behavior — which silently overrides on duplicate keys.
Starform exposes this behavior rather than hiding it: conflicts are allowed, precedence is
deterministic, and the UI surfaces them visibly.
UI warning contract:
- When attaching Var Group B to a service that already has Var Group A, and A and B share any keys, the dashboard displays a non-blocking warning: "Var Group B defines
DATABASE_URL, which is already defined in Var Group A. Group B's value will take precedence because it is attached later. Reorder attachments if this is unintended." - Attachment proceeds without requiring confirmation. Users who want overrides get them; users who don't see the warning before they deploy.
- The warning persists in the service detail view as long as the conflict exists, so it's not just a one-shot dialog.
§38.4 Kubernetes Translation¶
Shuttle converts each attached Var Group into a separate Kubernetes Secret. A service with three
attached Var Groups gets three Secrets mounted via envFrom:
envFrom:
- secretRef: { name: vg-<var_group_a_id> }
- secretRef: { name: vg-<var_group_b_id> }
- secretRef: { name: vg-<var_group_c_id> }
The envFrom list is ordered by attach_order. Kubelet applies the later entries' values on collision
(see §38.3).
Why separate Secrets, not merged:
- Independent rotation. Editing one Var Group's values triggers an update to exactly one K8s Secret. Services attached to that group get the new values on next pod restart; services attached to other groups are untouched.
- Blast radius containment. A leaked Secret compromises only the keys in that Var Group, not all env vars for the service.
- Cleaner audit trail. Each K8s Secret maps 1:1 to a Starform Var Group, making it trivial to answer "which services currently mount this group's values?" via label selector.
- K8s object cost is negligible. At 10× more Secrets than the merged approach, a cluster running 1,000 services with an average of 2 Var Groups each has 2,000 Secrets — well within K8s scale limits.
§38.5 Environment Scoping via Annotations¶
Each K8s Secret created by Shuttle carries annotations identifying its origin:
metadata:
annotations:
starform.io/var-group-id: "vg_abc123"
starform.io/var-group-name: "database-credentials"
starform.io/environment: "production"
starform.io/project-id: "proj_456"
labels:
app.starform.io/managed-by: "shuttle"
app.starform.io/var-group: "vg_abc123"
Annotations let Shuttle correlate K8s Secrets back to Starform records without a reverse lookup. Labels enable selector-based operations (e.g., list all Secrets for a var group, garbage-collect orphaned Secrets).
Same Var Group, multiple environments: a Var Group attached to both staging and production services produces two distinct K8s Secrets — one per environment, annotated accordingly. They may contain different values if the Var Group itself has environment-specific entries (future enhancement) or identical values (current model).
§38.6 RBAC Integration¶
Var Groups inherit the environment protection model from §15:
- Workspace Owner/Admin: full access (view secrets, edit, delete) in all environments
- Project Admin: full access in all environments within the project
- Project Developer: can view secret values and edit Var Groups only when the attached environment is not protected. In protected environments (production by default), Developers can see masked values and attachment lists but cannot edit
- Project Viewer: can see masked values and attachment lists; never sees plaintext secrets; cannot edit
Audit log (future, per §39) records every secret-value read.
§38.7 Rotation Workflow¶
- User edits a Var Group entry (changes the plaintext value)
- Starbase writes new encrypted value to
var_group_entries, bumps the Var Group'supdated_attimestamp, and marks the group's version in desired state - Desired state payload for all clusters hosting services attached to this group gets a bumped version field
- On next Shuttle tick (≤30s), Shuttle detects the change, patches the corresponding K8s Secret with new data
- K8s does not automatically restart pods when a Secret changes. Shuttle triggers a rolling restart of affected services by computing a
starform.io/var-group-checksum(see §24B.2) over all attached Var Group values, and setting that annotation on the Deployment's pod template. Changing the checksum bumps the pod template hash, which forces a new ReplicaSet and a zero-downtime rolling rollout. Computation is deterministic: sort attached Var Groups byattach_order, serialize each group's entries (sorted by key) to canonical JSON, concatenate, and hash with SHA-256.
Customers can choose "apply immediately" (rolling restart now) or "apply on next deploy" (new values picked up by the next image push). Default is "apply immediately" for security-critical rotations.
§38.8 Out of Scope for MVP¶
Deferred
- External secret store integration (AWS Secrets Manager, HashiCorp Vault) — post-MVP (see §39.3 #38)
- Per-environment value overrides within a single Var Group — post-MVP; for now, create separate Var Groups per environment if values differ
- Secret rotation scheduling / TTL expiry — post-MVP
- Git-sync of Var Groups from a customer's repo — post-MVP
Cross-references
Secret name convention vg-<var_group_id> + the var-group-id label →
§24.1 · the rollout-triggering checksum annotation →
§24B.2 · values flow to Shuttle through the
desired-state payload → §32 /
§25.1 · AES-256-GCM encryption-at-rest catalog →
Security & Isolation · the protection model →
§15. Canonical map: Canonical Sources.