feat: V2 microservices stack — backend services, gateway, JWT auth
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>
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
-- =====================================================================
|
||||
-- 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);
|
||||
Reference in New Issue
Block a user