feat: integrate platform backend and application interfaces

This commit is contained in:
Qiufeng
2026-09-02 21:09:47 +08:00
parent cd9ac1af70
commit b16390dd41
648 changed files with 102488 additions and 17737 deletions
+8
View File
@@ -0,0 +1,8 @@
-- V1 baseline. The in-memory adapter is used for local bootstrap; production
-- migrations must keep these ownership, snapshot, and ledger boundaries.
CREATE TABLE IF NOT EXISTS schema_migrations (
version TEXT PRIMARY KEY,
applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO schema_migrations(version) VALUES ('0001_v1_baseline') ON CONFLICT DO NOTHING;
+118
View File
@@ -0,0 +1,118 @@
-- Domain boundary for the first PostgreSQL adapter. Secrets and user files
-- are referenced by encrypted ids; they are never stored in static assets.
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY,
status TEXT NOT NULL,
display_name TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS user_identities (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
kind TEXT NOT NULL,
normalized_value TEXT NOT NULL,
verified_at TIMESTAMPTZ,
UNIQUE(kind, normalized_value)
);
CREATE TABLE IF NOT EXISTS password_credentials (
user_id UUID PRIMARY KEY REFERENCES users(id),
password_hash TEXT NOT NULL,
algorithm_version TEXT NOT NULL,
changed_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS verification_codes (
id UUID PRIMARY KEY,
target_hash TEXT NOT NULL,
purpose TEXT NOT NULL,
code_hash TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
consumed_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY,
subject_id UUID NOT NULL,
scope TEXT NOT NULL,
refresh_hash TEXT NOT NULL UNIQUE,
rotation_id UUID NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS balance_accounts (
user_id UUID PRIMARY KEY REFERENCES users(id),
available NUMERIC(24, 6) NOT NULL DEFAULT 0,
reserved NUMERIC(24, 6) NOT NULL DEFAULT 0,
version BIGINT NOT NULL DEFAULT 0,
CHECK (available >= 0 AND reserved >= 0)
);
CREATE TABLE IF NOT EXISTS balance_ledger (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
type TEXT NOT NULL,
amount NUMERIC(24, 6) NOT NULL,
unit_version INTEGER NOT NULL,
idempotency_key TEXT NOT NULL UNIQUE,
reference_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS model_products (
id TEXT PRIMARY KEY,
public_model_id TEXT NOT NULL UNIQUE,
tier TEXT NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT false,
version INTEGER NOT NULL DEFAULT 1,
snapshot JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS provider_channels (
id TEXT PRIMARY KEY,
status TEXT NOT NULL,
encrypted_secret_ref TEXT NOT NULL,
base_url TEXT NOT NULL,
health JSONB NOT NULL DEFAULT '{}'::jsonb,
version INTEGER NOT NULL DEFAULT 1
);
CREATE TABLE IF NOT EXISTS channel_groups (
id TEXT PRIMARY KEY,
version INTEGER NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT false,
channel_order JSONB NOT NULL
);
CREATE TABLE IF NOT EXISTS generation_tasks (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
task_type TEXT NOT NULL,
status TEXT NOT NULL,
idempotency_key TEXT NOT NULL,
model_snapshot JSONB NOT NULL,
route_snapshot JSONB NOT NULL,
pricing_snapshot JSONB NOT NULL,
reserved_amount NUMERIC(24, 6) NOT NULL DEFAULT 0,
state_version BIGINT NOT NULL DEFAULT 0,
lease_expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id, idempotency_key)
);
CREATE TABLE IF NOT EXISTS generation_attempts (
id UUID PRIMARY KEY,
task_id UUID NOT NULL REFERENCES generation_tasks(id),
channel_id TEXT NOT NULL,
sequence INTEGER NOT NULL,
status TEXT NOT NULL,
provider_request_id TEXT,
error_code TEXT,
started_at TIMESTAMPTZ NOT NULL,
finished_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS audit_logs (
id UUID PRIMARY KEY,
actor_id UUID,
action TEXT NOT NULL,
object_type TEXT NOT NULL,
object_id TEXT,
request_id TEXT NOT NULL,
payload_digest TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
+3
View File
@@ -0,0 +1,3 @@
CREATE INDEX IF NOT EXISTS generation_tasks_user_created_idx ON generation_tasks(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS generation_attempts_task_sequence_idx ON generation_attempts(task_id, sequence);
CREATE INDEX IF NOT EXISTS audit_logs_created_idx ON audit_logs(created_at DESC);
+166
View File
@@ -0,0 +1,166 @@
-- Extended V1 domain tables. Secrets are referenced by encrypted_ref only;
-- user files live in staging/object adapters and never in static directories.
CREATE TABLE IF NOT EXISTS balance_units (
id TEXT PRIMARY KEY,
version INTEGER NOT NULL,
name TEXT NOT NULL,
short_code TEXT NOT NULL,
icon TEXT NOT NULL,
precision INTEGER NOT NULL DEFAULT 0,
currency_code TEXT NOT NULL,
conversion_rate NUMERIC(24, 8) NOT NULL,
rounding TEXT NOT NULL,
published_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS balance_buckets (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
source TEXT NOT NULL,
remaining NUMERIC(24, 6) NOT NULL CHECK (remaining >= 0),
priority INTEGER NOT NULL DEFAULT 0,
expires_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS membership_plans (
id TEXT PRIMARY KEY,
code TEXT NOT NULL UNIQUE,
version INTEGER NOT NULL DEFAULT 1,
snapshot JSONB NOT NULL,
published BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE IF NOT EXISTS user_plan_entitlements (
user_id UUID NOT NULL REFERENCES users(id),
plan_id TEXT NOT NULL REFERENCES membership_plans(id),
version INTEGER NOT NULL,
starts_at TIMESTAMPTZ NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (user_id, plan_id, version)
);
CREATE TABLE IF NOT EXISTS recharge_orders (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
provider TEXT NOT NULL,
status TEXT NOT NULL,
fiat_amount NUMERIC(24, 6) NOT NULL,
balance_amount NUMERIC(24, 6) NOT NULL,
unit_version INTEGER NOT NULL,
idempotency_key TEXT NOT NULL UNIQUE,
provider_order_id TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
paid_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS payment_events (
id UUID PRIMARY KEY,
provider TEXT NOT NULL,
provider_event_id TEXT NOT NULL,
order_id UUID REFERENCES recharge_orders(id),
payload_digest TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(provider, provider_event_id)
);
CREATE TABLE IF NOT EXISTS generation_outputs (
id UUID PRIMARY KEY,
task_id UUID NOT NULL REFERENCES generation_tasks(id),
owner_id UUID NOT NULL REFERENCES users(id),
object_key TEXT NOT NULL,
mime_type TEXT NOT NULL,
status TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
revoked_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS upload_objects (
id UUID PRIMARY KEY,
owner_id UUID NOT NULL REFERENCES users(id),
object_key TEXT NOT NULL,
mime_type TEXT NOT NULL,
byte_size BIGINT NOT NULL,
checksum TEXT NOT NULL,
scan_status TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
deleted_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS assets (
id UUID PRIMARY KEY,
owner_id UUID NOT NULL REFERENCES users(id),
task_id UUID REFERENCES generation_tasks(id),
object_id UUID REFERENCES upload_objects(id),
asset_type TEXT NOT NULL,
snapshot JSONB NOT NULL,
deleted_at TIMESTAMPTZ,
retention_expires_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS email_sms_outbox (
id UUID PRIMARY KEY,
target_hash TEXT NOT NULL,
channel TEXT NOT NULL,
purpose TEXT NOT NULL,
payload JSONB NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
attempts INTEGER NOT NULL DEFAULT 0,
next_attempt_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS webdav_configs (
user_id UUID PRIMARY KEY REFERENCES users(id),
encrypted_url_ref TEXT NOT NULL,
encrypted_username_ref TEXT,
encrypted_password_ref TEXT,
directory TEXT NOT NULL,
state TEXT NOT NULL,
last_synced_at TIMESTAMPTZ,
retention_days INTEGER NOT NULL DEFAULT 30
);
CREATE TABLE IF NOT EXISTS webdav_manifests (
id UUID PRIMARY KEY,
user_id UUID NOT NULL REFERENCES users(id),
version BIGINT NOT NULL,
checksum TEXT NOT NULL,
manifest JSONB NOT NULL,
state TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(user_id, version)
);
CREATE TABLE IF NOT EXISTS storage_policies (
id TEXT PRIMARY KEY,
version INTEGER NOT NULL,
snapshot JSONB NOT NULL,
published BOOLEAN NOT NULL DEFAULT false
);
CREATE TABLE IF NOT EXISTS admin_roles (
id TEXT PRIMARY KEY,
permissions JSONB NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT true
);
CREATE TABLE IF NOT EXISTS admin_user_roles (
admin_id UUID NOT NULL,
role_id TEXT NOT NULL REFERENCES admin_roles(id),
PRIMARY KEY (admin_id, role_id)
);
CREATE TABLE IF NOT EXISTS admin_approvals (
id UUID PRIMARY KEY,
requester_id UUID NOT NULL,
approver_id UUID,
action TEXT NOT NULL,
object_type TEXT NOT NULL,
object_id TEXT,
status TEXT NOT NULL DEFAULT 'pending',
before_value JSONB,
after_value JSONB,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
approved_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS task_event_log (
task_id UUID NOT NULL REFERENCES generation_tasks(id),
sequence BIGINT NOT NULL,
event_id UUID NOT NULL UNIQUE,
event_type TEXT NOT NULL,
payload JSONB NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (task_id, sequence)
);
CREATE INDEX IF NOT EXISTS upload_objects_owner_idx ON upload_objects(owner_id, expires_at);
CREATE INDEX IF NOT EXISTS generation_outputs_owner_idx ON generation_outputs(owner_id, expires_at);
CREATE INDEX IF NOT EXISTS recharge_orders_user_created_idx ON recharge_orders(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS task_event_log_task_sequence_idx ON task_event_log(task_id, sequence);
@@ -0,0 +1,56 @@
-- V1 runtime extension tables. The local fixture adapter mirrors these records
-- in Store maps; production repositories must make these tables authoritative.
-- payment_events is created by 0004 and reconciled by 0006. Keep this
-- migration additive so fresh and upgraded databases share one schema.
ALTER TABLE IF EXISTS payment_events ADD COLUMN IF NOT EXISTS external_event_id TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS payment_events_provider_external_idx_v1 ON payment_events(provider, external_event_id) WHERE external_event_id IS NOT NULL;
CREATE TABLE IF NOT EXISTS admin_idempotency (
actor_id TEXT NOT NULL,
route TEXT NOT NULL,
idem_key TEXT NOT NULL,
request_fingerprint TEXT NOT NULL,
state TEXT NOT NULL,
status_code INTEGER,
response JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (actor_id, route, idem_key)
);
CREATE TABLE IF NOT EXISTS admin_approval_requests (
id UUID PRIMARY KEY,
kind TEXT NOT NULL,
requester_id TEXT NOT NULL,
approver_id TEXT,
status TEXT NOT NULL,
payload JSONB NOT NULL,
reason TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
approved_at TIMESTAMPTZ,
executed_at TIMESTAMPTZ,
CHECK (approver_id IS NULL OR approver_id <> requester_id)
);
CREATE TABLE IF NOT EXISTS plan_purchases (
id UUID PRIMARY KEY,
user_id UUID NOT NULL,
plan_id UUID NOT NULL,
plan_version INTEGER NOT NULL,
status TEXT NOT NULL,
purchase_amount BIGINT NOT NULL,
included_balance BIGINT NOT NULL,
idempotency_key TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
paid_at TIMESTAMPTZ,
UNIQUE (user_id, idempotency_key)
);
CREATE TABLE IF NOT EXISTS reserve_allocations (
user_id UUID NOT NULL,
reference_id UUID NOT NULL,
bucket_id UUID NOT NULL,
amount BIGINT NOT NULL,
PRIMARY KEY (user_id, reference_id, bucket_id)
);
@@ -0,0 +1,18 @@
-- Reconcile the early domain migration with the runtime contract. Existing
-- installations keep their data; the compatibility columns are populated by
-- the payment adapter and the canonical identifier remains provider/event.
ALTER TABLE IF EXISTS payment_events ADD COLUMN IF NOT EXISTS external_event_id TEXT;
ALTER TABLE IF EXISTS payment_events ADD COLUMN IF NOT EXISTS provider_event_id TEXT;
UPDATE payment_events SET external_event_id = COALESCE(external_event_id, provider_event_id) WHERE external_event_id IS NULL;
UPDATE payment_events SET provider_event_id = COALESCE(provider_event_id, external_event_id) WHERE provider_event_id IS NULL;
CREATE UNIQUE INDEX IF NOT EXISTS payment_events_provider_external_idx ON payment_events(provider, external_event_id) WHERE external_event_id IS NOT NULL;
ALTER TABLE IF EXISTS plan_purchases ALTER COLUMN plan_id TYPE TEXT USING plan_id::text;
ALTER TABLE IF EXISTS plan_purchases ALTER COLUMN purchase_amount TYPE BIGINT USING purchase_amount::bigint;
ALTER TABLE IF EXISTS plan_purchases ALTER COLUMN included_balance TYPE BIGINT USING included_balance::bigint;
CREATE UNIQUE INDEX IF NOT EXISTS plan_purchases_user_idempotency_idx ON plan_purchases(user_id, idempotency_key);
ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS absolute_expires_at TIMESTAMPTZ;
ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS family_id UUID;
ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS device_id TEXT;
UPDATE sessions SET absolute_expires_at = COALESCE(absolute_expires_at, expires_at) WHERE absolute_expires_at IS NULL;
@@ -0,0 +1,13 @@
-- Runtime repository projection. Each root is a domain collection and each
-- row is independently addressable, so PostgreSQL can be promoted from the
-- legacy snapshot without exposing a JSON blob as the only source of truth.
CREATE TABLE IF NOT EXISTS miragenflow_domain_rows (
root_key TEXT NOT NULL,
entity_id TEXT NOT NULL,
shape TEXT NOT NULL CHECK (shape IN ('map', 'array', 'scalar')),
payload JSONB,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (root_key, entity_id)
);
CREATE INDEX IF NOT EXISTS miragenflow_domain_rows_updated_idx
ON miragenflow_domain_rows(updated_at DESC);
@@ -0,0 +1,143 @@
-- Plan purchases use a separate payment-event table because the recharge
-- payment_events.order_id foreign key intentionally points at recharge_orders.
ALTER TABLE IF EXISTS plan_purchases ADD COLUMN IF NOT EXISTS provider TEXT NOT NULL DEFAULT 'adapter';
ALTER TABLE IF EXISTS plan_purchases ADD COLUMN IF NOT EXISTS plan_snapshot JSONB;
ALTER TABLE IF EXISTS plan_purchases ADD COLUMN IF NOT EXISTS credit_bucket_id UUID;
ALTER TABLE IF EXISTS plan_purchases ADD COLUMN IF NOT EXISTS entitlement_id UUID;
ALTER TABLE IF EXISTS recharge_orders ADD COLUMN IF NOT EXISTS credit_bucket_id UUID;
ALTER TABLE IF EXISTS user_plan_entitlements ADD COLUMN IF NOT EXISTS snapshot JSONB;
CREATE TABLE IF NOT EXISTS plan_payment_events (
id UUID PRIMARY KEY,
provider TEXT NOT NULL,
external_event_id TEXT NOT NULL,
purchase_id UUID NOT NULL REFERENCES plan_purchases(id),
payload_digest TEXT NOT NULL,
status TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
applied_at TIMESTAMPTZ,
UNIQUE(provider, external_event_id)
);
-- Runtime records that are part of the server's durable state but are not
-- user-facing business aggregates. They keep authentication challenges,
-- provider configuration and staging metadata relationally addressable while
-- preserving encrypted payloads and ownership boundaries.
ALTER TABLE IF EXISTS verification_codes ADD COLUMN IF NOT EXISTS record_key TEXT;
ALTER TABLE IF EXISTS verification_codes ADD COLUMN IF NOT EXISTS record_kind TEXT NOT NULL DEFAULT 'verification';
ALTER TABLE IF EXISTS verification_codes ADD COLUMN IF NOT EXISTS ip TEXT;
ALTER TABLE IF EXISTS verification_codes ADD COLUMN IF NOT EXISTS device_id TEXT;
ALTER TABLE IF EXISTS verification_codes ADD COLUMN IF NOT EXISTS max_attempts INTEGER NOT NULL DEFAULT 5;
CREATE INDEX IF NOT EXISTS verification_codes_record_key_idx ON verification_codes(record_kind, record_key);
ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS mfa_secret_encrypted TEXT;
ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS mfa_recovery_code_hashes JSONB;
ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS mfa_last_totp_counter BIGINT;
ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS mfa_required BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS mfa_enabled BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS roles JSONB NOT NULL DEFAULT '[]'::jsonb;
ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS failed_login_count INTEGER NOT NULL DEFAULT 0;
ALTER TABLE IF EXISTS users ADD COLUMN IF NOT EXISTS locked_until BIGINT;
ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS replaced_by TEXT;
ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS roles JSONB;
ALTER TABLE IF EXISTS sessions ADD COLUMN IF NOT EXISTS scopes JSONB;
CREATE TABLE IF NOT EXISTS captcha_challenges (
id TEXT PRIMARY KEY,
action TEXT NOT NULL,
answer_hash TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
max_attempts INTEGER NOT NULL DEFAULT 5,
consumed_at TIMESTAMPTZ,
ip TEXT NOT NULL,
device_id TEXT
);
CREATE TABLE IF NOT EXISTS mfa_challenges (
id TEXT PRIMARY KEY,
subject_id TEXT NOT NULL,
scope TEXT NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0
);
CREATE TABLE IF NOT EXISTS session_families (
family_id TEXT PRIMARY KEY,
subject_id TEXT NOT NULL,
scope TEXT NOT NULL,
revoked_at TIMESTAMPTZ
);
CREATE TABLE IF NOT EXISTS admin_accounts (
id TEXT PRIMARY KEY,
email TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
role TEXT NOT NULL,
mfa_secret_encrypted TEXT,
mfa_required BOOLEAN NOT NULL DEFAULT true,
recovery_code_hashes JSONB NOT NULL DEFAULT '[]'::jsonb,
mfa_last_totp_counter BIGINT
);
CREATE TABLE IF NOT EXISTS runtime_records (
record_type TEXT NOT NULL,
record_key TEXT NOT NULL,
payload JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (record_type, record_key)
);
CREATE TABLE IF NOT EXISTS message_providers (
id TEXT PRIMARY KEY,
snapshot JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS message_templates (
id TEXT PRIMARY KEY,
snapshot JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS payment_providers (
id TEXT PRIMARY KEY,
snapshot JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS webdav_files (
user_id TEXT NOT NULL,
path TEXT NOT NULL,
mime_type TEXT NOT NULL,
data TEXT NOT NULL DEFAULT '',
checksum TEXT NOT NULL,
etag TEXT NOT NULL,
version INTEGER NOT NULL DEFAULT 1,
updated_at TIMESTAMPTZ NOT NULL,
deleted_at TIMESTAMPTZ,
PRIMARY KEY (user_id, path)
);
CREATE TABLE IF NOT EXISTS webdav_sync_jobs (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
operation TEXT NOT NULL,
path TEXT NOT NULL,
data TEXT,
mime_type TEXT,
if_match TEXT,
attempts INTEGER NOT NULL DEFAULT 0,
status TEXT NOT NULL,
next_attempt_at TIMESTAMPTZ NOT NULL,
lease_expires_at TIMESTAMPTZ,
last_error TEXT,
created_at TIMESTAMPTZ NOT NULL
);
CREATE TABLE IF NOT EXISTS stored_objects (
id TEXT PRIMARY KEY,
owner_id TEXT NOT NULL,
mime_type TEXT NOT NULL,
data TEXT,
staging_key TEXT,
expires_at TIMESTAMPTZ NOT NULL,
task_id TEXT,
revoked BOOLEAN NOT NULL DEFAULT false
);
CREATE INDEX IF NOT EXISTS webdav_files_user_idx ON webdav_files(user_id, updated_at DESC);
CREATE INDEX IF NOT EXISTS webdav_sync_jobs_user_idx ON webdav_sync_jobs(user_id, status, next_attempt_at);
CREATE INDEX IF NOT EXISTS stored_objects_owner_idx ON stored_objects(owner_id, expires_at);
ALTER TABLE IF EXISTS webdav_configs ADD COLUMN IF NOT EXISTS manifest_retention_expires_at TIMESTAMPTZ;
ALTER TABLE IF EXISTS webdav_configs ADD COLUMN IF NOT EXISTS manifest_extension_days INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1 @@
ALTER TABLE captcha_challenges ADD COLUMN IF NOT EXISTS target_hash TEXT NOT NULL DEFAULT '';
@@ -0,0 +1,40 @@
CREATE TABLE IF NOT EXISTS miragenflow_store_snapshots (
snapshot_key TEXT PRIMARY KEY,
payload JSONB NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS miragenflow_domain_rows (
root_key TEXT NOT NULL,
entity_id TEXT NOT NULL,
shape TEXT NOT NULL,
payload JSONB,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
PRIMARY KEY (root_key, entity_id)
);
CREATE TABLE IF NOT EXISTS miragenflow_store_revisions (
snapshot_key TEXT PRIMARY KEY,
revision BIGINT NOT NULL DEFAULT 0,
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
ALTER TABLE webdav_files ADD COLUMN IF NOT EXISTS sync_state TEXT NOT NULL DEFAULT 'synced';
ALTER TABLE webdav_sync_jobs ADD COLUMN IF NOT EXISTS intent TEXT;
ALTER TABLE webdav_sync_jobs ADD COLUMN IF NOT EXISTS conflict_copy_path TEXT;
ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS event_sequence BIGINT NOT NULL DEFAULT 0;
ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS reserve_expires_at TIMESTAMPTZ;
ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now();
ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS lease_token TEXT;
ALTER TABLE generation_attempts ADD COLUMN IF NOT EXISTS platform_idempotency_key TEXT;
ALTER TABLE generation_attempts ADD COLUMN IF NOT EXISTS reconciliation_status TEXT;
ALTER TABLE generation_attempts ADD COLUMN IF NOT EXISTS lease_expires_at TIMESTAMPTZ;
ALTER TABLE generation_outputs ADD COLUMN IF NOT EXISTS unit_price_snapshot BIGINT;
ALTER TABLE generation_outputs ADD COLUMN IF NOT EXISTS charged_amount BIGINT;
ALTER TABLE balance_ledger ADD COLUMN IF NOT EXISTS task_id UUID;
ALTER TABLE balance_ledger ADD COLUMN IF NOT EXISTS output_id UUID;
ALTER TABLE balance_ledger ADD COLUMN IF NOT EXISTS bucket_id UUID;
ALTER TABLE balance_ledger ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;
ALTER TABLE balance_ledger ADD COLUMN IF NOT EXISTS status TEXT NOT NULL DEFAULT 'posted';
CREATE UNIQUE INDEX IF NOT EXISTS generation_attempts_task_sequence_uq ON generation_attempts(task_id, sequence);
CREATE UNIQUE INDEX IF NOT EXISTS generation_attempts_platform_idem_uq ON generation_attempts(platform_idempotency_key) WHERE platform_idempotency_key IS NOT NULL;
@@ -0,0 +1,2 @@
ALTER TABLE generation_outputs
ADD COLUMN IF NOT EXISTS metadata JSONB;
@@ -0,0 +1,9 @@
ALTER TABLE webdav_configs
ADD COLUMN IF NOT EXISTS retention_state TEXT NOT NULL DEFAULT 'active';
ALTER TABLE webdav_configs
DROP CONSTRAINT IF EXISTS webdav_configs_retention_state_check;
ALTER TABLE webdav_configs
ADD CONSTRAINT webdav_configs_retention_state_check
CHECK (retention_state IN ('active', 'deleting', 'deleted', 'error'));
@@ -0,0 +1,26 @@
-- Nullable organization/team scope columns are schema reservations only. V1
-- keeps every object user-scoped and exposes no organization API.
ALTER TABLE balance_accounts ADD COLUMN IF NOT EXISTS organization_id UUID;
ALTER TABLE balance_accounts ADD COLUMN IF NOT EXISTS object_scope TEXT NOT NULL DEFAULT 'user';
ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS organization_id UUID;
ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS workspace_owner_id UUID;
ALTER TABLE generation_tasks ADD COLUMN IF NOT EXISTS object_scope TEXT NOT NULL DEFAULT 'user';
ALTER TABLE upload_objects ADD COLUMN IF NOT EXISTS organization_id UUID;
ALTER TABLE upload_objects ADD COLUMN IF NOT EXISTS workspace_owner_id UUID;
ALTER TABLE upload_objects ADD COLUMN IF NOT EXISTS object_scope TEXT NOT NULL DEFAULT 'user';
ALTER TABLE assets ADD COLUMN IF NOT EXISTS organization_id UUID;
ALTER TABLE assets ADD COLUMN IF NOT EXISTS workspace_owner_id UUID;
ALTER TABLE assets ADD COLUMN IF NOT EXISTS object_scope TEXT NOT NULL DEFAULT 'user';
ALTER TABLE stored_objects ADD COLUMN IF NOT EXISTS organization_id TEXT;
ALTER TABLE stored_objects ADD COLUMN IF NOT EXISTS workspace_owner_id TEXT;
ALTER TABLE stored_objects ADD COLUMN IF NOT EXISTS object_scope TEXT NOT NULL DEFAULT 'user';
DO $$
DECLARE
table_name TEXT;
BEGIN
FOR table_name IN SELECT unnest(ARRAY['balance_accounts', 'generation_tasks', 'upload_objects', 'assets', 'stored_objects']) LOOP
EXECUTE format('ALTER TABLE %I DROP CONSTRAINT IF EXISTS %I_object_scope_check', table_name, table_name);
EXECUTE format('ALTER TABLE %I ADD CONSTRAINT %I_object_scope_check CHECK (object_scope IN (''user'', ''organization''))', table_name, table_name);
END LOOP;
END $$;
@@ -0,0 +1,4 @@
ALTER TABLE upload_objects ADD COLUMN IF NOT EXISTS idempotency_key TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS upload_objects_owner_idem_uq
ON upload_objects(owner_id, idempotency_key)
WHERE idempotency_key IS NOT NULL;
@@ -0,0 +1,18 @@
ALTER TABLE generation_tasks
ADD COLUMN IF NOT EXISTS task_snapshot JSONB NOT NULL DEFAULT '{}'::jsonb;
CREATE TABLE IF NOT EXISTS task_dispatch_outbox (
task_id UUID PRIMARY KEY REFERENCES generation_tasks(id) ON DELETE CASCADE,
status TEXT NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending', 'leased', 'sent', 'failed')),
available_at TIMESTAMPTZ NOT NULL DEFAULT now(),
attempts INTEGER NOT NULL DEFAULT 0,
lease_owner TEXT,
lease_expires_at TIMESTAMPTZ,
last_error TEXT,
dispatched_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS task_dispatch_outbox_poll_idx
ON task_dispatch_outbox(status, available_at);
@@ -0,0 +1,6 @@
-- Preserve payment/order request fingerprints across PostgreSQL reloads so
-- idempotency checks cannot fall back to a lossy amount/plan comparison.
ALTER TABLE IF EXISTS recharge_orders
ADD COLUMN IF NOT EXISTS request_fingerprint TEXT;
ALTER TABLE IF EXISTS plan_purchases
ADD COLUMN IF NOT EXISTS request_fingerprint TEXT;
@@ -0,0 +1,9 @@
-- Preserve message outbox lease and provider result metadata across restarts.
ALTER TABLE IF EXISTS email_sms_outbox
ADD COLUMN IF NOT EXISTS lease_owner TEXT,
ADD COLUMN IF NOT EXISTS lease_expires_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS provider_message_id TEXT,
ADD COLUMN IF NOT EXISTS last_error TEXT,
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT now();
CREATE INDEX IF NOT EXISTS email_sms_outbox_poll_idx
ON email_sms_outbox(status, next_attempt_at);
@@ -0,0 +1,5 @@
ALTER TABLE webdav_sync_jobs
ADD COLUMN IF NOT EXISTS lease_owner TEXT;
CREATE INDEX IF NOT EXISTS webdav_sync_jobs_claim_idx
ON webdav_sync_jobs(status, next_attempt_at, lease_expires_at, created_at);
@@ -0,0 +1,6 @@
CREATE INDEX IF NOT EXISTS webdav_retention_scan_idx
ON webdav_configs(manifest_retention_expires_at, retention_state)
WHERE manifest_retention_expires_at IS NOT NULL;
CREATE INDEX IF NOT EXISTS webdav_retention_job_dedupe_idx
ON webdav_sync_jobs(user_id, intent, path, status);
@@ -0,0 +1,6 @@
ALTER TABLE IF EXISTS email_sms_outbox
ADD COLUMN IF NOT EXISTS idempotency_key TEXT;
CREATE UNIQUE INDEX IF NOT EXISTS email_sms_outbox_idempotency_idx
ON email_sms_outbox(idempotency_key)
WHERE idempotency_key IS NOT NULL;
@@ -0,0 +1,5 @@
ALTER TABLE IF EXISTS recharge_orders
DROP CONSTRAINT IF EXISTS recharge_orders_idempotency_key_key;
CREATE UNIQUE INDEX IF NOT EXISTS recharge_orders_user_idempotency_idx
ON recharge_orders(user_id, idempotency_key);
@@ -0,0 +1,3 @@
ALTER TABLE admin_idempotency
ADD COLUMN IF NOT EXISTS claim_token TEXT,
ADD COLUMN IF NOT EXISTS lease_expires_at TIMESTAMPTZ;
@@ -0,0 +1,4 @@
ALTER TABLE IF EXISTS mfa_challenges
ADD COLUMN IF NOT EXISTS purpose TEXT NOT NULL DEFAULT 'login',
ADD COLUMN IF NOT EXISTS pending_secret_encrypted TEXT,
ADD COLUMN IF NOT EXISTS pending_recovery_code_hashes JSONB NOT NULL DEFAULT '[]'::jsonb;