-- Add slug to projects and create project_domains table for flexible multi-domain support. -- This enables: -- 1. Auto-generated random subdomains (e.g., k7m2x9p4.threesix.ai) -- 2. Optional custom subdomains (e.g., my-app.threesix.ai) -- 3. Multiple alias domains per project (e.g., www.myapp.com, myapp.com) -- 4. Clean tracking of all DNS records for resource management -- Add slug column to projects (immutable, auto-generated identifier) ALTER TABLE projects ADD COLUMN IF NOT EXISTS slug VARCHAR(16); -- Backfill existing projects with slugs derived from their IDs -- Uses first 8 chars of MD5 hash for consistent, reproducible slugs UPDATE projects SET slug = LOWER(SUBSTRING(MD5(id::text), 1, 8)) WHERE slug IS NULL; -- Make slug NOT NULL and UNIQUE after backfill ALTER TABLE projects ALTER COLUMN slug SET NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS idx_projects_slug ON projects(slug); -- Project domains table for flexible multi-domain support CREATE TABLE IF NOT EXISTS project_domains ( id SERIAL PRIMARY KEY, project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE, domain VARCHAR(255) NOT NULL, type VARCHAR(20) NOT NULL CHECK (type IN ('primary_auto', 'primary_custom', 'alias')), dns_record_id VARCHAR(64), -- Cloudflare record ID for cleanup dns_record_type VARCHAR(10), -- A, CNAME, etc. verified BOOLEAN DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT unique_domain UNIQUE (domain) ); -- Indexes for common queries CREATE INDEX IF NOT EXISTS idx_project_domains_project_id ON project_domains(project_id); CREATE INDEX IF NOT EXISTS idx_project_domains_type ON project_domains(type); -- Note: Migration of existing domain data was removed as projects table -- does not have domain/custom_domain columns. Domains are created via API -- when deploying projects with auto-generated slugs. -- Update trigger for project_domains updated_at CREATE OR REPLACE FUNCTION update_project_domains_updated_at() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS project_domains_updated_at ON project_domains; CREATE TRIGGER project_domains_updated_at BEFORE UPDATE ON project_domains FOR EACH ROW EXECUTE FUNCTION update_project_domains_updated_at(); -- Comments COMMENT ON TABLE project_domains IS 'Domains associated with projects - supports multiple domains per project'; COMMENT ON COLUMN project_domains.project_id IS 'Reference to parent project'; COMMENT ON COLUMN project_domains.domain IS 'Fully qualified domain name (e.g., k7m2x9p4.threesix.ai or www.myapp.com)'; COMMENT ON COLUMN project_domains.type IS 'Domain type: primary_auto (system-generated), primary_custom (user subdomain), alias (external domain)'; COMMENT ON COLUMN project_domains.dns_record_id IS 'Cloudflare DNS record ID for automated cleanup'; COMMENT ON COLUMN project_domains.dns_record_type IS 'DNS record type: A, CNAME, etc.'; COMMENT ON COLUMN project_domains.verified IS 'Whether domain ownership has been verified (for external domains)'; COMMENT ON COLUMN projects.slug IS 'Immutable 8-char random identifier used for auto-generated subdomain';