feat: add workflow studio and postgres support

This commit is contained in:
boos
2026-05-25 16:03:07 +08:00
parent 7cdb02ef1d
commit cb35d5fc6e
96 changed files with 12414 additions and 15548 deletions
+868
View File
@@ -0,0 +1,868 @@
-- BoosAPI initial schema
-- Generated from the existing SQLite database (hub.db)
-- This migration creates all tables, indexes, and constraints.
-- ============================================================
-- SITES
-- ============================================================
CREATE TABLE IF NOT EXISTS sites (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
url TEXT NOT NULL,
platform TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'active',
api_key TEXT,
proxy_url TEXT,
use_system_proxy BOOLEAN DEFAULT FALSE,
custom_headers TEXT,
external_checkin_url TEXT,
global_weight DOUBLE PRECISION DEFAULT 1,
post_refresh_probe_enabled BOOLEAN DEFAULT FALSE,
post_refresh_probe_model TEXT DEFAULT '',
post_refresh_probe_scope TEXT DEFAULT 'single',
post_refresh_probe_latency_threshold_ms BIGINT DEFAULT 0,
is_pinned BOOLEAN DEFAULT FALSE,
sort_order BIGINT DEFAULT 0,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE UNIQUE INDEX IF NOT EXISTS sites_platform_url_unique
ON sites (platform, url);
CREATE INDEX IF NOT EXISTS sites_status_idx
ON sites (status);
-- ============================================================
-- USERS
-- ============================================================
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
username TEXT NOT NULL,
email TEXT NOT NULL,
password_hash TEXT NOT NULL,
role TEXT NOT NULL DEFAULT 'user',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE UNIQUE INDEX IF NOT EXISTS users_email_unique
ON users (email);
CREATE INDEX IF NOT EXISTS users_status_idx
ON users (status);
-- ============================================================
-- TOKEN ROUTES
-- ============================================================
CREATE TABLE IF NOT EXISTS token_routes (
id BIGSERIAL PRIMARY KEY,
model_pattern TEXT NOT NULL,
model_mapping TEXT,
display_name TEXT,
display_icon TEXT,
decision_snapshot TEXT,
decision_refreshed_at TEXT,
routing_strategy TEXT DEFAULT 'weighted',
route_mode TEXT DEFAULT 'pattern',
enabled BOOLEAN DEFAULT TRUE,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE INDEX IF NOT EXISTS token_routes_model_pattern_idx
ON token_routes (model_pattern);
CREATE INDEX IF NOT EXISTS token_routes_enabled_idx
ON token_routes (enabled);
-- ============================================================
-- ACCOUNTS
-- ============================================================
CREATE TABLE IF NOT EXISTS accounts (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
username TEXT,
access_token TEXT NOT NULL,
api_token TEXT,
balance DOUBLE PRECISION DEFAULT 0,
balance_used DOUBLE PRECISION DEFAULT 0,
quota DOUBLE PRECISION DEFAULT 0,
unit_cost DOUBLE PRECISION,
value_score DOUBLE PRECISION DEFAULT 0,
status TEXT DEFAULT 'active',
checkin_enabled BOOLEAN DEFAULT TRUE,
last_checkin_at TEXT,
last_balance_refresh TEXT,
extra_config TEXT,
is_pinned BOOLEAN DEFAULT FALSE,
sort_order BIGINT DEFAULT 0,
oauth_provider TEXT,
oauth_account_key TEXT,
oauth_project_id TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS accounts_site_id_idx
ON accounts (site_id);
CREATE INDEX IF NOT EXISTS accounts_status_idx
ON accounts (status);
CREATE INDEX IF NOT EXISTS accounts_site_status_idx
ON accounts (site_id, status);
CREATE INDEX IF NOT EXISTS accounts_oauth_provider_idx
ON accounts (oauth_provider);
CREATE INDEX IF NOT EXISTS accounts_oauth_identity_idx
ON accounts (oauth_provider, oauth_account_key, oauth_project_id);
-- ============================================================
-- DOWNSTREAM API KEYS
-- ============================================================
CREATE TABLE IF NOT EXISTS downstream_api_keys (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
key TEXT NOT NULL,
description TEXT,
group_name TEXT,
tags TEXT,
enabled BOOLEAN DEFAULT TRUE,
expires_at TEXT,
max_cost DOUBLE PRECISION,
used_cost DOUBLE PRECISION DEFAULT 0,
max_requests BIGINT,
used_requests BIGINT DEFAULT 0,
supported_models TEXT,
allowed_route_ids TEXT,
site_weight_multipliers TEXT,
excluded_site_ids TEXT,
excluded_credential_refs TEXT,
last_used_at TEXT,
user_id BIGINT REFERENCES users(id),
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE UNIQUE INDEX IF NOT EXISTS downstream_api_keys_key_unique
ON downstream_api_keys (key);
CREATE INDEX IF NOT EXISTS downstream_api_keys_name_idx
ON downstream_api_keys (name);
CREATE INDEX IF NOT EXISTS downstream_api_keys_enabled_idx
ON downstream_api_keys (enabled);
CREATE INDEX IF NOT EXISTS downstream_api_keys_expires_at_idx
ON downstream_api_keys (expires_at);
-- ============================================================
-- SITE DISABLED MODELS
-- ============================================================
CREATE TABLE IF NOT EXISTS site_disabled_models (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
model_name TEXT NOT NULL,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS site_disabled_models_site_model_unique
ON site_disabled_models (site_id, model_name);
CREATE INDEX IF NOT EXISTS site_disabled_models_site_id_idx
ON site_disabled_models (site_id);
-- ============================================================
-- SITE API ENDPOINTS
-- ============================================================
CREATE TABLE IF NOT EXISTS site_api_endpoints (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
url TEXT NOT NULL,
enabled BOOLEAN DEFAULT TRUE,
sort_order BIGINT DEFAULT 0,
cooldown_until TEXT,
last_selected_at TEXT,
last_failed_at TEXT,
last_failure_reason TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS site_api_endpoints_site_url_unique
ON site_api_endpoints (site_id, url);
CREATE INDEX IF NOT EXISTS site_api_endpoints_site_enabled_sort_idx
ON site_api_endpoints (site_id, enabled, sort_order);
CREATE INDEX IF NOT EXISTS site_api_endpoints_site_cooldown_idx
ON site_api_endpoints (site_id, cooldown_until);
-- ============================================================
-- ACCOUNT TOKENS
-- ============================================================
CREATE TABLE IF NOT EXISTS account_tokens (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL,
name TEXT NOT NULL,
token TEXT NOT NULL,
source TEXT DEFAULT 'manual',
token_group TEXT,
value_status TEXT NOT NULL DEFAULT 'ready',
enabled BOOLEAN DEFAULT TRUE,
is_default BOOLEAN DEFAULT FALSE,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS account_tokens_account_id_idx
ON account_tokens (account_id);
CREATE INDEX IF NOT EXISTS account_tokens_account_enabled_idx
ON account_tokens (account_id, enabled);
CREATE INDEX IF NOT EXISTS account_tokens_enabled_idx
ON account_tokens (enabled);
-- ============================================================
-- CHECKIN LOGS
-- ============================================================
CREATE TABLE IF NOT EXISTS checkin_logs (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL,
status TEXT NOT NULL,
message TEXT,
reward TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS checkin_logs_account_created_at_idx
ON checkin_logs (account_id, created_at);
CREATE INDEX IF NOT EXISTS checkin_logs_created_at_idx
ON checkin_logs (created_at);
CREATE INDEX IF NOT EXISTS checkin_logs_status_idx
ON checkin_logs (status);
-- ============================================================
-- MODEL AVAILABILITY
-- ============================================================
CREATE TABLE IF NOT EXISTS model_availability (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL,
model_name TEXT NOT NULL,
available BIGINT,
latency_ms BIGINT,
checked_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
is_manual BOOLEAN DEFAULT FALSE,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS model_availability_account_model_unique
ON model_availability (account_id, model_name);
CREATE INDEX IF NOT EXISTS model_availability_account_available_idx
ON model_availability (account_id, available);
CREATE INDEX IF NOT EXISTS model_availability_model_name_idx
ON model_availability (model_name);
-- ============================================================
-- TOKEN MODEL AVAILABILITY
-- ============================================================
CREATE TABLE IF NOT EXISTS token_model_availability (
id BIGSERIAL PRIMARY KEY,
token_id BIGINT NOT NULL,
model_name TEXT NOT NULL,
available BIGINT,
latency_ms BIGINT,
checked_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (token_id) REFERENCES account_tokens(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS token_model_availability_token_model_unique
ON token_model_availability (token_id, model_name);
CREATE INDEX IF NOT EXISTS token_model_availability_token_available_idx
ON token_model_availability (token_id, available);
CREATE INDEX IF NOT EXISTS token_model_availability_model_name_idx
ON token_model_availability (model_name);
CREATE INDEX IF NOT EXISTS token_model_availability_available_idx
ON token_model_availability (available);
-- ============================================================
-- ROUTE CHANNELS
-- ============================================================
CREATE TABLE IF NOT EXISTS route_channels (
id BIGSERIAL PRIMARY KEY,
route_id BIGINT NOT NULL,
account_id BIGINT NOT NULL,
token_id BIGINT,
oauth_route_unit_id BIGINT,
source_model TEXT,
priority BIGINT DEFAULT 0,
weight BIGINT DEFAULT 10,
enabled BOOLEAN DEFAULT TRUE,
manual_override BOOLEAN DEFAULT FALSE,
success_count BIGINT DEFAULT 0,
fail_count BIGINT DEFAULT 0,
total_latency_ms BIGINT DEFAULT 0,
total_cost DOUBLE PRECISION DEFAULT 0,
last_used_at TEXT,
last_selected_at TEXT,
last_fail_at TEXT,
consecutive_fail_count BIGINT NOT NULL DEFAULT 0,
cooldown_level BIGINT NOT NULL DEFAULT 0,
cooldown_until TEXT,
FOREIGN KEY (route_id) REFERENCES token_routes(id) ON DELETE CASCADE,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE,
FOREIGN KEY (token_id) REFERENCES account_tokens(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS route_channels_route_id_idx
ON route_channels (route_id);
CREATE INDEX IF NOT EXISTS route_channels_account_id_idx
ON route_channels (account_id);
CREATE INDEX IF NOT EXISTS route_channels_token_id_idx
ON route_channels (token_id);
CREATE INDEX IF NOT EXISTS route_channels_route_enabled_idx
ON route_channels (route_id, enabled);
CREATE INDEX IF NOT EXISTS route_channels_route_token_idx
ON route_channels (route_id, token_id);
CREATE INDEX IF NOT EXISTS route_channels_oauth_route_unit_id_idx
ON route_channels (oauth_route_unit_id);
-- ============================================================
-- ROUTE GROUP SOURCES
-- ============================================================
CREATE TABLE IF NOT EXISTS route_group_sources (
id BIGSERIAL PRIMARY KEY,
group_route_id BIGINT NOT NULL,
source_route_id BIGINT NOT NULL,
FOREIGN KEY (group_route_id) REFERENCES token_routes(id) ON DELETE CASCADE,
FOREIGN KEY (source_route_id) REFERENCES token_routes(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS route_group_sources_group_source_unique
ON route_group_sources (group_route_id, source_route_id);
CREATE INDEX IF NOT EXISTS route_group_sources_source_route_id_idx
ON route_group_sources (source_route_id);
-- ============================================================
-- PROXY LOGS
-- ============================================================
CREATE TABLE IF NOT EXISTS proxy_logs (
id BIGSERIAL PRIMARY KEY,
route_id BIGINT,
channel_id BIGINT,
account_id BIGINT,
downstream_api_key_id BIGINT,
model_requested TEXT,
model_actual TEXT,
status TEXT,
http_status BIGINT,
latency_ms BIGINT,
first_byte_latency_ms BIGINT,
prompt_tokens BIGINT,
completion_tokens BIGINT,
total_tokens BIGINT,
estimated_cost DOUBLE PRECISION,
error_message TEXT,
retry_count BIGINT DEFAULT 0,
is_stream BIGINT,
client_family TEXT,
client_app_id TEXT,
client_app_name TEXT,
client_confidence TEXT,
billing_details TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE INDEX IF NOT EXISTS proxy_logs_created_at_idx
ON proxy_logs (created_at);
CREATE INDEX IF NOT EXISTS proxy_logs_account_created_at_idx
ON proxy_logs (account_id, created_at);
CREATE INDEX IF NOT EXISTS proxy_logs_status_created_at_idx
ON proxy_logs (status, created_at);
CREATE INDEX IF NOT EXISTS proxy_logs_model_actual_created_at_idx
ON proxy_logs (model_actual, created_at);
CREATE INDEX IF NOT EXISTS proxy_logs_downstream_api_key_created_at_idx
ON proxy_logs (downstream_api_key_id, created_at);
CREATE INDEX IF NOT EXISTS proxy_logs_client_app_id_created_at_idx
ON proxy_logs (client_app_id, created_at);
CREATE INDEX IF NOT EXISTS proxy_logs_client_family_created_at_idx
ON proxy_logs (client_family, created_at);
-- ============================================================
-- PROXY VIDEO TASKS
-- ============================================================
CREATE TABLE IF NOT EXISTS proxy_video_tasks (
id BIGSERIAL PRIMARY KEY,
public_id TEXT NOT NULL,
upstream_video_id TEXT NOT NULL,
site_url TEXT NOT NULL,
token_value TEXT NOT NULL,
requested_model TEXT,
actual_model TEXT,
channel_id BIGINT,
account_id BIGINT,
status_snapshot TEXT,
upstream_response_meta TEXT,
last_upstream_status BIGINT,
last_polled_at TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE UNIQUE INDEX IF NOT EXISTS proxy_video_tasks_public_id_unique
ON proxy_video_tasks (public_id);
CREATE INDEX IF NOT EXISTS proxy_video_tasks_upstream_video_id_idx
ON proxy_video_tasks (upstream_video_id);
CREATE INDEX IF NOT EXISTS proxy_video_tasks_created_at_idx
ON proxy_video_tasks (created_at);
-- ============================================================
-- PROXY FILES
-- ============================================================
CREATE TABLE IF NOT EXISTS proxy_files (
id BIGSERIAL PRIMARY KEY,
public_id TEXT NOT NULL,
owner_type TEXT NOT NULL,
owner_id TEXT NOT NULL,
filename TEXT NOT NULL,
mime_type TEXT NOT NULL,
purpose TEXT,
byte_size BIGINT NOT NULL,
sha256 TEXT NOT NULL,
content_base64 TEXT NOT NULL,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
deleted_at TEXT
);
CREATE UNIQUE INDEX IF NOT EXISTS proxy_files_public_id_unique
ON proxy_files (public_id);
CREATE INDEX IF NOT EXISTS proxy_files_owner_lookup_idx
ON proxy_files (owner_type, owner_id, deleted_at);
-- ============================================================
-- PROXY DEBUG TRACES
-- ============================================================
CREATE TABLE IF NOT EXISTS proxy_debug_traces (
id BIGSERIAL PRIMARY KEY,
downstream_path TEXT NOT NULL,
client_kind TEXT,
session_id TEXT,
trace_hint TEXT,
requested_model TEXT,
downstream_api_key_id BIGINT,
request_headers_json TEXT,
request_body_json TEXT,
sticky_session_key TEXT,
sticky_hit_channel_id BIGINT,
selected_channel_id BIGINT,
selected_route_id BIGINT,
selected_account_id BIGINT,
selected_site_id BIGINT,
selected_site_platform TEXT,
endpoint_candidates_json TEXT,
endpoint_runtime_state_json TEXT,
decision_summary_json TEXT,
final_status TEXT,
final_http_status BIGINT,
final_upstream_path TEXT,
final_response_headers_json TEXT,
final_response_body_json TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE INDEX IF NOT EXISTS proxy_debug_traces_created_at_idx
ON proxy_debug_traces (created_at);
CREATE INDEX IF NOT EXISTS proxy_debug_traces_session_created_at_idx
ON proxy_debug_traces (session_id, created_at);
CREATE INDEX IF NOT EXISTS proxy_debug_traces_model_created_at_idx
ON proxy_debug_traces (requested_model, created_at);
CREATE INDEX IF NOT EXISTS proxy_debug_traces_final_status_created_at_idx
ON proxy_debug_traces (final_status, created_at);
-- ============================================================
-- PROXY DEBUG ATTEMPTS
-- ============================================================
CREATE TABLE IF NOT EXISTS proxy_debug_attempts (
id BIGSERIAL PRIMARY KEY,
trace_id BIGINT NOT NULL,
attempt_index BIGINT NOT NULL,
endpoint TEXT NOT NULL,
request_path TEXT NOT NULL,
target_url TEXT NOT NULL,
runtime_executor TEXT,
request_headers_json TEXT,
request_body_json TEXT,
response_status BIGINT,
response_headers_json TEXT,
response_body_json TEXT,
raw_error_text TEXT,
recover_applied BOOLEAN DEFAULT FALSE,
downgrade_decision BOOLEAN DEFAULT FALSE,
downgrade_reason TEXT,
memory_write_json TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (trace_id) REFERENCES proxy_debug_traces(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS proxy_debug_attempts_trace_attempt_unique
ON proxy_debug_attempts (trace_id, attempt_index);
CREATE INDEX IF NOT EXISTS proxy_debug_attempts_trace_created_at_idx
ON proxy_debug_attempts (trace_id, created_at);
-- ============================================================
-- EVENTS
-- ============================================================
CREATE TABLE IF NOT EXISTS events (
id BIGSERIAL PRIMARY KEY,
type TEXT NOT NULL,
title TEXT NOT NULL,
message TEXT,
level TEXT NOT NULL DEFAULT 'info',
"read" BOOLEAN DEFAULT FALSE,
related_id BIGINT,
related_type TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE INDEX IF NOT EXISTS events_read_created_at_idx
ON events ("read", created_at);
CREATE INDEX IF NOT EXISTS events_type_created_at_idx
ON events (type, created_at);
CREATE INDEX IF NOT EXISTS events_created_at_idx
ON events (created_at);
-- ============================================================
-- SITE ANNOUNCEMENTS
-- ============================================================
CREATE TABLE IF NOT EXISTS site_announcements (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
platform TEXT NOT NULL,
source_key TEXT NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
level TEXT NOT NULL DEFAULT 'info',
source_url TEXT,
starts_at TEXT,
ends_at TEXT,
upstream_created_at TEXT,
upstream_updated_at TEXT,
first_seen_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
last_seen_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
read_at TEXT,
dismissed_at TEXT,
raw_payload TEXT,
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS site_announcements_site_source_key_unique
ON site_announcements (site_id, source_key);
CREATE INDEX IF NOT EXISTS site_announcements_site_id_first_seen_at_idx
ON site_announcements (site_id, first_seen_at);
CREATE INDEX IF NOT EXISTS site_announcements_read_at_idx
ON site_announcements (read_at);
-- ============================================================
-- OAUTH ROUTE UNITS
-- ============================================================
CREATE TABLE IF NOT EXISTS oauth_route_units (
id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
provider TEXT NOT NULL,
name TEXT NOT NULL,
strategy TEXT NOT NULL DEFAULT 'round_robin',
enabled BOOLEAN DEFAULT TRUE,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS oauth_route_units_site_provider_idx
ON oauth_route_units (site_id, provider);
CREATE INDEX IF NOT EXISTS oauth_route_units_enabled_idx
ON oauth_route_units (enabled);
-- ============================================================
-- OAUTH ROUTE UNIT MEMBERS
-- ============================================================
CREATE TABLE IF NOT EXISTS oauth_route_unit_members (
id BIGSERIAL PRIMARY KEY,
unit_id BIGINT NOT NULL,
account_id BIGINT NOT NULL,
sort_order BIGINT DEFAULT 0,
success_count BIGINT DEFAULT 0,
fail_count BIGINT DEFAULT 0,
total_latency_ms BIGINT DEFAULT 0,
total_cost DOUBLE PRECISION DEFAULT 0,
last_used_at TEXT,
last_selected_at TEXT,
last_fail_at TEXT,
consecutive_fail_count BIGINT NOT NULL DEFAULT 0,
cooldown_level BIGINT NOT NULL DEFAULT 0,
cooldown_until TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (unit_id) REFERENCES oauth_route_units(id) ON DELETE CASCADE,
FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS oauth_route_unit_members_unit_account_unique
ON oauth_route_unit_members (unit_id, account_id);
CREATE UNIQUE INDEX IF NOT EXISTS oauth_route_unit_members_account_unique
ON oauth_route_unit_members (account_id);
CREATE INDEX IF NOT EXISTS oauth_route_unit_members_unit_sort_idx
ON oauth_route_unit_members (unit_id, sort_order);
CREATE INDEX IF NOT EXISTS oauth_route_unit_members_unit_cooldown_idx
ON oauth_route_unit_members (unit_id, cooldown_until);
-- ============================================================
-- SETTINGS
-- ============================================================
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY NOT NULL,
value TEXT
);
-- ============================================================
-- ADMIN SNAPSHOTS
-- ============================================================
CREATE TABLE IF NOT EXISTS admin_snapshots (
id BIGSERIAL PRIMARY KEY,
namespace TEXT NOT NULL,
snapshot_key TEXT NOT NULL,
payload TEXT NOT NULL,
generated_at TEXT NOT NULL,
expires_at TEXT NOT NULL,
stale_until TEXT NOT NULL,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE UNIQUE INDEX IF NOT EXISTS admin_snapshots_namespace_key_unique
ON admin_snapshots (namespace, snapshot_key);
CREATE INDEX IF NOT EXISTS admin_snapshots_expires_at_idx
ON admin_snapshots (expires_at);
CREATE INDEX IF NOT EXISTS admin_snapshots_stale_until_idx
ON admin_snapshots (stale_until);
-- ============================================================
-- ANALYTICS PROJECTION CHECKPOINTS
-- ============================================================
CREATE TABLE IF NOT EXISTS analytics_projection_checkpoints (
projector_key TEXT PRIMARY KEY NOT NULL,
time_zone TEXT NOT NULL DEFAULT 'Local',
last_proxy_log_id BIGINT NOT NULL DEFAULT 0,
watermark_created_at TEXT,
recompute_from_id BIGINT,
recompute_requested_at TEXT,
recompute_reason TEXT,
recompute_started_at TEXT,
recompute_completed_at TEXT,
last_projected_at TEXT,
last_successful_at TEXT,
last_error TEXT,
lease_owner TEXT,
lease_token TEXT,
lease_expires_at TEXT,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE INDEX IF NOT EXISTS analytics_projection_checkpoints_recompute_from_id_idx
ON analytics_projection_checkpoints (recompute_from_id);
CREATE INDEX IF NOT EXISTS analytics_projection_checkpoints_lease_expires_at_idx
ON analytics_projection_checkpoints (lease_expires_at);
-- ============================================================
-- MODEL DAY USAGE
-- ============================================================
CREATE TABLE IF NOT EXISTS model_day_usage (
id BIGSERIAL PRIMARY KEY,
local_day TEXT NOT NULL,
site_id BIGINT NOT NULL,
model TEXT NOT NULL,
total_calls BIGINT NOT NULL DEFAULT 0,
success_calls BIGINT NOT NULL DEFAULT 0,
failed_calls BIGINT NOT NULL DEFAULT 0,
total_tokens BIGINT NOT NULL DEFAULT 0,
total_spend DOUBLE PRECISION NOT NULL DEFAULT 0,
total_latency_ms BIGINT NOT NULL DEFAULT 0,
latency_count BIGINT NOT NULL DEFAULT 0,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
CONSTRAINT model_day_usage_non_negative CHECK (
total_calls >= 0
AND success_calls >= 0
AND failed_calls >= 0
AND total_tokens >= 0
AND total_spend >= 0
AND total_latency_ms >= 0
AND latency_count >= 0
)
);
CREATE UNIQUE INDEX IF NOT EXISTS model_day_usage_day_site_model_unique
ON model_day_usage (local_day, site_id, model);
CREATE INDEX IF NOT EXISTS model_day_usage_day_idx
ON model_day_usage (local_day);
CREATE INDEX IF NOT EXISTS model_day_usage_site_id_idx
ON model_day_usage (site_id);
CREATE INDEX IF NOT EXISTS model_day_usage_model_idx
ON model_day_usage (model);
-- ============================================================
-- SITE DAY USAGE
-- ============================================================
CREATE TABLE IF NOT EXISTS site_day_usage (
id BIGSERIAL PRIMARY KEY,
local_day TEXT NOT NULL,
site_id BIGINT NOT NULL,
total_calls BIGINT NOT NULL DEFAULT 0,
success_calls BIGINT NOT NULL DEFAULT 0,
failed_calls BIGINT NOT NULL DEFAULT 0,
total_tokens BIGINT NOT NULL DEFAULT 0,
total_summary_spend DOUBLE PRECISION NOT NULL DEFAULT 0,
total_site_spend DOUBLE PRECISION NOT NULL DEFAULT 0,
total_latency_ms BIGINT NOT NULL DEFAULT 0,
latency_count BIGINT NOT NULL DEFAULT 0,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
CONSTRAINT site_day_usage_non_negative CHECK (
total_calls >= 0
AND success_calls >= 0
AND failed_calls >= 0
AND total_tokens >= 0
AND total_summary_spend >= 0
AND total_site_spend >= 0
AND total_latency_ms >= 0
AND latency_count >= 0
)
);
CREATE UNIQUE INDEX IF NOT EXISTS site_day_usage_day_site_unique
ON site_day_usage (local_day, site_id);
CREATE INDEX IF NOT EXISTS site_day_usage_day_idx
ON site_day_usage (local_day);
CREATE INDEX IF NOT EXISTS site_day_usage_site_id_idx
ON site_day_usage (site_id);
-- ============================================================
-- SITE HOUR USAGE
-- ============================================================
CREATE TABLE IF NOT EXISTS site_hour_usage (
id BIGSERIAL PRIMARY KEY,
bucket_start_utc TEXT NOT NULL,
site_id BIGINT NOT NULL,
total_calls BIGINT NOT NULL DEFAULT 0,
success_calls BIGINT NOT NULL DEFAULT 0,
failed_calls BIGINT NOT NULL DEFAULT 0,
total_tokens BIGINT NOT NULL DEFAULT 0,
total_summary_spend DOUBLE PRECISION NOT NULL DEFAULT 0,
total_site_spend DOUBLE PRECISION NOT NULL DEFAULT 0,
total_latency_ms BIGINT NOT NULL DEFAULT 0,
latency_count BIGINT NOT NULL DEFAULT 0,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (site_id) REFERENCES sites(id) ON DELETE CASCADE,
CONSTRAINT site_hour_usage_non_negative CHECK (
total_calls >= 0
AND success_calls >= 0
AND failed_calls >= 0
AND total_tokens >= 0
AND total_summary_spend >= 0
AND total_site_spend >= 0
AND total_latency_ms >= 0
AND latency_count >= 0
)
);
CREATE UNIQUE INDEX IF NOT EXISTS site_hour_usage_hour_site_unique
ON site_hour_usage (bucket_start_utc, site_id);
CREATE INDEX IF NOT EXISTS site_hour_usage_hour_idx
ON site_hour_usage (bucket_start_utc);
CREATE INDEX IF NOT EXISTS site_hour_usage_site_id_idx
ON site_hour_usage (site_id);
@@ -0,0 +1,24 @@
-- Background tasks tracking
CREATE TABLE IF NOT EXISTS background_tasks (
id TEXT PRIMARY KEY NOT NULL,
task_type TEXT NOT NULL,
title TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
message TEXT DEFAULT '',
error TEXT,
dedupe_key TEXT,
success_title TEXT,
failure_title TEXT,
notify_on_success BOOLEAN DEFAULT TRUE,
notify_on_failure BOOLEAN DEFAULT TRUE,
result_json TEXT,
progress BIGINT DEFAULT 0,
progress_total BIGINT DEFAULT 0,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
started_at TEXT,
finished_at TEXT
);
CREATE INDEX IF NOT EXISTS background_tasks_status_idx
ON background_tasks (status);
@@ -0,0 +1,53 @@
-- Organizations, members, and invitations for multi-tenant support
CREATE TABLE IF NOT EXISTS organizations (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
slug TEXT NOT NULL UNIQUE,
plan TEXT NOT NULL DEFAULT 'free',
status TEXT NOT NULL DEFAULT 'active',
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE UNIQUE INDEX IF NOT EXISTS org_slug_idx
ON organizations (slug);
CREATE INDEX IF NOT EXISTS org_plan_idx
ON organizations (plan);
CREATE TABLE IF NOT EXISTS organization_members (
id BIGSERIAL PRIMARY KEY,
org_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
role TEXT NOT NULL DEFAULT 'member',
status TEXT NOT NULL DEFAULT 'active',
joined_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE UNIQUE INDEX IF NOT EXISTS org_member_unique
ON organization_members (org_id, user_id);
CREATE INDEX IF NOT EXISTS org_members_org_id_idx
ON organization_members (org_id);
CREATE INDEX IF NOT EXISTS org_members_user_id_idx
ON organization_members (user_id);
CREATE TABLE IF NOT EXISTS invitations (
id BIGSERIAL PRIMARY KEY,
org_id BIGINT NOT NULL REFERENCES organizations(id) ON DELETE CASCADE,
email TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
role TEXT NOT NULL DEFAULT 'member',
invited_by BIGINT REFERENCES users(id),
status TEXT NOT NULL DEFAULT 'pending',
expires_at TEXT NOT NULL,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT)
);
CREATE UNIQUE INDEX IF NOT EXISTS invitations_token_unique
ON invitations (token);
CREATE INDEX IF NOT EXISTS invitations_status_idx
ON invitations (status);
@@ -0,0 +1,37 @@
-- Usage tracking tables migrated from the Node.js backend.
CREATE TABLE IF NOT EXISTS usage_logs (
id BIGSERIAL PRIMARY KEY,
downstream_key_id BIGINT,
model TEXT,
prompt_tokens BIGINT DEFAULT 0,
completion_tokens BIGINT DEFAULT 0,
total_tokens BIGINT DEFAULT 0,
cost DOUBLE PRECISION DEFAULT 0.0,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY (downstream_key_id) REFERENCES downstream_api_keys(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS usage_logs_downstream_key_created_idx
ON usage_logs (downstream_key_id, created_at);
CREATE INDEX IF NOT EXISTS usage_logs_created_idx
ON usage_logs (created_at);
CREATE TABLE IF NOT EXISTS usage_daily_snapshots (
id BIGSERIAL PRIMARY KEY,
local_day TEXT NOT NULL,
downstream_key_id BIGINT,
request_count BIGINT NOT NULL DEFAULT 0,
prompt_tokens BIGINT NOT NULL DEFAULT 0,
completion_tokens BIGINT NOT NULL DEFAULT 0,
total_tokens BIGINT NOT NULL DEFAULT 0,
total_cost DOUBLE PRECISION NOT NULL DEFAULT 0.0,
created_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT DEFAULT (CURRENT_TIMESTAMP::TEXT),
UNIQUE(),
FOREIGN KEY (downstream_key_id) REFERENCES downstream_api_keys(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS usage_daily_snapshots_day_idx
ON usage_daily_snapshots (local_day);
@@ -0,0 +1,33 @@
-- Admin-authored announcements for normal users.
CREATE TABLE IF NOT EXISTS user_announcements (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
content TEXT NOT NULL,
level TEXT NOT NULL DEFAULT 'info',
status TEXT NOT NULL DEFAULT 'published',
audience TEXT NOT NULL DEFAULT 'all',
starts_at TEXT,
ends_at TEXT,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
published_at TEXT
);
CREATE INDEX IF NOT EXISTS user_announcements_status_created_at_idx
ON user_announcements (status, created_at);
CREATE INDEX IF NOT EXISTS user_announcements_active_window_idx
ON user_announcements (starts_at, ends_at);
CREATE TABLE IF NOT EXISTS user_announcement_reads (
announcement_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
read_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
PRIMARY KEY (announcement_id, user_id),
FOREIGN KEY (announcement_id) REFERENCES user_announcements(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS user_announcement_reads_user_id_idx
ON user_announcement_reads (user_id);
@@ -0,0 +1,34 @@
-- Workspace assets: local archive for creative outputs from Picture Agent,
-- ComfyUI Agent, Video Agent and ComfyUI workspaces.
CREATE TABLE IF NOT EXISTS workspace_assets (
id BIGSERIAL PRIMARY KEY,
public_id TEXT NOT NULL UNIQUE,
user_id BIGINT,
source TEXT NOT NULL,
kind TEXT NOT NULL,
title TEXT,
prompt TEXT,
model TEXT,
status TEXT NOT NULL DEFAULT 'completed',
mime_type TEXT,
file_name TEXT,
file_path TEXT,
remote_url TEXT,
thumbnail_path TEXT,
size_bytes BIGINT NOT NULL DEFAULT 0,
metadata TEXT NOT NULL DEFAULT '{}',
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
deleted_at TEXT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_workspace_assets_user_created
ON workspace_assets(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_workspace_assets_source_created
ON workspace_assets(source, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_workspace_assets_kind_created
ON workspace_assets(kind, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_workspace_assets_deleted
ON workspace_assets(deleted_at);
@@ -0,0 +1,64 @@
-- BoosAPI Workflow Studio core tables.
CREATE TABLE IF NOT EXISTS workflows (
id BIGSERIAL PRIMARY KEY,
public_id TEXT NOT NULL UNIQUE,
user_id BIGINT,
name TEXT NOT NULL,
description TEXT,
graph TEXT NOT NULL DEFAULT '{}',
is_template BIGINT NOT NULL DEFAULT 0,
visibility TEXT NOT NULL DEFAULT 'private',
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
updated_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
deleted_at TEXT,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_workflows_user_updated
ON workflows(user_id, updated_at DESC);
CREATE INDEX IF NOT EXISTS idx_workflows_visibility
ON workflows(visibility);
CREATE INDEX IF NOT EXISTS idx_workflows_deleted
ON workflows(deleted_at);
CREATE TABLE IF NOT EXISTS workflow_runs (
id BIGSERIAL PRIMARY KEY,
public_id TEXT NOT NULL UNIQUE,
workflow_id BIGINT NOT NULL,
user_id BIGINT,
status TEXT NOT NULL,
input_overrides TEXT NOT NULL DEFAULT '{}',
final_outputs TEXT,
error_message TEXT,
started_at TEXT,
completed_at TEXT,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY(workflow_id) REFERENCES workflows(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE SET NULL
);
CREATE INDEX IF NOT EXISTS idx_workflow_runs_workflow_created
ON workflow_runs(workflow_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_workflow_runs_user_created
ON workflow_runs(user_id, created_at DESC);
CREATE INDEX IF NOT EXISTS idx_workflow_runs_status
ON workflow_runs(status);
CREATE TABLE IF NOT EXISTS workflow_node_runs (
id BIGSERIAL PRIMARY KEY,
run_id BIGINT NOT NULL,
node_id TEXT NOT NULL,
node_type TEXT NOT NULL,
status TEXT NOT NULL,
inputs TEXT NOT NULL DEFAULT '{}',
outputs TEXT,
error_message TEXT,
started_at TEXT,
completed_at TEXT,
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::TEXT),
FOREIGN KEY(run_id) REFERENCES workflow_runs(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_workflow_node_runs_run
ON workflow_node_runs(run_id, id);