-- Create initial schema for certificate control plane -- IDs are TEXT to support application-generated prefixed IDs (e.g., "team-123", "cert-456") -- Table: teams CREATE TABLE IF NOT EXISTS teams ( id TEXT PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, description TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_teams_name ON teams(name); -- Table: owners CREATE TABLE IF NOT EXISTS owners ( id TEXT PRIMARY KEY, name VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_owners_email ON owners(email); CREATE INDEX IF NOT EXISTS idx_owners_team_id ON owners(team_id); -- Table: renewal_policies CREATE TABLE IF NOT EXISTS renewal_policies ( id TEXT PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, renewal_window_days INT NOT NULL, auto_renew BOOLEAN NOT NULL DEFAULT true, max_retries INT NOT NULL DEFAULT 3, retry_interval_minutes INT NOT NULL DEFAULT 60, alert_thresholds_days JSONB NOT NULL DEFAULT '[30, 14, 7, 0]', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_renewal_policies_name ON renewal_policies(name); -- Table: issuers CREATE TABLE IF NOT EXISTS issuers ( id TEXT PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, type VARCHAR(255) NOT NULL, config JSONB, enabled BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_issuers_name ON issuers(name); CREATE INDEX IF NOT EXISTS idx_issuers_enabled ON issuers(enabled); -- Table: agents CREATE TABLE IF NOT EXISTS agents ( id TEXT PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, hostname VARCHAR(255) NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'offline', last_heartbeat_at TIMESTAMPTZ, registered_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), api_key_hash VARCHAR(255) NOT NULL ); CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status); CREATE INDEX IF NOT EXISTS idx_agents_hostname ON agents(hostname); CREATE INDEX IF NOT EXISTS idx_agents_last_heartbeat_at ON agents(last_heartbeat_at); -- Table: managed_certificates CREATE TABLE IF NOT EXISTS managed_certificates ( id TEXT PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, common_name VARCHAR(255) NOT NULL, sans TEXT[] DEFAULT ARRAY[]::TEXT[], environment VARCHAR(50), owner_id TEXT NOT NULL REFERENCES owners(id) ON DELETE RESTRICT, team_id TEXT NOT NULL REFERENCES teams(id) ON DELETE CASCADE, issuer_id TEXT NOT NULL REFERENCES issuers(id) ON DELETE RESTRICT, renewal_policy_id TEXT NOT NULL REFERENCES renewal_policies(id) ON DELETE RESTRICT, status VARCHAR(50) NOT NULL DEFAULT 'pending', expires_at TIMESTAMPTZ, tags JSONB NOT NULL DEFAULT '{}', last_renewal_at TIMESTAMPTZ, last_deployment_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_managed_certificates_status ON managed_certificates(status); CREATE INDEX IF NOT EXISTS idx_managed_certificates_expires_at ON managed_certificates(expires_at); CREATE INDEX IF NOT EXISTS idx_managed_certificates_owner_id ON managed_certificates(owner_id); CREATE INDEX IF NOT EXISTS idx_managed_certificates_team_id ON managed_certificates(team_id); CREATE INDEX IF NOT EXISTS idx_managed_certificates_issuer_id ON managed_certificates(issuer_id); CREATE INDEX IF NOT EXISTS idx_managed_certificates_name ON managed_certificates(name); -- Table: deployment_targets CREATE TABLE IF NOT EXISTS deployment_targets ( id TEXT PRIMARY KEY, name VARCHAR(255) NOT NULL, type VARCHAR(255) NOT NULL, agent_id TEXT NOT NULL REFERENCES agents(id) ON DELETE CASCADE, config JSONB, enabled BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_deployment_targets_agent_id ON deployment_targets(agent_id); CREATE INDEX IF NOT EXISTS idx_deployment_targets_enabled ON deployment_targets(enabled); CREATE INDEX IF NOT EXISTS idx_deployment_targets_name ON deployment_targets(name); -- Table: certificate_target_mappings CREATE TABLE IF NOT EXISTS certificate_target_mappings ( certificate_id TEXT NOT NULL REFERENCES managed_certificates(id) ON DELETE CASCADE, target_id TEXT NOT NULL REFERENCES deployment_targets(id) ON DELETE CASCADE, PRIMARY KEY (certificate_id, target_id) ); CREATE INDEX IF NOT EXISTS idx_certificate_target_mappings_target_id ON certificate_target_mappings(target_id); -- Table: certificate_versions CREATE TABLE IF NOT EXISTS certificate_versions ( id TEXT PRIMARY KEY, certificate_id TEXT NOT NULL REFERENCES managed_certificates(id) ON DELETE CASCADE, serial_number VARCHAR(255) NOT NULL, not_before TIMESTAMPTZ NOT NULL, not_after TIMESTAMPTZ NOT NULL, fingerprint_sha256 VARCHAR(255) NOT NULL UNIQUE, pem_chain TEXT NOT NULL, csr_pem TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_certificate_versions_certificate_id ON certificate_versions(certificate_id); CREATE INDEX IF NOT EXISTS idx_certificate_versions_fingerprint ON certificate_versions(fingerprint_sha256); -- Table: jobs CREATE TABLE IF NOT EXISTS jobs ( id TEXT PRIMARY KEY, type VARCHAR(255) NOT NULL, certificate_id TEXT REFERENCES managed_certificates(id) ON DELETE CASCADE, target_id TEXT REFERENCES deployment_targets(id) ON DELETE SET NULL, agent_id TEXT REFERENCES agents(id) ON DELETE SET NULL, status VARCHAR(50) NOT NULL DEFAULT 'pending', attempts INT NOT NULL DEFAULT 0, max_attempts INT NOT NULL DEFAULT 3, last_error TEXT, deployment_result JSONB, scheduled_at TIMESTAMPTZ, started_at TIMESTAMPTZ, completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status); CREATE INDEX IF NOT EXISTS idx_jobs_certificate_id ON jobs(certificate_id); CREATE INDEX IF NOT EXISTS idx_jobs_scheduled_at ON jobs(scheduled_at); CREATE INDEX IF NOT EXISTS idx_jobs_agent_id ON jobs(agent_id); -- Table: policy_rules CREATE TABLE IF NOT EXISTS policy_rules ( id TEXT PRIMARY KEY, name VARCHAR(255) NOT NULL UNIQUE, type VARCHAR(255) NOT NULL, config JSONB NOT NULL, enabled BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_policy_rules_name ON policy_rules(name); CREATE INDEX IF NOT EXISTS idx_policy_rules_enabled ON policy_rules(enabled); -- Table: policy_violations CREATE TABLE IF NOT EXISTS policy_violations ( id TEXT PRIMARY KEY, certificate_id TEXT NOT NULL REFERENCES managed_certificates(id) ON DELETE CASCADE, rule_id TEXT NOT NULL REFERENCES policy_rules(id) ON DELETE CASCADE, message TEXT NOT NULL, severity VARCHAR(50) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_policy_violations_certificate_id ON policy_violations(certificate_id); CREATE INDEX IF NOT EXISTS idx_policy_violations_rule_id ON policy_violations(rule_id); CREATE INDEX IF NOT EXISTS idx_policy_violations_severity ON policy_violations(severity); -- Table: audit_events CREATE TABLE IF NOT EXISTS audit_events ( id TEXT PRIMARY KEY, actor VARCHAR(255) NOT NULL, actor_type VARCHAR(50) NOT NULL, action VARCHAR(255) NOT NULL, resource_type VARCHAR(255) NOT NULL, resource_id VARCHAR(255) NOT NULL, details JSONB, timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_audit_events_resource_type_id ON audit_events(resource_type, resource_id); CREATE INDEX IF NOT EXISTS idx_audit_events_actor ON audit_events(actor); CREATE INDEX IF NOT EXISTS idx_audit_events_timestamp ON audit_events(timestamp); CREATE INDEX IF NOT EXISTS idx_audit_events_action ON audit_events(action); -- Table: notification_events CREATE TABLE IF NOT EXISTS notification_events ( id TEXT PRIMARY KEY, type VARCHAR(255) NOT NULL, certificate_id TEXT REFERENCES managed_certificates(id) ON DELETE CASCADE, channel VARCHAR(255) NOT NULL, recipient VARCHAR(255) NOT NULL, message TEXT NOT NULL, sent_at TIMESTAMPTZ, status VARCHAR(50) NOT NULL DEFAULT 'pending', error TEXT ); CREATE INDEX IF NOT EXISTS idx_notification_events_certificate_id ON notification_events(certificate_id); CREATE INDEX IF NOT EXISTS idx_notification_events_status ON notification_events(status); CREATE INDEX IF NOT EXISTS idx_notification_events_type ON notification_events(type); -- Performance indexes for scheduler queries (added for v1.0 hardening) CREATE INDEX IF NOT EXISTS idx_jobs_status_scheduled_at ON jobs(status, scheduled_at); CREATE INDEX IF NOT EXISTS idx_certificate_versions_cert_created ON certificate_versions(certificate_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_audit_events_timestamp_desc ON audit_events(timestamp DESC); CREATE INDEX IF NOT EXISTS idx_agents_online_heartbeat ON agents(status, last_heartbeat_at) WHERE status = 'online'; CREATE UNIQUE INDEX IF NOT EXISTS idx_deployment_targets_agent_name ON deployment_targets(agent_id, name);