CREATE TABLE IF NOT EXISTS clusters ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), slug TEXT NOT NULL UNIQUE, name TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', region TEXT, metadata JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT clusters_status_check CHECK (status IN ('active', 'disabled', 'archived', 'degraded')) ); INSERT INTO clusters (slug, name, status, region, metadata) VALUES ('default', 'Default Cluster', 'active', NULL, '{"bootstrap":true}'::jsonb) ON CONFLICT (slug) DO NOTHING; CREATE TABLE IF NOT EXISTS cluster_memberships ( cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE, node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, membership_status TEXT NOT NULL DEFAULT 'active', joined_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), last_seen_at TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}'::JSONB, PRIMARY KEY (cluster_id, node_id), CONSTRAINT cluster_memberships_status_check CHECK (membership_status IN ('active', 'draining', 'disabled', 'revoked')) ); INSERT INTO cluster_memberships (cluster_id, node_id, membership_status, joined_at, last_seen_at, metadata) SELECT c.id, n.id, 'active', COALESCE(n.created_at, NOW()), n.last_seen_at, '{"backfilled":true}'::jsonb FROM clusters c CROSS JOIN nodes n WHERE c.slug = 'default' ON CONFLICT (cluster_id, node_id) DO NOTHING; CREATE TABLE IF NOT EXISTS node_identities ( node_id UUID PRIMARY KEY REFERENCES nodes(id) ON DELETE CASCADE, public_key TEXT NOT NULL, certificate_serial TEXT, certificate_not_before TIMESTAMPTZ, certificate_not_after TIMESTAMPTZ, identity_status TEXT NOT NULL DEFAULT 'pending', rotated_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, metadata JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT node_identities_status_check CHECK (identity_status IN ('pending', 'active', 'rotating', 'revoked')) ); CREATE TABLE IF NOT EXISTS node_join_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, scope JSONB NOT NULL DEFAULT '{}'::JSONB, expires_at TIMESTAMPTZ NOT NULL, max_uses INTEGER NOT NULL DEFAULT 1, used_count INTEGER NOT NULL DEFAULT 0, status TEXT NOT NULL DEFAULT 'active', created_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), revoked_at TIMESTAMPTZ, CONSTRAINT node_join_tokens_status_check CHECK (status IN ('active', 'revoked', 'expired')), CONSTRAINT node_join_tokens_max_uses_check CHECK (max_uses > 0), CONSTRAINT node_join_tokens_used_count_check CHECK (used_count >= 0) ); CREATE INDEX IF NOT EXISTS idx_node_join_tokens_cluster_status ON node_join_tokens(cluster_id, status, expires_at); CREATE TABLE IF NOT EXISTS node_join_requests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE, join_token_id UUID REFERENCES node_join_tokens(id) ON DELETE SET NULL, node_name TEXT NOT NULL, node_fingerprint TEXT NOT NULL, public_key TEXT NOT NULL, reported_capabilities JSONB NOT NULL DEFAULT '{}'::JSONB, reported_facts JSONB NOT NULL DEFAULT '{}'::JSONB, requested_roles JSONB NOT NULL DEFAULT '[]'::JSONB, status TEXT NOT NULL DEFAULT 'pending', reviewed_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, reviewed_at TIMESTAMPTZ, approved_node_id UUID REFERENCES nodes(id) ON DELETE SET NULL, rejection_reason TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT node_join_requests_status_check CHECK (status IN ('pending', 'approved', 'rejected', 'cancelled')) ); CREATE INDEX IF NOT EXISTS idx_node_join_requests_cluster_status ON node_join_requests(cluster_id, status, created_at DESC); CREATE UNIQUE INDEX IF NOT EXISTS idx_node_join_requests_pending_fingerprint ON node_join_requests(cluster_id, node_fingerprint) WHERE status = 'pending'; CREATE TABLE IF NOT EXISTS node_role_assignments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE, node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE, role TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', policy JSONB NOT NULL DEFAULT '{}'::JSONB, assigned_by_user_id UUID REFERENCES users(id) ON DELETE SET NULL, assigned_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), revoked_at TIMESTAMPTZ, CONSTRAINT node_role_assignments_status_check CHECK (status IN ('active', 'disabled', 'revoked')), CONSTRAINT node_role_assignments_role_check CHECK (role IN ( 'entry-node', 'relay-node', 'core-mesh', 'rdp-worker', 'vnc-worker', 'vpn-exit', 'vpn-connector', 'file-storage-cache', 'update-cache', 'video-relay' )) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_node_role_assignments_unique_active ON node_role_assignments(cluster_id, node_id, role, COALESCE(organization_id, '00000000-0000-0000-0000-000000000000'::uuid)) WHERE status = 'active'; CREATE INDEX IF NOT EXISTS idx_node_role_assignments_cluster ON node_role_assignments(cluster_id, role, status); CREATE TABLE IF NOT EXISTS node_heartbeats ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE, node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, health_status TEXT NOT NULL DEFAULT 'unknown', reported_version TEXT, capabilities JSONB NOT NULL DEFAULT '{}'::JSONB, service_states JSONB NOT NULL DEFAULT '{}'::JSONB, metadata JSONB NOT NULL DEFAULT '{}'::JSONB, observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT node_heartbeats_health_status_check CHECK (health_status IN ('unknown', 'healthy', 'warning', 'critical')) ); CREATE INDEX IF NOT EXISTS idx_node_heartbeats_cluster_node_observed ON node_heartbeats(cluster_id, node_id, observed_at DESC); CREATE TABLE IF NOT EXISTS node_latest_heartbeats ( cluster_id UUID NOT NULL REFERENCES clusters(id) ON DELETE CASCADE, node_id UUID NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, heartbeat_id UUID REFERENCES node_heartbeats(id) ON DELETE SET NULL, health_status TEXT NOT NULL DEFAULT 'unknown', reported_version TEXT, capabilities JSONB NOT NULL DEFAULT '{}'::JSONB, service_states JSONB NOT NULL DEFAULT '{}'::JSONB, metadata JSONB NOT NULL DEFAULT '{}'::JSONB, observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), PRIMARY KEY (cluster_id, node_id), CONSTRAINT node_latest_heartbeats_health_status_check CHECK (health_status IN ('unknown', 'healthy', 'warning', 'critical')) ); CREATE TABLE IF NOT EXISTS cluster_audit_events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), cluster_id UUID REFERENCES clusters(id) ON DELETE SET NULL, actor_user_id UUID REFERENCES users(id) ON DELETE SET NULL, event_type TEXT NOT NULL, target_type TEXT NOT NULL, target_id TEXT, payload JSONB NOT NULL DEFAULT '{}'::JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_cluster_audit_events_cluster_created ON cluster_audit_events(cluster_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_cluster_audit_events_type_created ON cluster_audit_events(event_type, created_at DESC);