90ac0b81d1
Add full V2 architecture: identity, content, studio (.NET 10) and file, render, notification, gateway (Go) services with vendored deps, plus DB migrations, event/API contracts, and an init-db script. Wire the Next.js frontend to the gateway: server-side JWT auth routes (login/register/refresh/logout/me), gateway fetch helper, and session/ cookie/jwt helpers under src/lib. Containerize the stack via docker-compose.v2.yml and per-service Dockerfiles. Base images resolve through a Nexus mirror (Docker Hub) and MCR directly; npm/NuGet pull from Nexus groups. Self-host fonts via next/font/local to avoid Google Fonts (geo-blocked). Add CI workflow and ignore .env.v2, *.stackdump, and .NET bin/obj. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
353 lines
14 KiB
SQL
353 lines
14 KiB
SQL
-- =====================================================================
|
|
-- IDENTITY SCHEMA — Part 1: Tenants (Multi-tenancy / Reseller API)
|
|
-- =====================================================================
|
|
-- Every user, project, render, export belongs to a tenant.
|
|
-- FlatRender's own customers belong to the DEFAULT tenant.
|
|
-- Resellers (B2B) are their own tenants with their own users.
|
|
-- =====================================================================
|
|
|
|
SET search_path TO identity, public;
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- tenants — companies / resellers (and FlatRender itself = default)
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TYPE tenant_status AS ENUM ('Active','Trial','Suspended','Cancelled');
|
|
CREATE TYPE tenant_kind AS ENUM ('Internal','Reseller','Enterprise');
|
|
|
|
CREATE TABLE tenants (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
slug CITEXT UNIQUE NOT NULL, -- 'flatrender', 'acme', ...
|
|
name TEXT NOT NULL,
|
|
kind tenant_kind NOT NULL DEFAULT 'Reseller',
|
|
status tenant_status NOT NULL DEFAULT 'Trial',
|
|
|
|
-- Domains
|
|
custom_domain CITEXT UNIQUE, -- videos.acme.com
|
|
domain_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
allowed_origins TEXT[] NOT NULL DEFAULT '{}', -- CORS
|
|
|
|
-- Contact
|
|
contact_name TEXT,
|
|
contact_email CITEXT,
|
|
contact_phone TEXT,
|
|
billing_email CITEXT,
|
|
|
|
-- Limits (overrideable from default plan)
|
|
max_users INT, -- NULL = unlimited
|
|
max_storage_gb INT,
|
|
monthly_render_qty INT,
|
|
monthly_render_sec INT,
|
|
|
|
-- Lifecycle
|
|
trial_ends_at TIMESTAMPTZ,
|
|
suspended_at TIMESTAMPTZ,
|
|
suspension_reason TEXT,
|
|
|
|
-- Metadata
|
|
metadata JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
deleted_at TIMESTAMPTZ
|
|
);
|
|
|
|
CREATE INDEX idx_tenants_status ON tenants(status) WHERE deleted_at IS NULL;
|
|
CREATE INDEX idx_tenants_custom_domain ON tenants(custom_domain) WHERE custom_domain IS NOT NULL;
|
|
|
|
CREATE TRIGGER tg_tenants_updated_at
|
|
BEFORE UPDATE ON tenants
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
-- Seed the default (internal) FlatRender tenant
|
|
INSERT INTO tenants (id, slug, name, kind, status)
|
|
VALUES (
|
|
'00000000-0000-0000-0000-000000000001',
|
|
'flatrender',
|
|
'FlatRender',
|
|
'Internal',
|
|
'Active'
|
|
);
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- tenant_branding — white-label appearance
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE tenant_branding (
|
|
tenant_id UUID PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Identity
|
|
display_name TEXT,
|
|
logo_url TEXT,
|
|
logo_dark_url TEXT,
|
|
favicon_url TEXT,
|
|
og_image_url TEXT,
|
|
|
|
-- Theme
|
|
primary_color TEXT NOT NULL DEFAULT '#3B82F6',
|
|
secondary_color TEXT NOT NULL DEFAULT '#8B5CF6',
|
|
accent_color TEXT,
|
|
background_color TEXT,
|
|
font_family TEXT,
|
|
|
|
-- Email
|
|
email_from_name TEXT,
|
|
email_from_address CITEXT,
|
|
email_reply_to CITEXT,
|
|
email_footer_html TEXT,
|
|
|
|
-- Links
|
|
support_url TEXT,
|
|
terms_url TEXT,
|
|
privacy_url TEXT,
|
|
|
|
-- Studio embed
|
|
embed_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
|
embed_allowed_hosts TEXT[] NOT NULL DEFAULT '{}',
|
|
|
|
-- Custom CSS for advanced clients (sanitized)
|
|
custom_css TEXT,
|
|
|
|
-- Watermark for free renders (reseller can override)
|
|
watermark_text TEXT,
|
|
watermark_image_url TEXT,
|
|
watermark_enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TRIGGER tg_tenant_branding_updated_at
|
|
BEFORE UPDATE ON tenant_branding
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- tenant_settings — feature flags + integration config
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE tenant_settings (
|
|
tenant_id UUID PRIMARY KEY REFERENCES tenants(id) ON DELETE CASCADE,
|
|
|
|
-- Locale
|
|
default_locale TEXT NOT NULL DEFAULT 'fa',
|
|
supported_locales TEXT[] NOT NULL DEFAULT '{fa,en}',
|
|
default_currency TEXT NOT NULL DEFAULT 'IRR',
|
|
|
|
-- Payments allowed for this tenant's users
|
|
payment_gateways TEXT[] NOT NULL DEFAULT '{ZarinPal}', -- {ZarinPal,IdPay,Bazaar,Stripe}
|
|
|
|
-- Render
|
|
max_resolution TEXT NOT NULL DEFAULT 'FullHD', -- HD/FullHD/FourK
|
|
max_duration_sec INT NOT NULL DEFAULT 600,
|
|
allow_4k BOOLEAN NOT NULL DEFAULT FALSE,
|
|
allow_voiceover BOOLEAN NOT NULL DEFAULT TRUE,
|
|
allow_music_visualizer BOOLEAN NOT NULL DEFAULT TRUE,
|
|
allow_mockup BOOLEAN NOT NULL DEFAULT TRUE,
|
|
|
|
-- Webhooks
|
|
webhook_signing_secret TEXT,
|
|
|
|
-- Feature flags
|
|
features JSONB NOT NULL DEFAULT '{}'::jsonb,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE TRIGGER tg_tenant_settings_updated_at
|
|
BEFORE UPDATE ON tenant_settings
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- tenant_api_keys — server-to-server API access
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TYPE api_key_environment AS ENUM ('Live','Test');
|
|
|
|
CREATE TABLE tenant_api_keys (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
|
|
name TEXT NOT NULL, -- "Production server"
|
|
environment api_key_environment NOT NULL DEFAULT 'Live',
|
|
|
|
-- Storage
|
|
key_prefix TEXT NOT NULL, -- "fr_live_abc..." (first 12 chars visible)
|
|
key_hash TEXT NOT NULL, -- SHA-256 of full key
|
|
last4 TEXT NOT NULL, -- last 4 chars for UI
|
|
|
|
-- Permissions
|
|
scopes TEXT[] NOT NULL DEFAULT '{}', -- ["renders:create","projects:read",...]
|
|
allowed_ips INET[], -- NULL = any
|
|
rate_limit_rpm INT NOT NULL DEFAULT 600,
|
|
|
|
-- Lifecycle
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
expires_at TIMESTAMPTZ,
|
|
revoked_at TIMESTAMPTZ,
|
|
revoked_reason TEXT,
|
|
last_used_at TIMESTAMPTZ,
|
|
last_used_ip INET,
|
|
usage_count BIGINT NOT NULL DEFAULT 0,
|
|
|
|
created_by_user_id UUID, -- FK added after users table exists
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE UNIQUE INDEX uq_api_keys_hash ON tenant_api_keys(key_hash);
|
|
CREATE INDEX idx_api_keys_tenant ON tenant_api_keys(tenant_id) WHERE is_active = TRUE;
|
|
CREATE INDEX idx_api_keys_prefix ON tenant_api_keys(key_prefix);
|
|
|
|
CREATE TRIGGER tg_tenant_api_keys_updated_at
|
|
BEFORE UPDATE ON tenant_api_keys
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- tenant_webhooks — outbound events to reseller systems
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE tenant_webhooks (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
|
|
name TEXT NOT NULL,
|
|
url TEXT NOT NULL,
|
|
events TEXT[] NOT NULL, -- ["render.completed","render.failed",...]
|
|
secret TEXT NOT NULL, -- HMAC signing secret
|
|
|
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
|
last_triggered_at TIMESTAMPTZ,
|
|
last_status_code INT,
|
|
consecutive_failures INT NOT NULL DEFAULT 0,
|
|
disabled_at TIMESTAMPTZ, -- auto-disable after N failures
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_webhooks_tenant_active ON tenant_webhooks(tenant_id) WHERE is_active = TRUE;
|
|
|
|
CREATE TRIGGER tg_tenant_webhooks_updated_at
|
|
BEFORE UPDATE ON tenant_webhooks
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- tenant_webhook_deliveries — log of attempts
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE tenant_webhook_deliveries (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
webhook_id UUID NOT NULL REFERENCES tenant_webhooks(id) ON DELETE CASCADE,
|
|
tenant_id UUID NOT NULL, -- denormalized for partitioning
|
|
|
|
event_type TEXT NOT NULL,
|
|
event_id UUID NOT NULL,
|
|
payload JSONB NOT NULL,
|
|
|
|
request_url TEXT NOT NULL,
|
|
response_status INT,
|
|
response_body TEXT,
|
|
duration_ms INT,
|
|
|
|
attempt INT NOT NULL DEFAULT 1,
|
|
succeeded BOOLEAN NOT NULL DEFAULT FALSE,
|
|
next_retry_at TIMESTAMPTZ,
|
|
error_message TEXT,
|
|
|
|
delivered_at TIMESTAMPTZ,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_webhook_deliveries_webhook ON tenant_webhook_deliveries(webhook_id, created_at DESC);
|
|
CREATE INDEX idx_webhook_deliveries_pending ON tenant_webhook_deliveries(next_retry_at) WHERE succeeded = FALSE AND next_retry_at IS NOT NULL;
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- tenant_usage_daily — aggregate metering for billing the reseller
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE tenant_usage_daily (
|
|
id BIGSERIAL PRIMARY KEY,
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
usage_date DATE NOT NULL,
|
|
|
|
-- Render
|
|
renders_started INT NOT NULL DEFAULT 0,
|
|
renders_completed INT NOT NULL DEFAULT 0,
|
|
renders_failed INT NOT NULL DEFAULT 0,
|
|
render_seconds BIGINT NOT NULL DEFAULT 0, -- output duration sum
|
|
render_compute_sec BIGINT NOT NULL DEFAULT 0, -- node compute time
|
|
|
|
-- Storage
|
|
storage_bytes BIGINT NOT NULL DEFAULT 0, -- snapshot at end of day
|
|
|
|
-- API
|
|
api_calls BIGINT NOT NULL DEFAULT 0,
|
|
api_4xx BIGINT NOT NULL DEFAULT 0,
|
|
api_5xx BIGINT NOT NULL DEFAULT 0,
|
|
|
|
-- Users
|
|
active_users INT NOT NULL DEFAULT 0, -- DAU
|
|
new_users INT NOT NULL DEFAULT 0,
|
|
|
|
-- Billing
|
|
amount_billed NUMERIC(14,2) NOT NULL DEFAULT 0,
|
|
billing_currency TEXT NOT NULL DEFAULT 'IRR',
|
|
billing_status TEXT NOT NULL DEFAULT 'Pending', -- Pending/Invoiced/Paid
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
UNIQUE (tenant_id, usage_date)
|
|
);
|
|
|
|
CREATE INDEX idx_tenant_usage_tenant_date ON tenant_usage_daily(tenant_id, usage_date DESC);
|
|
|
|
CREATE TRIGGER tg_tenant_usage_updated_at
|
|
BEFORE UPDATE ON tenant_usage_daily
|
|
FOR EACH ROW EXECUTE FUNCTION public.tg_set_updated_at();
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- tenant_api_request_logs — per-call audit (partitioned by month)
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE tenant_api_request_logs (
|
|
id BIGSERIAL,
|
|
tenant_id UUID NOT NULL,
|
|
api_key_id UUID,
|
|
|
|
method TEXT NOT NULL,
|
|
path TEXT NOT NULL,
|
|
status_code INT NOT NULL,
|
|
duration_ms INT NOT NULL,
|
|
|
|
request_id UUID NOT NULL,
|
|
user_id UUID,
|
|
ip_address INET,
|
|
user_agent TEXT,
|
|
|
|
error_message TEXT,
|
|
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
|
|
PRIMARY KEY (id, created_at)
|
|
) PARTITION BY RANGE (created_at);
|
|
|
|
CREATE INDEX idx_api_logs_tenant ON tenant_api_request_logs(tenant_id, created_at DESC);
|
|
CREATE INDEX idx_api_logs_key ON tenant_api_request_logs(api_key_id, created_at DESC);
|
|
|
|
-- Initial partition (ops creates monthly going forward)
|
|
CREATE TABLE tenant_api_request_logs_y2026m01
|
|
PARTITION OF tenant_api_request_logs
|
|
FOR VALUES FROM ('2026-01-01') TO ('2026-02-01');
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- tenant_domain_verifications — DNS / file verification
|
|
-- ---------------------------------------------------------------------
|
|
CREATE TABLE tenant_domain_verifications (
|
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
tenant_id UUID NOT NULL REFERENCES tenants(id) ON DELETE CASCADE,
|
|
domain CITEXT NOT NULL,
|
|
method TEXT NOT NULL, -- 'DNS_TXT' or 'HTTP_FILE'
|
|
challenge_token TEXT NOT NULL,
|
|
verified BOOLEAN NOT NULL DEFAULT FALSE,
|
|
verified_at TIMESTAMPTZ,
|
|
last_checked_at TIMESTAMPTZ,
|
|
expires_at TIMESTAMPTZ NOT NULL,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
CREATE INDEX idx_domain_verif_tenant ON tenant_domain_verifications(tenant_id);
|