Landing page cookbook implementation (Weeks 1-4): Domain Infrastructure: - Add project_domains table with migration (013_project_domains.sql) - Add ProjectDomain model with domain types (primary_auto, primary_custom, alias) - Add SlugGenerator and ProjectDomainRepository interfaces - Implement postgres adapters for domain and slug management Service Layer: - Add domain CRUD methods to ProjectInfraService - Generate 8-char random slugs for auto-domains - Support custom subdomains during project creation - Add site_live health check to project status - Trigger CI build after template seeding Handler Updates: - Add DomainService interface and adapter pattern - Rewrite domain handlers to use database-backed service - Add proper error handling for duplicate/missing domains CI Integration: - Add TriggerBuild to CIProvider interface - Implement TriggerBuild in Woodpecker adapter - Manually trigger initial build after template seed Cookbook & Scripts: - Add landing-test.sh script for E2E testing - Add release.sh for version releases - Add logs.sh for quick log access Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
100 lines
4.0 KiB
PL/PgSQL
100 lines
4.0 KiB
PL/PgSQL
-- 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), 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 TEXT 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);
|
|
|
|
-- Migrate existing domain data to project_domains table
|
|
-- Only insert if the domain column has a value and doesn't already exist in project_domains
|
|
INSERT INTO project_domains (project_id, domain, type, dns_record_type, verified, created_at, updated_at)
|
|
SELECT
|
|
id,
|
|
domain,
|
|
'primary_auto',
|
|
'A',
|
|
TRUE,
|
|
created_at,
|
|
updated_at
|
|
FROM projects
|
|
WHERE domain IS NOT NULL
|
|
AND domain != ''
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM project_domains pd WHERE pd.domain = projects.domain
|
|
);
|
|
|
|
-- Migrate custom_domain if present
|
|
INSERT INTO project_domains (project_id, domain, type, dns_record_type, verified, created_at, updated_at)
|
|
SELECT
|
|
id,
|
|
custom_domain,
|
|
'primary_custom',
|
|
'A',
|
|
TRUE,
|
|
created_at,
|
|
updated_at
|
|
FROM projects
|
|
WHERE custom_domain IS NOT NULL
|
|
AND custom_domain != ''
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM project_domains pd WHERE pd.domain = projects.custom_domain
|
|
);
|
|
|
|
-- 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';
|