-- +goose Up -- +goose StatementBegin -- Upstream backends (target servers behind the reverse-proxy). CREATE TABLE IF NOT EXISTS backends ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, scheme TEXT NOT NULL DEFAULT 'http', address TEXT NOT NULL, port INTEGER NOT NULL, health_check_path TEXT, active BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT backends_name_unique UNIQUE (name), CONSTRAINT backends_scheme_check CHECK (scheme IN ('http', 'https')), CONSTRAINT backends_port_check CHECK (port > 0 AND port < 65536) ); CREATE INDEX IF NOT EXISTS idx_backends_active ON backends (active) WHERE active; -- Public-facing domains. primary_backend_id is the default upstream -- when no path-rule matches. CREATE TABLE IF NOT EXISTS domains ( id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL, active BOOLEAN NOT NULL DEFAULT TRUE, primary_backend_id BIGINT REFERENCES backends(id) ON DELETE SET NULL, http_to_https BOOLEAN NOT NULL DEFAULT TRUE, hsts_enabled BOOLEAN NOT NULL DEFAULT FALSE, notes TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT domains_name_unique UNIQUE (name) ); CREATE INDEX IF NOT EXISTS idx_domains_active ON domains (active) WHERE active; -- Path-based routing rules. Higher priority wins; ties broken by id. CREATE TABLE IF NOT EXISTS routing_rules ( id BIGSERIAL PRIMARY KEY, domain_id BIGINT NOT NULL REFERENCES domains(id) ON DELETE CASCADE, path_prefix TEXT NOT NULL DEFAULT '/', backend_id BIGINT NOT NULL REFERENCES backends(id) ON DELETE RESTRICT, priority INTEGER NOT NULL DEFAULT 100, active 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_routing_rules_domain ON routing_rules (domain_id); CREATE INDEX IF NOT EXISTS idx_routing_rules_priority ON routing_rules (domain_id, priority DESC); -- ACME-managed TLS certificates. status mirrors certbot's view: -- pending — issue requested, not yet completed -- active — issued, currently deployed -- renewing — renewal in progress -- expired — past not_after, awaiting cleanup -- error — last attempt failed (see audit_log for detail) CREATE TABLE IF NOT EXISTS tls_certs ( id BIGSERIAL PRIMARY KEY, domain TEXT NOT NULL, issuer TEXT NOT NULL DEFAULT 'letsencrypt', status TEXT NOT NULL DEFAULT 'pending', cert_path TEXT, key_path TEXT, not_before TIMESTAMPTZ, not_after TIMESTAMPTZ, last_renewed_at TIMESTAMPTZ, last_error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT tls_certs_domain_unique UNIQUE (domain), CONSTRAINT tls_certs_status_check CHECK (status IN ('pending', 'active', 'renewing', 'expired', 'error')) ); CREATE INDEX IF NOT EXISTS idx_tls_certs_not_after ON tls_certs (not_after); CREATE INDEX IF NOT EXISTS idx_tls_certs_status ON tls_certs (status); -- +goose StatementEnd -- +goose Down -- +goose StatementBegin DROP TABLE IF EXISTS tls_certs; DROP TABLE IF EXISTS routing_rules; DROP TABLE IF EXISTS domains; DROP TABLE IF EXISTS backends; -- +goose StatementEnd