-- Initial embedded Turso schema. Every statement is safe to run repeatedly. CREATE TABLE IF NOT EXISTS users (id TEXT PRIMARY KEY, username TEXT NOT NULL UNIQUE, login_username TEXT, password_hash TEXT, discord_id TEXT UNIQUE, role TEXT NOT NULL DEFAULT 'user', created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS user_profiles ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, display_name TEXT NOT NULL, updated_at TEXT NOT NULL ); INSERT OR IGNORE INTO user_profiles(user_id,display_name,updated_at) SELECT id,username,created_at FROM users; CREATE TABLE IF NOT EXISTS user_avatars ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, source TEXT NOT NULL CHECK(source IN ('preset', 'upload')), value TEXT NOT NULL, byte_size INTEGER CHECK(byte_size IS NULL OR byte_size >= 0), updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS builds (id TEXT PRIMARY KEY, owner_id TEXT NOT NULL REFERENCES users(id), title TEXT NOT NULL, slug TEXT NOT NULL UNIQUE, visibility TEXT NOT NULL DEFAULT 'draft', summary TEXT, guide_body TEXT, source_build_id TEXT REFERENCES builds(id), created_at TEXT NOT NULL, updated_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS build_items (id TEXT PRIMARY KEY, build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, slot_id TEXT NOT NULL, item_definition_id TEXT NOT NULL, configuration_json TEXT NOT NULL); CREATE TABLE IF NOT EXISTS ratings (user_id TEXT NOT NULL REFERENCES users(id), build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, value INTEGER NOT NULL CHECK(value BETWEEN 1 AND 5), PRIMARY KEY(user_id, build_id)); CREATE TABLE IF NOT EXISTS comments (id TEXT PRIMARY KEY, build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, user_id TEXT NOT NULL REFERENCES users(id), body TEXT NOT NULL, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS favorites (user_id TEXT NOT NULL REFERENCES users(id), build_id TEXT NOT NULL REFERENCES builds(id) ON DELETE CASCADE, created_at TEXT NOT NULL, PRIMARY KEY(user_id, build_id)); CREATE TABLE IF NOT EXISTS equipment_types (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, category TEXT NOT NULL); CREATE TABLE IF NOT EXISTS stat_definitions (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, value_kind TEXT NOT NULL, min_value REAL, max_value REAL, unit TEXT, active INTEGER NOT NULL DEFAULT 1); CREATE TABLE IF NOT EXISTS stat_equipment_types (stat_id TEXT NOT NULL REFERENCES stat_definitions(id), equipment_type_id TEXT NOT NULL REFERENCES equipment_types(id), PRIMARY KEY(stat_id, equipment_type_id)); CREATE TABLE IF NOT EXISTS audit_log (id TEXT PRIMARY KEY, actor_id TEXT NOT NULL REFERENCES users(id), entity_type TEXT NOT NULL, entity_id TEXT NOT NULL, action TEXT NOT NULL, before_json TEXT, after_json TEXT, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS roles (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT); CREATE TABLE IF NOT EXISTS permissions (id TEXT PRIMARY KEY, name TEXT NOT NULL UNIQUE, description TEXT); CREATE TABLE IF NOT EXISTS user_roles (user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, role_id TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE, PRIMARY KEY(user_id, role_id)); CREATE TABLE IF NOT EXISTS role_permissions (role_id TEXT NOT NULL REFERENCES roles(id) ON DELETE CASCADE, permission_id TEXT NOT NULL REFERENCES permissions(id) ON DELETE CASCADE, scope_json TEXT, PRIMARY KEY(role_id, permission_id)); CREATE TABLE IF NOT EXISTS user_permissions (user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, permission_id TEXT NOT NULL REFERENCES permissions(id) ON DELETE CASCADE, PRIMARY KEY(user_id, permission_id)); CREATE TABLE IF NOT EXISTS user_status ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, last_active_at TEXT, blocked_at TEXT, blocked_by TEXT REFERENCES users(id), banned_until TEXT, moderation_reason TEXT, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS login_events ( id TEXT PRIMARY KEY, user_id TEXT REFERENCES users(id) ON DELETE SET NULL, provider TEXT NOT NULL CHECK(provider IN ('password', 'discord', 'magic_link', 'passkey')), succeeded INTEGER NOT NULL CHECK(succeeded IN (0, 1)), ip_address TEXT, user_agent TEXT, client_name TEXT, operating_system TEXT, device_type TEXT, location_country TEXT, location_region TEXT, location_city TEXT, failure_reason TEXT, risk_flags TEXT, credential_id TEXT, session_id TEXT REFERENCES sessions(id) ON DELETE SET NULL, session_ended_at TEXT, session_end_reason TEXT, sensitive_purged_at TEXT, created_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS user_emails ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, email TEXT NOT NULL COLLATE NOCASE UNIQUE, verified_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS magic_link_tokens ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, email TEXT NOT NULL COLLATE NOCASE, purpose TEXT NOT NULL CHECK(purpose IN ('login', 'verify_email')), token_hash TEXT NOT NULL UNIQUE, expires_at TEXT NOT NULL, consumed_at TEXT, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS magic_link_tokens_expiry_idx ON magic_link_tokens(expires_at); CREATE TABLE IF NOT EXISTS passkeys ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, webauthn_user_id TEXT NOT NULL, public_key BLOB NOT NULL, counter INTEGER NOT NULL DEFAULT 0, device_type TEXT NOT NULL, backed_up INTEGER NOT NULL DEFAULT 0 CHECK(backed_up IN (0, 1)), transports TEXT, name TEXT NOT NULL, last_used_at TEXT, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS passkeys_user_idx ON passkeys(user_id); CREATE TABLE IF NOT EXISTS webauthn_challenges ( id TEXT PRIMARY KEY, user_id TEXT REFERENCES users(id) ON DELETE CASCADE, flow TEXT NOT NULL CHECK(flow IN ('registration', 'authentication')), challenge TEXT NOT NULL, rp_id TEXT NOT NULL, origin TEXT NOT NULL, expires_at TEXT NOT NULL, created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS webauthn_challenges_expiry_idx ON webauthn_challenges(expires_at); CREATE TABLE IF NOT EXISTS deleted_accounts ( user_id TEXT PRIMARY KEY REFERENCES users(id), original_username TEXT NOT NULL, deleted_at TEXT NOT NULL, deleted_by TEXT NOT NULL REFERENCES users(id), reason TEXT ); CREATE INDEX IF NOT EXISTS login_events_user_created_idx ON login_events(user_id, created_at DESC); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-data-editor','Data Editor','Access to ISAC game-data management'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-user-operator','User Operator','View and moderate SHD.build users'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-access-manager','Access Manager','Manage user roles and direct permissions'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-admin','Admin','Full administrative access to all current and future ISAC capabilities'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-moderator','Moderator','Community moderation role reserved for build, guide, and comment tools'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-system-admin','system.admin','Unrestricted access to all administrative capabilities'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-isac-access','isac.access','Open the ISAC interface'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-data-manage','data.manage','Create and edit builder source data'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-view','users.view','View users, activity, and content history'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-moderate','users.moderate','Block, ban, and reactivate users'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-roles','users.roles','Assign roles and direct permissions'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-delete','users.delete','Soft-delete user accounts while retaining authored content'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-users-identities','users.identities','View and manage linked external identities'); INSERT OR IGNORE INTO permissions(id,name,description) VALUES('permission-audit-view','audit.view','View and search the immutable ISAC audit history'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-editor','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-data-editor','permission-data-manage'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-users-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-user-operator','permission-users-moderate'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-users-view'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-users-roles'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-access-manager','permission-users-identities'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-admin','permission-system-admin'); INSERT OR IGNORE INTO roles(id,name,description) VALUES('role-auditor','Auditor','Read-only access to ISAC audit history'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-auditor','permission-isac-access'); INSERT OR IGNORE INTO role_permissions(role_id,permission_id) VALUES('role-auditor','permission-audit-view'); CREATE TABLE IF NOT EXISTS oauth_states (state_hash TEXT PRIMARY KEY, provider TEXT NOT NULL, mode TEXT NOT NULL, user_id TEXT REFERENCES users(id) ON DELETE CASCADE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL); CREATE TABLE IF NOT EXISTS discord_profiles ( user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE, discord_id TEXT NOT NULL UNIQUE, username TEXT NOT NULL, display_name TEXT, avatar_hash TEXT, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS users_username_nocase_idx ON users(username COLLATE NOCASE); CREATE UNIQUE INDEX IF NOT EXISTS users_login_username_nocase_idx ON users(login_username COLLATE NOCASE) WHERE login_username IS NOT NULL; CREATE TABLE IF NOT EXISTS stat_attributes ( id TEXT PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL CHECK(type IN ('Offensive', 'Defensive')), category TEXT NOT NULL CHECK(category IN ('Weapon', 'Skill', 'Character')), calculation_type TEXT NOT NULL CHECK(calculation_type IN ('Additive', 'Multiplicative')), unit TEXT NOT NULL DEFAULT '', gear_min REAL, gear_max REAL, weapon_min REAL, weapon_max REAL, skill_chip_min REAL, skill_chip_max REAL, cap_value REAL, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK((gear_min IS NULL AND gear_max IS NULL) OR (gear_min IS NOT NULL AND gear_max IS NOT NULL AND gear_min <= gear_max)), CHECK((weapon_min IS NULL AND weapon_max IS NULL) OR (weapon_min IS NOT NULL AND weapon_max IS NOT NULL AND weapon_min <= weapon_max)), CHECK((skill_chip_min IS NULL AND skill_chip_max IS NULL) OR (skill_chip_min IS NOT NULL AND skill_chip_max IS NOT NULL AND skill_chip_min <= skill_chip_max)), CHECK(cap_value IS NULL OR cap_value >= 0) ); CREATE UNIQUE INDEX IF NOT EXISTS stat_attributes_name_nocase_idx ON stat_attributes(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS talents ( id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL CHECK(category IN ('Weapon', 'Backpack', 'Body Armor', 'Vest')), active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS talents_name_category_nocase_idx ON talents(name COLLATE NOCASE, category); CREATE TABLE IF NOT EXISTS talent_tiers ( id TEXT PRIMARY KEY, talent_id TEXT NOT NULL REFERENCES talents(id) ON DELETE CASCADE, tier TEXT NOT NULL CHECK(tier IN ('I', 'II')), description TEXT NOT NULL, trigger_text TEXT NOT NULL, effect_text TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(talent_id, tier) ); CREATE TABLE IF NOT EXISTS talent_triggers ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT NOT NULL, supports_uptime INTEGER NOT NULL DEFAULT 0 CHECK(supports_uptime IN (0, 1)), min_uptime REAL, max_uptime REAL, calculation_script TEXT, active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK( (supports_uptime = 0 AND min_uptime IS NULL AND max_uptime IS NULL) OR (supports_uptime = 1 AND min_uptime IS NOT NULL AND max_uptime IS NOT NULL AND min_uptime >= 0 AND max_uptime <= 100 AND min_uptime <= max_uptime) ) ); CREATE UNIQUE INDEX IF NOT EXISTS talent_triggers_name_nocase_idx ON talent_triggers(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS talent_tier_triggers ( talent_tier_id TEXT NOT NULL REFERENCES talent_tiers(id) ON DELETE CASCADE, trigger_id TEXT NOT NULL REFERENCES talent_triggers(id), PRIMARY KEY(talent_tier_id, trigger_id) ); CREATE TABLE IF NOT EXISTS talent_tier_effects ( id TEXT PRIMARY KEY, talent_tier_id TEXT NOT NULL REFERENCES talent_tiers(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL, created_at TEXT NOT NULL, UNIQUE(talent_tier_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS talent_description_templates ( talent_id TEXT PRIMARY KEY REFERENCES talents(id) ON DELETE CASCADE, template TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE TABLE IF NOT EXISTS talent_weapon_scopes ( talent_id TEXT PRIMARY KEY REFERENCES talents(id) ON DELETE CASCADE, scope TEXT NOT NULL CHECK(scope IN ('Standard', 'Exotic')) ); INSERT OR IGNORE INTO talent_weapon_scopes(talent_id,scope) SELECT id,'Standard' FROM talents WHERE category='Weapon'; CREATE TABLE IF NOT EXISTS weapon_talent_slots ( talent_id TEXT PRIMARY KEY REFERENCES talents(id) ON DELETE CASCADE, slot INTEGER NOT NULL CHECK(slot IN (1, 2)) ); CREATE TABLE IF NOT EXISTS brand_sets ( id TEXT PRIMARY KEY, name TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL ); CREATE UNIQUE INDEX IF NOT EXISTS brand_sets_name_nocase_idx ON brand_sets(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS brand_set_effects ( id TEXT PRIMARY KEY, brand_set_id TEXT NOT NULL REFERENCES brand_sets(id) ON DELETE CASCADE, pieces_required INTEGER NOT NULL CHECK(pieces_required IN (2, 3, 4)), stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value >= 0), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, UNIQUE(brand_set_id, pieces_required) ); CREATE TABLE IF NOT EXISTS gear ( id TEXT PRIMARY KEY, name TEXT NOT NULL, slot TEXT NOT NULL CHECK(slot IN ('Holster', 'Gloves', 'Backpack', 'Knee Pads', 'Body Armor', 'Mask')), tier INTEGER NOT NULL CHECK(tier BETWEEN 0 AND 2), level INTEGER NOT NULL CHECK(level BETWEEN 1 AND 50), base_health_min REAL NOT NULL CHECK(base_health_min >= 0), base_health_max REAL NOT NULL CHECK(base_health_max >= base_health_min), base_armor_min REAL, base_armor_max REAL, active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK( (slot IN ('Gloves', 'Knee Pads', 'Body Armor') AND base_armor_min IS NOT NULL AND base_armor_max IS NOT NULL AND base_armor_min >= 0 AND base_armor_max >= base_armor_min) OR (slot NOT IN ('Gloves', 'Knee Pads', 'Body Armor') AND base_armor_min IS NULL AND base_armor_max IS NULL) ) ); CREATE UNIQUE INDEX IF NOT EXISTS gear_name_nocase_idx ON gear(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS gear_brand_sets ( gear_id TEXT NOT NULL REFERENCES gear(id) ON DELETE CASCADE, brand_set_id TEXT NOT NULL REFERENCES brand_sets(id), PRIMARY KEY(gear_id, brand_set_id) ); CREATE TABLE IF NOT EXISTS gear_talents ( gear_id TEXT NOT NULL REFERENCES gear(id) ON DELETE CASCADE, talent_id TEXT NOT NULL REFERENCES talents(id), PRIMARY KEY(gear_id, talent_id) ); CREATE TABLE IF NOT EXISTS weapons ( id TEXT PRIMARY KEY, name TEXT NOT NULL, category TEXT NOT NULL CHECK(category IN ('Shotgun', 'Rifle', 'MMR', 'AR', 'LMG', 'SMG')), is_exotic INTEGER NOT NULL DEFAULT 0 CHECK(is_exotic IN (0, 1)), native_tier INTEGER NOT NULL CHECK(native_tier BETWEEN 0 AND 2), damage_type TEXT NOT NULL CHECK(damage_type IN ('Blast', 'Pierce', 'Shredding')), rpm REAL NOT NULL CHECK(rpm >= 0), magazine REAL NOT NULL CHECK(magazine >= 0), reload_speed REAL NOT NULL CHECK(reload_speed >= 0), accuracy REAL NOT NULL CHECK(accuracy >= 0), stability REAL NOT NULL CHECK(stability >= 0), exotic_talent_id TEXT REFERENCES talents(id), active INTEGER NOT NULL DEFAULT 1 CHECK(active IN (0, 1)), created_at TEXT NOT NULL, updated_at TEXT NOT NULL, CHECK( (is_exotic = 1 AND exotic_talent_id IS NOT NULL) OR (is_exotic = 0 AND exotic_talent_id IS NULL) ) ); CREATE UNIQUE INDEX IF NOT EXISTS weapons_name_nocase_idx ON weapons(name COLLATE NOCASE); CREATE TABLE IF NOT EXISTS weapon_tier_ranges ( weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, tier INTEGER NOT NULL CHECK(tier BETWEEN 0 AND 2), damage_min REAL NOT NULL CHECK(damage_min >= 0), damage_max REAL NOT NULL CHECK(damage_max >= damage_min), firepower_min REAL NOT NULL CHECK(firepower_min >= 0), firepower_max REAL NOT NULL CHECK(firepower_max >= firepower_min), toughness_min REAL NOT NULL CHECK(toughness_min >= 0), toughness_max REAL NOT NULL CHECK(toughness_max >= toughness_min), engineering_min REAL NOT NULL CHECK(engineering_min >= 0), engineering_max REAL NOT NULL CHECK(engineering_max >= engineering_min), PRIMARY KEY(weapon_id, tier) ); CREATE TABLE IF NOT EXISTS weapon_talent_options ( weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, talent_slot INTEGER NOT NULL CHECK(talent_slot IN (1, 2)), talent_id TEXT NOT NULL REFERENCES talents(id), PRIMARY KEY(weapon_id, talent_slot, talent_id) ); CREATE TABLE IF NOT EXISTS weapon_augmentation_effects ( id TEXT PRIMARY KEY, weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value > 0), sort_order INTEGER NOT NULL CHECK(sort_order IN (1, 2)), UNIQUE(weapon_id, sort_order), UNIQUE(weapon_id, stat_attribute_id) ); CREATE TABLE IF NOT EXISTS weapon_base_attribute_bonuses ( id TEXT PRIMARY KEY, weapon_id TEXT NOT NULL REFERENCES weapons(id) ON DELETE CASCADE, stat_attribute_id TEXT NOT NULL REFERENCES stat_attributes(id), value REAL NOT NULL CHECK(value > 0), sort_order INTEGER NOT NULL CHECK(sort_order >= 1), UNIQUE(weapon_id, stat_attribute_id), UNIQUE(weapon_id, sort_order) );