feat: integrate platform backend and application interfaces
This commit is contained in:
@@ -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;
|
||||
@@ -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()
|
||||
);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user