Files
git.stella-ops.org/docs/modules/release-orchestrator/data-model/schema.md

26 KiB

Database Schema (PostgreSQL)

This document specifies the complete PostgreSQL schema for the Release Orchestrator.

Schema Organization

All release orchestration tables reside in the release schema:

CREATE SCHEMA IF NOT EXISTS release;
SET search_path TO release, public;

Core Tables

Tenant and Authority Extensions

-- Extended: Add release-related permissions
ALTER TABLE permissions ADD COLUMN IF NOT EXISTS
  resource_type VARCHAR(50) CHECK (resource_type IN (
    'environment', 'release', 'promotion', 'target', 'workflow', 'plugin'
  ));

Integration Hub

CREATE TABLE integration_types (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name                VARCHAR(100) NOT NULL UNIQUE,
    category            VARCHAR(50) NOT NULL CHECK (category IN (
                          'scm', 'ci', 'registry', 'vault', 'target', 'router'
                        )),
    plugin_id           UUID REFERENCES plugins(id),
    config_schema       JSONB NOT NULL,
    secrets_schema      JSONB NOT NULL,
    is_builtin          BOOLEAN NOT NULL DEFAULT FALSE,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE integrations (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    integration_type_id UUID NOT NULL REFERENCES integration_types(id),
    name                VARCHAR(255) NOT NULL,
    config              JSONB NOT NULL,
    credential_ref      VARCHAR(500),              -- Vault path or encrypted ref
    status              VARCHAR(50) NOT NULL DEFAULT 'unknown' CHECK (status IN (
                          'healthy', 'degraded', 'unhealthy', 'unknown'
                        )),
    last_health_check   TIMESTAMPTZ,
    last_health_message TEXT,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by          UUID REFERENCES users(id),
    UNIQUE (tenant_id, name)
);

CREATE INDEX idx_integrations_tenant ON integrations(tenant_id);
CREATE INDEX idx_integrations_type ON integrations(integration_type_id);

CREATE TABLE connection_profiles (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    user_id             UUID NOT NULL REFERENCES users(id),
    integration_type_id UUID NOT NULL REFERENCES integration_types(id),
    name                VARCHAR(255) NOT NULL,
    config_defaults     JSONB NOT NULL,
    is_default          BOOLEAN NOT NULL DEFAULT FALSE,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (tenant_id, user_id, integration_type_id, name)
);

Environment & Inventory

CREATE TABLE environments (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    name                VARCHAR(100) NOT NULL,
    display_name        VARCHAR(255) NOT NULL,
    order_index         INTEGER NOT NULL,
    config              JSONB NOT NULL DEFAULT '{}',
    freeze_windows      JSONB NOT NULL DEFAULT '[]',
    required_approvals  INTEGER NOT NULL DEFAULT 0,
    require_sod         BOOLEAN NOT NULL DEFAULT FALSE,
    auto_promote_from   UUID REFERENCES environments(id),
    promotion_policy    VARCHAR(255),
    deployment_timeout  INTEGER NOT NULL DEFAULT 600,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (tenant_id, name)
);

CREATE INDEX idx_environments_tenant ON environments(tenant_id);
CREATE INDEX idx_environments_order ON environments(tenant_id, order_index);

CREATE TABLE target_groups (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    environment_id      UUID NOT NULL REFERENCES environments(id) ON DELETE CASCADE,
    name                VARCHAR(255) NOT NULL,
    labels              JSONB NOT NULL DEFAULT '{}',
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (tenant_id, environment_id, name)
);

CREATE TABLE targets (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    environment_id      UUID NOT NULL REFERENCES environments(id) ON DELETE CASCADE,
    target_group_id     UUID REFERENCES target_groups(id),
    name                VARCHAR(255) NOT NULL,
    target_type         VARCHAR(100) NOT NULL,
    connection          JSONB NOT NULL,
    capabilities        JSONB NOT NULL DEFAULT '[]',
    labels              JSONB NOT NULL DEFAULT '{}',
    deployment_directory VARCHAR(500),
    health_status       VARCHAR(50) NOT NULL DEFAULT 'unknown' CHECK (health_status IN (
                          'healthy', 'degraded', 'unhealthy', 'unknown'
                        )),
    last_health_check   TIMESTAMPTZ,
    current_digest      VARCHAR(100),
    agent_id            UUID REFERENCES agents(id),
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (tenant_id, environment_id, name)
);

CREATE INDEX idx_targets_tenant_env ON targets(tenant_id, environment_id);
CREATE INDEX idx_targets_type ON targets(target_type);
CREATE INDEX idx_targets_labels ON targets USING GIN (labels);

CREATE TABLE agents (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    name                VARCHAR(255) NOT NULL,
    version             VARCHAR(50) NOT NULL,
    capabilities        JSONB NOT NULL DEFAULT '[]',
    labels              JSONB NOT NULL DEFAULT '{}',
    status              VARCHAR(50) NOT NULL DEFAULT 'offline' CHECK (status IN (
                          'online', 'offline', 'degraded'
                        )),
    last_heartbeat      TIMESTAMPTZ,
    resource_usage      JSONB,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (tenant_id, name)
);

CREATE INDEX idx_agents_tenant ON agents(tenant_id);
CREATE INDEX idx_agents_status ON agents(status);
CREATE INDEX idx_agents_capabilities ON agents USING GIN (capabilities);

Release Management

CREATE TABLE components (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    name                VARCHAR(255) NOT NULL,
    display_name        VARCHAR(255) NOT NULL,
    image_repository    VARCHAR(500) NOT NULL,
    registry_integration_id UUID REFERENCES integrations(id),
    versioning_strategy JSONB NOT NULL DEFAULT '{"type": "semver"}',
    deployment_template VARCHAR(255),
    default_channel     VARCHAR(50) NOT NULL DEFAULT 'stable',
    metadata            JSONB NOT NULL DEFAULT '{}',
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (tenant_id, name)
);

CREATE INDEX idx_components_tenant ON components(tenant_id);

CREATE TABLE version_maps (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    component_id        UUID NOT NULL REFERENCES components(id) ON DELETE CASCADE,
    tag                 VARCHAR(255) NOT NULL,
    digest              VARCHAR(100) NOT NULL,
    semver              VARCHAR(50),
    channel             VARCHAR(50) NOT NULL DEFAULT 'stable',
    prerelease          BOOLEAN NOT NULL DEFAULT FALSE,
    build_metadata      VARCHAR(255),
    resolved_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    source              VARCHAR(50) NOT NULL DEFAULT 'auto' CHECK (source IN ('auto', 'manual')),
    UNIQUE (tenant_id, component_id, digest)
);

CREATE INDEX idx_version_maps_component ON version_maps(component_id);
CREATE INDEX idx_version_maps_digest ON version_maps(digest);
CREATE INDEX idx_version_maps_semver ON version_maps(semver);

CREATE TABLE releases (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    name                VARCHAR(255) NOT NULL,
    display_name        VARCHAR(255) NOT NULL,
    components          JSONB NOT NULL,            -- [{componentId, digest, semver, tag, role}]
    source_ref          JSONB,                     -- {scmIntegrationId, commitSha, ciIntegrationId, buildId}
    status              VARCHAR(50) NOT NULL DEFAULT 'draft' CHECK (status IN (
                          'draft', 'ready', 'promoting', 'deployed', 'deprecated', 'archived'
                        )),
    metadata            JSONB NOT NULL DEFAULT '{}',
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by          UUID REFERENCES users(id),
    UNIQUE (tenant_id, name)
);

CREATE INDEX idx_releases_tenant ON releases(tenant_id);
CREATE INDEX idx_releases_status ON releases(status);
CREATE INDEX idx_releases_created ON releases(created_at DESC);

CREATE TABLE release_environment_state (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    environment_id      UUID NOT NULL REFERENCES environments(id) ON DELETE CASCADE,
    release_id          UUID NOT NULL REFERENCES releases(id),
    status              VARCHAR(50) NOT NULL CHECK (status IN (
                          'deployed', 'deploying', 'failed', 'rolling_back', 'rolled_back'
                        )),
    deployed_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    deployed_by         UUID REFERENCES users(id),
    promotion_id        UUID,                      -- will reference promotions
    evidence_ref        VARCHAR(255),
    UNIQUE (tenant_id, environment_id)
);

CREATE INDEX idx_release_env_state_env ON release_environment_state(environment_id);
CREATE INDEX idx_release_env_state_release ON release_environment_state(release_id);

Workflow Engine

CREATE TABLE workflow_templates (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID REFERENCES tenants(id) ON DELETE CASCADE, -- NULL for builtin
    name                VARCHAR(255) NOT NULL,
    display_name        VARCHAR(255) NOT NULL,
    description         TEXT,
    version             INTEGER NOT NULL DEFAULT 1,
    nodes               JSONB NOT NULL,
    edges               JSONB NOT NULL,
    inputs              JSONB NOT NULL DEFAULT '[]',
    outputs             JSONB NOT NULL DEFAULT '[]',
    is_builtin          BOOLEAN NOT NULL DEFAULT FALSE,
    tags                JSONB NOT NULL DEFAULT '[]',
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    created_by          UUID REFERENCES users(id),
    UNIQUE (tenant_id, name, version)
);

CREATE INDEX idx_workflow_templates_tenant ON workflow_templates(tenant_id);
CREATE INDEX idx_workflow_templates_builtin ON workflow_templates(is_builtin);

CREATE TABLE workflow_runs (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    template_id         UUID NOT NULL REFERENCES workflow_templates(id),
    template_version    INTEGER NOT NULL,
    status              VARCHAR(50) NOT NULL DEFAULT 'created' CHECK (status IN (
                          'created', 'running', 'paused', 'succeeded', 'failed', 'cancelled'
                        )),
    context             JSONB NOT NULL,            -- inputs, variables, release info
    outputs             JSONB,
    started_at          TIMESTAMPTZ,
    completed_at        TIMESTAMPTZ,
    error_message       TEXT,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    triggered_by        UUID REFERENCES users(id)
);

CREATE INDEX idx_workflow_runs_tenant ON workflow_runs(tenant_id);
CREATE INDEX idx_workflow_runs_status ON workflow_runs(status);
CREATE INDEX idx_workflow_runs_template ON workflow_runs(template_id);

CREATE TABLE step_runs (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    workflow_run_id     UUID NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
    node_id             VARCHAR(100) NOT NULL,
    step_type           VARCHAR(100) NOT NULL,
    status              VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN (
                          'pending', 'running', 'succeeded', 'failed', 'skipped', 'retrying', 'cancelled'
                        )),
    inputs              JSONB NOT NULL,
    config              JSONB NOT NULL,
    outputs             JSONB,
    started_at          TIMESTAMPTZ,
    completed_at        TIMESTAMPTZ,
    attempt_number      INTEGER NOT NULL DEFAULT 1,
    error_message       TEXT,
    error_type          VARCHAR(100),
    logs                TEXT,
    artifacts           JSONB NOT NULL DEFAULT '[]',
    t_hlc               BIGINT,                    -- Hybrid Logical Clock for ordering (optional)
    ts_wall             TIMESTAMPTZ,               -- Wall-clock timestamp for debugging (optional)
    UNIQUE (workflow_run_id, node_id, attempt_number)
);

CREATE INDEX idx_step_runs_workflow ON step_runs(workflow_run_id);
CREATE INDEX idx_step_runs_status ON step_runs(status);

Promotion & Approval

CREATE TABLE promotions (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    release_id          UUID NOT NULL REFERENCES releases(id),
    source_environment_id UUID REFERENCES environments(id),
    target_environment_id UUID NOT NULL REFERENCES environments(id),
    status              VARCHAR(50) NOT NULL DEFAULT 'pending_approval' CHECK (status IN (
                          'pending_approval', 'pending_gate', 'approved', 'rejected',
                          'deploying', 'deployed', 'failed', 'cancelled', 'rolled_back'
                        )),
    decision_record     JSONB,
    workflow_run_id     UUID REFERENCES workflow_runs(id),
    requested_at        TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    requested_by        UUID NOT NULL REFERENCES users(id),
    request_reason      TEXT,
    decided_at          TIMESTAMPTZ,
    started_at          TIMESTAMPTZ,
    completed_at        TIMESTAMPTZ,
    evidence_packet_id  UUID,
    t_hlc               BIGINT,                    -- Hybrid Logical Clock for ordering (optional)
    ts_wall             TIMESTAMPTZ                -- Wall-clock timestamp for debugging (optional)
);

CREATE INDEX idx_promotions_tenant ON promotions(tenant_id);
CREATE INDEX idx_promotions_release ON promotions(release_id);
CREATE INDEX idx_promotions_status ON promotions(status);
CREATE INDEX idx_promotions_target_env ON promotions(target_environment_id);

-- Add FK to release_environment_state
ALTER TABLE release_environment_state
  ADD CONSTRAINT fk_release_env_state_promotion
  FOREIGN KEY (promotion_id) REFERENCES promotions(id);

CREATE TABLE approvals (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    promotion_id        UUID NOT NULL REFERENCES promotions(id) ON DELETE CASCADE,
    approver_id         UUID NOT NULL REFERENCES users(id),
    action              VARCHAR(50) NOT NULL CHECK (action IN ('approved', 'rejected')),
    comment             TEXT,
    approved_at         TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    approver_role       VARCHAR(255),
    approver_groups     JSONB NOT NULL DEFAULT '[]'
);

CREATE INDEX idx_approvals_promotion ON approvals(promotion_id);
CREATE INDEX idx_approvals_approver ON approvals(approver_id);

CREATE TABLE approval_policies (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    environment_id      UUID NOT NULL REFERENCES environments(id) ON DELETE CASCADE,
    required_count      INTEGER NOT NULL DEFAULT 1,
    required_roles      JSONB NOT NULL DEFAULT '[]',
    required_groups     JSONB NOT NULL DEFAULT '[]',
    require_sod         BOOLEAN NOT NULL DEFAULT FALSE,
    allow_self_approval BOOLEAN NOT NULL DEFAULT FALSE,
    expiration_minutes  INTEGER NOT NULL DEFAULT 1440,
    UNIQUE (tenant_id, environment_id)
);

Deployment

CREATE TABLE deployment_jobs (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    promotion_id        UUID NOT NULL REFERENCES promotions(id),
    release_id          UUID NOT NULL REFERENCES releases(id),
    environment_id      UUID NOT NULL REFERENCES environments(id),
    status              VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN (
                          'pending', 'running', 'succeeded', 'failed', 'cancelled', 'rolling_back', 'rolled_back'
                        )),
    strategy            VARCHAR(50) NOT NULL DEFAULT 'all-at-once',
    started_at          TIMESTAMPTZ,
    completed_at        TIMESTAMPTZ,
    artifacts           JSONB NOT NULL DEFAULT '[]',
    rollback_of         UUID REFERENCES deployment_jobs(id),
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    t_hlc               BIGINT,                    -- Hybrid Logical Clock for ordering (optional)
    ts_wall             TIMESTAMPTZ                -- Wall-clock timestamp for debugging (optional)
);

CREATE INDEX idx_deployment_jobs_promotion ON deployment_jobs(promotion_id);
CREATE INDEX idx_deployment_jobs_status ON deployment_jobs(status);

CREATE TABLE deployment_tasks (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    job_id              UUID NOT NULL REFERENCES deployment_jobs(id) ON DELETE CASCADE,
    target_id           UUID NOT NULL REFERENCES targets(id),
    digest              VARCHAR(100) NOT NULL,
    status              VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN (
                          'pending', 'running', 'succeeded', 'failed', 'cancelled', 'skipped'
                        )),
    agent_id            UUID REFERENCES agents(id),
    started_at          TIMESTAMPTZ,
    completed_at        TIMESTAMPTZ,
    exit_code           INTEGER,
    logs                TEXT,
    previous_digest     VARCHAR(100),
    sticker_written     BOOLEAN NOT NULL DEFAULT FALSE,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_deployment_tasks_job ON deployment_tasks(job_id);
CREATE INDEX idx_deployment_tasks_target ON deployment_tasks(target_id);
CREATE INDEX idx_deployment_tasks_status ON deployment_tasks(status);

CREATE TABLE generated_artifacts (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    deployment_job_id   UUID REFERENCES deployment_jobs(id) ON DELETE CASCADE,
    artifact_type       VARCHAR(50) NOT NULL CHECK (artifact_type IN (
                          'compose_lock', 'script', 'sticker', 'evidence', 'config'
                        )),
    name                VARCHAR(255) NOT NULL,
    content_hash        VARCHAR(100) NOT NULL,
    content             BYTEA,                     -- for small artifacts
    storage_ref         VARCHAR(500),              -- for large artifacts (S3, etc.)
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_generated_artifacts_job ON generated_artifacts(deployment_job_id);

Progressive Delivery

CREATE TABLE ab_releases (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    environment_id      UUID NOT NULL REFERENCES environments(id),
    name                VARCHAR(255) NOT NULL,
    variations          JSONB NOT NULL,            -- [{name, releaseId, targetGroupId, trafficPercentage}]
    active_variation    VARCHAR(50) NOT NULL DEFAULT 'A',
    traffic_split       JSONB NOT NULL,
    rollout_strategy    JSONB NOT NULL,
    status              VARCHAR(50) NOT NULL DEFAULT 'created' CHECK (status IN (
                          'created', 'deploying', 'running', 'promoting', 'completed', 'rolled_back'
                        )),
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    completed_at        TIMESTAMPTZ,
    created_by          UUID REFERENCES users(id)
);

CREATE INDEX idx_ab_releases_tenant_env ON ab_releases(tenant_id, environment_id);
CREATE INDEX idx_ab_releases_status ON ab_releases(status);

CREATE TABLE canary_stages (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    ab_release_id       UUID NOT NULL REFERENCES ab_releases(id) ON DELETE CASCADE,
    stage_number        INTEGER NOT NULL,
    traffic_percentage  INTEGER NOT NULL,
    status              VARCHAR(50) NOT NULL DEFAULT 'pending' CHECK (status IN (
                          'pending', 'running', 'succeeded', 'failed', 'skipped'
                        )),
    health_threshold    DECIMAL(5,2),
    duration_seconds    INTEGER,
    require_approval    BOOLEAN NOT NULL DEFAULT FALSE,
    started_at          TIMESTAMPTZ,
    completed_at        TIMESTAMPTZ,
    health_result       JSONB,
    UNIQUE (ab_release_id, stage_number)
);

Release Evidence

CREATE TABLE evidence_packets (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    promotion_id        UUID NOT NULL REFERENCES promotions(id),
    packet_type         VARCHAR(50) NOT NULL CHECK (packet_type IN (
                          'release_decision', 'deployment', 'rollback', 'ab_promotion'
                        )),
    content             JSONB NOT NULL,
    content_hash        VARCHAR(100) NOT NULL,
    signature           TEXT,
    signer_key_ref      VARCHAR(255),
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
    -- Note: No UPDATE or DELETE allowed (append-only)
);

CREATE INDEX idx_evidence_packets_promotion ON evidence_packets(promotion_id);
CREATE INDEX idx_evidence_packets_created ON evidence_packets(created_at DESC);

-- Append-only enforcement via trigger
CREATE OR REPLACE FUNCTION prevent_evidence_modification()
RETURNS TRIGGER AS $$
BEGIN
    RAISE EXCEPTION 'Evidence packets are immutable and cannot be modified or deleted';
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER evidence_packets_immutable
BEFORE UPDATE OR DELETE ON evidence_packets
FOR EACH ROW EXECUTE FUNCTION prevent_evidence_modification();

CREATE TABLE version_stickers (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    target_id           UUID NOT NULL REFERENCES targets(id),
    deployment_job_id   UUID REFERENCES deployment_jobs(id),
    release_id          UUID NOT NULL REFERENCES releases(id),
    digest              VARCHAR(100) NOT NULL,
    sticker_content     JSONB NOT NULL,
    written_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    verified_at         TIMESTAMPTZ,
    verification_status VARCHAR(50) CHECK (verification_status IN ('valid', 'mismatch', 'missing'))
);

CREATE INDEX idx_version_stickers_target ON version_stickers(target_id);
CREATE INDEX idx_version_stickers_release ON version_stickers(release_id);

Plugin Infrastructure

CREATE TABLE plugins (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name                VARCHAR(255) NOT NULL UNIQUE,
    display_name        VARCHAR(255) NOT NULL,
    version             VARCHAR(50) NOT NULL,
    description         TEXT,
    manifest            JSONB NOT NULL,
    status              VARCHAR(50) NOT NULL DEFAULT 'inactive' CHECK (status IN (
                          'active', 'inactive', 'error'
                        )),
    error_message       TEXT,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE plugin_instances (
    id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id           UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
    plugin_id           UUID NOT NULL REFERENCES plugins(id),
    config              JSONB NOT NULL DEFAULT '{}',
    enabled             BOOLEAN NOT NULL DEFAULT TRUE,
    created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    UNIQUE (tenant_id, plugin_id)
);

CREATE INDEX idx_plugin_instances_tenant ON plugin_instances(tenant_id);


Hybrid Logical Clock (HLC) for Distributed Ordering

Optional Enhancement: For strict distributed ordering and multi-region support, the following tables include optional t_hlc (Hybrid Logical Clock timestamp) and ts_wall (wall-clock timestamp) columns:

  • promotions — Promotion state transitions
  • deployment_jobs — Deployment task ordering
  • step_runs — Workflow step execution ordering

When to use HLC:

  • Multi-region deployments requiring strict causal ordering
  • Deterministic replay across distributed systems
  • Timeline event ordering in audit logs

HLC Schema:

t_hlc    BIGINT       -- HLC timestamp (monotonic, skew-tolerant)
ts_wall  TIMESTAMPTZ  -- Wall-clock timestamp (informational)

Usage:

  • t_hlc is generated by IHybridLogicalClock.Tick() on state transitions
  • ts_wall is populated by TimeProvider.GetUtcNow() for debugging
  • Index on t_hlc for ordering queries: CREATE INDEX idx_promotions_hlc ON promotions(t_hlc);

Reference: See Implementation Guide for HLC usage patterns.


Row-Level Security (Multi-Tenancy)

All tables with tenant_id should have RLS enabled:

-- Enable RLS on all release tables
ALTER TABLE integrations ENABLE ROW LEVEL SECURITY;
ALTER TABLE environments ENABLE ROW LEVEL SECURITY;
ALTER TABLE targets ENABLE ROW LEVEL SECURITY;
ALTER TABLE releases ENABLE ROW LEVEL SECURITY;
ALTER TABLE promotions ENABLE ROW LEVEL SECURITY;
-- ... etc.

-- Example policy
CREATE POLICY tenant_isolation ON integrations
    FOR ALL
    USING (tenant_id = current_setting('app.tenant_id')::UUID);