CREATE TABLE users ( id INTEGER PRIMARY KEY, email TEXT NOT NULL COLLATE NOCASE UNIQUE CHECK(length(trim(email)) > 3), password_hash TEXT NOT NULL CHECK(length(password_hash) > 0), role TEXT NOT NULL CHECK(role IN ('admin', 'editor', 'viewer')), is_active INTEGER NOT NULL DEFAULT 1 CHECK(is_active IN (0, 1)), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), last_login_at TEXT ); CREATE TABLE projects ( id INTEGER PRIMARY KEY, name TEXT NOT NULL CHECK(length(trim(name)) > 0), slug TEXT NOT NULL COLLATE NOCASE UNIQUE CHECK(length(trim(slug)) > 0), description TEXT NOT NULL DEFAULT '', is_active INTEGER NOT NULL DEFAULT 1 CHECK(is_active IN (0, 1)), created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) ); CREATE TABLE tags ( id INTEGER PRIMARY KEY, name TEXT NOT NULL CHECK(length(trim(name)) > 0), slug TEXT NOT NULL COLLATE NOCASE UNIQUE CHECK(length(trim(slug)) > 0), description TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) ); CREATE TABLE project_tags ( project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), PRIMARY KEY (project_id, tag_id) ); CREATE TABLE releases ( id INTEGER PRIMARY KEY, project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, version TEXT NOT NULL CHECK(length(trim(version)) > 0), build TEXT NOT NULL DEFAULT '', filename TEXT NOT NULL CHECK(length(trim(filename)) > 0), storage_path TEXT NOT NULL UNIQUE CHECK(length(trim(storage_path)) > 0), checksum_sha256 TEXT NOT NULL CHECK(length(checksum_sha256) = 64), size_bytes INTEGER NOT NULL CHECK(size_bytes >= 0), content_type TEXT NOT NULL DEFAULT 'application/octet-stream', release_notes TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), uploaded_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, is_active INTEGER NOT NULL DEFAULT 1 CHECK(is_active IN (0, 1)), UNIQUE (project_id, version, build) ); CREATE TABLE api_keys ( id INTEGER PRIMARY KEY, name TEXT NOT NULL CHECK(length(trim(name)) > 0), key_prefix TEXT NOT NULL UNIQUE CHECK(length(trim(key_prefix)) > 0), key_hash TEXT NOT NULL UNIQUE CHECK(length(trim(key_hash)) > 0), description TEXT NOT NULL DEFAULT '', scope_mode TEXT NOT NULL CHECK(scope_mode IN ('all_projects', 'project_allow_list', 'project_deny_list', 'tag_allow_list', 'tag_deny_list')), can_download INTEGER NOT NULL DEFAULT 0 CHECK(can_download IN (0, 1)), can_upload INTEGER NOT NULL DEFAULT 0 CHECK(can_upload IN (0, 1)), can_delete INTEGER NOT NULL DEFAULT 0 CHECK(can_delete IN (0, 1)), can_manage_projects INTEGER NOT NULL DEFAULT 0 CHECK(can_manage_projects IN (0, 1)), is_active INTEGER NOT NULL DEFAULT 1 CHECK(is_active IN (0, 1)), expires_at TEXT, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), updated_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), last_used_at TEXT, created_by_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL ); CREATE TABLE api_key_project_access ( api_key_id INTEGER NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE, project_id INTEGER NOT NULL REFERENCES projects(id) ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), PRIMARY KEY (api_key_id, project_id) ); CREATE TABLE api_key_tag_access ( api_key_id INTEGER NOT NULL REFERENCES api_keys(id) ON DELETE CASCADE, tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')), PRIMARY KEY (api_key_id, tag_id) ); CREATE TABLE sessions ( id INTEGER PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash TEXT NOT NULL UNIQUE CHECK(length(trim(token_hash)) > 0), expires_at TEXT NOT NULL, last_seen_at TEXT, invalidated_at TEXT, ip_address TEXT NOT NULL DEFAULT '', user_agent TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) ); CREATE TABLE audit_logs ( id INTEGER PRIMARY KEY, actor_user_id INTEGER REFERENCES users(id) ON DELETE SET NULL, api_key_id INTEGER REFERENCES api_keys(id) ON DELETE SET NULL, action TEXT NOT NULL CHECK(length(trim(action)) > 0), target_type TEXT NOT NULL DEFAULT '', target_id INTEGER, target_identifier TEXT NOT NULL DEFAULT '', metadata_json TEXT NOT NULL DEFAULT '{}', ip_address TEXT NOT NULL DEFAULT '', created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%SZ', 'now')) );