template-saas/apps/database/ddl/schemas/feature_flags/tables/01-flags.sql
rckrdmrd 26f0e52ca7 feat: Initial commit - template-saas
Template base para proyectos SaaS multi-tenant.

Estructura inicial:
- apps/backend (NestJS API)
- apps/frontend (React/Vite)
- apps/database (PostgreSQL DDL)
- docs/ (Documentación)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 04:41:24 -06:00

231 lines
7.3 KiB
PL/PgSQL

-- ============================================
-- TEMPLATE-SAAS: Feature Flags
-- Schema: feature_flags
-- Version: 1.0.0
-- ============================================
-- Feature flag definitions (global)
CREATE TABLE feature_flags.flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Flag identification
code VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
category VARCHAR(100), -- 'billing', 'ui', 'api', 'experimental'
-- Status
status feature_flags.flag_status DEFAULT 'disabled' NOT NULL,
-- Rollout
rollout_stage feature_flags.rollout_stage DEFAULT 'development',
rollout_percentage INT DEFAULT 0, -- 0-100
-- Default value
default_value BOOLEAN DEFAULT FALSE,
-- Targeting rules (JSONB)
targeting_rules JSONB DEFAULT '[]'::jsonb,
-- Example:
-- [
-- { "type": "plan", "operator": "in", "values": ["pro", "enterprise"] },
-- { "type": "user_attribute", "attribute": "country", "operator": "eq", "value": "MX" }
-- ]
-- Metadata
metadata JSONB DEFAULT '{}'::jsonb,
tags JSONB DEFAULT '[]'::jsonb,
-- Lifecycle
is_permanent BOOLEAN DEFAULT FALSE, -- Won't be cleaned up
expires_at TIMESTAMPTZ,
-- Audit
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
created_by VARCHAR(100),
-- Constraints
CONSTRAINT valid_flag_code CHECK (code ~ '^[a-z][a-z0-9_]*$'),
CONSTRAINT valid_percentage CHECK (rollout_percentage >= 0 AND rollout_percentage <= 100)
);
-- Tenant-specific flag overrides
CREATE TABLE feature_flags.tenant_flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
flag_id UUID NOT NULL REFERENCES feature_flags.flags(id) ON DELETE CASCADE,
-- Override value
is_enabled BOOLEAN NOT NULL,
-- Reason
reason VARCHAR(500),
-- Audit
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_by UUID,
-- Constraints
CONSTRAINT unique_tenant_flag UNIQUE (tenant_id, flag_id)
);
-- User-specific flag overrides
CREATE TABLE feature_flags.user_flags (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
user_id UUID NOT NULL REFERENCES users.users(id) ON DELETE CASCADE,
flag_id UUID NOT NULL REFERENCES feature_flags.flags(id) ON DELETE CASCADE,
-- Override value
is_enabled BOOLEAN NOT NULL,
-- Reason
reason VARCHAR(500),
-- Expiration
expires_at TIMESTAMPTZ,
-- Audit
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
updated_by UUID,
-- Constraints
CONSTRAINT unique_user_flag UNIQUE (user_id, flag_id)
);
-- Flag evaluation history (for analytics)
CREATE TABLE feature_flags.evaluations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id UUID NOT NULL REFERENCES tenants.tenants(id) ON DELETE CASCADE,
flag_id UUID NOT NULL REFERENCES feature_flags.flags(id) ON DELETE CASCADE,
user_id UUID REFERENCES users.users(id),
-- Result
flag_code VARCHAR(100) NOT NULL,
result BOOLEAN NOT NULL,
evaluation_reason VARCHAR(100), -- 'default', 'tenant_override', 'user_override', 'targeting_rule', 'percentage'
-- Context
context JSONB DEFAULT '{}'::jsonb,
-- Timestamp
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
);
-- Indexes
CREATE INDEX idx_flags_code ON feature_flags.flags(code);
CREATE INDEX idx_flags_status ON feature_flags.flags(status) WHERE status != 'disabled';
CREATE INDEX idx_flags_category ON feature_flags.flags(category);
CREATE INDEX idx_tenant_flags_tenant ON feature_flags.tenant_flags(tenant_id);
CREATE INDEX idx_tenant_flags_flag ON feature_flags.tenant_flags(flag_id);
CREATE INDEX idx_user_flags_user ON feature_flags.user_flags(user_id);
CREATE INDEX idx_user_flags_tenant ON feature_flags.user_flags(tenant_id);
CREATE INDEX idx_evaluations_tenant ON feature_flags.evaluations(tenant_id, created_at DESC);
CREATE INDEX idx_evaluations_flag ON feature_flags.evaluations(flag_id, created_at DESC);
-- RLS
ALTER TABLE feature_flags.tenant_flags ENABLE ROW LEVEL SECURITY;
ALTER TABLE feature_flags.user_flags ENABLE ROW LEVEL SECURITY;
ALTER TABLE feature_flags.evaluations ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_flags_isolation ON feature_flags.tenant_flags
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY user_flags_isolation ON feature_flags.user_flags
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
CREATE POLICY evaluations_isolation ON feature_flags.evaluations
USING (tenant_id = current_setting('app.current_tenant_id', true)::UUID);
-- Trigger
CREATE OR REPLACE FUNCTION feature_flags.update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_flags_updated_at
BEFORE UPDATE ON feature_flags.flags
FOR EACH ROW
EXECUTE FUNCTION feature_flags.update_updated_at();
CREATE TRIGGER trg_tenant_flags_updated_at
BEFORE UPDATE ON feature_flags.tenant_flags
FOR EACH ROW
EXECUTE FUNCTION feature_flags.update_updated_at();
CREATE TRIGGER trg_user_flags_updated_at
BEFORE UPDATE ON feature_flags.user_flags
FOR EACH ROW
EXECUTE FUNCTION feature_flags.update_updated_at();
-- Function to evaluate flag
CREATE OR REPLACE FUNCTION feature_flags.evaluate_flag(
p_flag_code VARCHAR,
p_tenant_id UUID,
p_user_id UUID DEFAULT NULL
)
RETURNS BOOLEAN AS $$
DECLARE
v_flag RECORD;
v_tenant_override RECORD;
v_user_override RECORD;
v_result BOOLEAN;
BEGIN
-- Get flag
SELECT * INTO v_flag FROM feature_flags.flags WHERE code = p_flag_code;
IF NOT FOUND THEN
RETURN FALSE;
END IF;
-- Check user override
IF p_user_id IS NOT NULL THEN
SELECT * INTO v_user_override
FROM feature_flags.user_flags
WHERE flag_id = v_flag.id AND user_id = p_user_id
AND (expires_at IS NULL OR expires_at > NOW());
IF FOUND THEN
RETURN v_user_override.is_enabled;
END IF;
END IF;
-- Check tenant override
SELECT * INTO v_tenant_override
FROM feature_flags.tenant_flags
WHERE flag_id = v_flag.id AND tenant_id = p_tenant_id;
IF FOUND THEN
RETURN v_tenant_override.is_enabled;
END IF;
-- Check status
IF v_flag.status = 'disabled' THEN
RETURN FALSE;
ELSIF v_flag.status = 'enabled' THEN
RETURN TRUE;
ELSIF v_flag.status = 'percentage' THEN
-- Simple percentage rollout based on tenant_id
RETURN (abs(hashtext(p_tenant_id::TEXT)) % 100) < v_flag.rollout_percentage;
END IF;
RETURN v_flag.default_value;
END;
$$ LANGUAGE plpgsql STABLE;
-- Comments
COMMENT ON TABLE feature_flags.flags IS 'Global feature flag definitions';
COMMENT ON TABLE feature_flags.tenant_flags IS 'Tenant-specific flag overrides';
COMMENT ON TABLE feature_flags.user_flags IS 'User-specific flag overrides';
COMMENT ON TABLE feature_flags.evaluations IS 'Flag evaluation history for analytics';
COMMENT ON COLUMN feature_flags.flags.targeting_rules IS 'JSON rules for advanced targeting';