Skip to content

Database Schema

Procella uses PostgreSQL 17 for all metadata and state. The schema is managed through Drizzle ORM migrations that run automatically on server startup.

projects ◄──── stacks ◄──── updates ◄──── update_events
checkpoints

Namespace for stacks. Each project is identified by a tenant ID (from Descope JWT) and a name.

ColumnTypeConstraints
idUUIDPK, default gen_random_uuid()
tenantIdTEXTNOT NULL (from Descope JWT)
nameTEXTNOT NULL
descriptionTEXT
createdAtTIMESTAMPNOT NULL DEFAULT now()
updatedAtTIMESTAMPNOT NULL DEFAULT now()
UNIQUE (tenantId, name)

Index: idx_projects_tenant_name on (tenantId, name) — fast lookup by tenant and project name.

The core entity. Each stack belongs to a project and tracks its current active update.

ColumnTypeConstraints
idUUIDPK, default gen_random_uuid()
projectIdUUIDFK → projects(id) ON DELETE CASCADE
nameTEXTNOT NULL
tagsJSONBNOT NULL DEFAULT '{}'
activeUpdateIdUUIDNullable — set when an update is running
createdAtTIMESTAMPNOT NULL DEFAULT now()
updatedAtTIMESTAMPNOT NULL DEFAULT now()
UNIQUE (projectId, name)

Index: idx_stacks_project_name on (projectId, name) — fast lookup by project and stack name.

Tracks every operation performed on a stack (update, preview, refresh, destroy, import, etc.).

ColumnTypeConstraints
idUUIDPK, default gen_random_uuid()
stackIdUUIDSoft reference (no FK) — identifies the stack
kindTEXTNOT NULL — update, preview, refresh, destroy, import, etc.
statusTEXTNOT NULL DEFAULT 'not started' — not started, requested, running, succeeded, failed, cancelled
resultTEXTNullable — final result message
messageTEXTNullable — status message
versionINTNOT NULL DEFAULT 1 — checkpoint version
leaseTokenTEXTNullable — token for execution phase
leaseExpiresAtTIMESTAMPNullable — lease expiration time
startedAtTIMESTAMPNullable
completedAtTIMESTAMPNullable
createdAtTIMESTAMPNOT NULL DEFAULT now()
updatedAtTIMESTAMPNOT NULL DEFAULT now()
configJSONBNOT NULL DEFAULT '{}' — stack config
programJSONBNOT NULL DEFAULT '{}' — program metadata

Index: idx_updates_activePartial unique on (stackId) WHERE status IN ('not started', 'requested', 'running') — prevents concurrent updates on the same stack.

Infrastructure state snapshots. Each checkpoint is associated with an update and a version number.

ColumnTypeConstraints
idUUIDPK, default gen_random_uuid()
updateIdUUIDFK → updates(id) ON DELETE CASCADE
stackIdUUIDSoft reference — identifies the stack
versionINTNOT NULL — checkpoint version
dataJSONBNOT NULL — deployment state
blobKeyTEXTNullable — reference to blob storage
isDeltaBOOLEANNOT NULL DEFAULT false — whether this is a delta checkpoint
createdAtTIMESTAMPNOT NULL DEFAULT now()

Index: idx_checkpoints_update_version on (updateId, version) — fast lookup of checkpoints per update.

Engine events emitted during an update (resource operations, diagnostics, outputs).

ColumnTypeConstraints
idUUIDPK, default gen_random_uuid()
updateIdUUIDFK → updates(id) ON DELETE CASCADE
sequenceINTNOT NULL — event sequence number
kindTEXTNOT NULL — event type
fieldsJSONBNOT NULL — event data
createdAtTIMESTAMPNOT NULL DEFAULT now()
UNIQUE (updateId, sequence)

Index: idx_update_events_update_sequence on (updateId, sequence) — ordered event retrieval.

IndexTablePurpose
idx_projects_tenant_nameprojects(tenantId, name) — fast lookup by tenant and project
idx_stacks_project_namestacks(projectId, name) — fast lookup by project and stack
idx_updates_activeupdatesPartial unique: (stackId) WHERE status IN ('not started', 'requested', 'running') — prevents concurrent updates
idx_checkpoints_update_versioncheckpoints(updateId, version) — fast checkpoint lookup
idx_update_events_update_sequenceupdate_events(updateId, sequence) — ordered event retrieval

When creating a stack, Procella auto-creates the project if it doesn’t exist using Drizzle’s INSERT ... ON CONFLICT DO NOTHING:

await db.insert(projects).values({
id: projectId,
tenantId,
name: projectName,
}).onConflictDoNothing();

This simplifies the CLI workflow — pulumi stack init creates everything in one step.

The GC worker uses PostgreSQL advisory locks for cluster-safe execution:

const lockId = 0x5472617461_4743; // GC lock (historic value, do not change)
const acquired = await db.execute(
sql`SELECT pg_try_advisory_lock(${lockId})`
);
// ... do GC work ...
await db.execute(sql`SELECT pg_advisory_unlock(${lockId})`);

This ensures only one replica runs garbage collection at a time, even in a multi-instance deployment. The lock is acquired per-cycle and released after each cycle completes.

Foreign keys use ON DELETE CASCADE:

  • Deleting a project cascades to stacks, updates, events, checkpoints
  • Deleting a stack cascades to updates, events, checkpoints
  • Deleting an update cascades to events, checkpoints

This means pulumi stack rm cleanly removes all associated data.

Migrations are managed by Drizzle Kit (drizzle-kit) and run automatically on server startup. The schema is defined in TypeScript in packages/db/src/schema.ts and migrations are generated and applied via Drizzle’s migration system.