Database — SQLite Schema

General Information

ER Diagram (Phase 65)

erDiagram
    users ||--o{ projects : owns
    users ||--|| account_settings : has
    users ||--o{ recovery_keys : holds
    users ||--o{ subscriptions : pays
    users ||--o{ invites : creates
    users ||--o{ chat_messages : sends
    users ||--o{ trial_credits : claims
    users ||--o{ activity_log : generates

    projects ||--o{ project_phases : tracks
    projects ||--o{ pinned_notes : pins
    projects ||--o{ chat_messages : contains
    projects ||--o{ timeline_events : records
    projects ||--o{ github_links : links
    projects ||--o{ skill_benchmarks : runs
    projects ||--o{ marketplace_cache : caches

    github_links ||--o{ github_events : receives

    invites }o--|| users : "consumed by"
    subscriptions ||--o{ stripe_events : "logs idempotency"

    users {
        TEXT id PK
        TEXT email UK
        TEXT password_hash
        TEXT role "admin · user"
        TEXT google_id UK
        TEXT github_id UK
        INTEGER email_verified
        INTEGER two_factor_enabled
        TEXT last_login
    }

    projects {
        INTEGER id PK
        TEXT technical_name
        TEXT owner_id FK
        TEXT display_name
        INTEGER is_archived
        TEXT model_mode "strict|optimize (#520, migration 060)"
    }

    chat_messages {
        INTEGER id PK
        TEXT project_name
        TEXT worker_id
        TEXT role "user · assistant · system"
        TEXT content "AES-256-GCM if encrypted=1"
        INTEGER encrypted
        TEXT timestamp
    }

    subscriptions {
        TEXT user_id PK
        TEXT plan "free · min · max · beta"
        TEXT stripe_customer_id
        TEXT status
    }

    invites {
        TEXT code PK "arc-XXXX-XXXX"
        TEXT created_by FK
        TEXT used_by FK
        TEXT parent_invite_code
        TEXT status "active · used · revoked"
    }

    recovery_keys {
        INTEGER id PK
        TEXT user_id FK
        TEXT encrypted_key
        TEXT key_hint "first 4 chars"
    }

    trial_credits {
        TEXT email PK
        INTEGER credits_used
        TEXT first_used_at
    }

30+ tables across 35 migrations. Auth + projects + workspace data + intelligence + billing + beta gating + context-export audit. All FK constraints enforced.

Tables

users — Users

Column Type Description
id TEXT PK UUID
email TEXT UNIQUE Email (required)
password_hash TEXT Password hash (bcrypt)
role TEXT 'admin' or 'user'
name TEXT Name
avatar_url TEXT Avatar URL
google_id TEXT UNIQUE Google OAuth ID
github_id TEXT UNIQUE GitHub OAuth ID
email_verified INTEGER 0/1
two_factor_enabled INTEGER 0/1
totp_secret TEXT TOTP secret (2FA)
last_login TEXT ISO 8601
created_at TEXT ISO 8601
password_version INTEGER Password change counter (migration 035). Included in the JWT as pv; crmAuthMiddleware rejects stale tokens after a password change.

projects — Projects

Column Type Description
id INTEGER PK Auto
technical_name TEXT Technical name (unique per owner)
owner_id TEXT FK→users Owner
display_name TEXT Display name
description TEXT Description
color TEXT Color (hex)
icon TEXT Icon
is_archived INTEGER 0/1
sort_order INTEGER Sort order (default 999)
project_protocol TEXT Project protocol
created_at, updated_at TEXT Timestamps

UNIQUE(technical_name, owner_id)

account_settings — Account settings

Column Type Description
user_id TEXT PK FK→users
anthropic_key TEXT Anthropic API key
openai_key TEXT OpenAI API key
name TEXT Name
role TEXT Role
auto_harvest INTEGER Auto skill harvesting 0/1

chat_messages — Chat history

Column Type Description
id INTEGER PK Auto
project_name TEXT Project
worker_id TEXT Worker
role TEXT 'user', 'assistant', 'system'
content TEXT Message text (AES-256-GCM encrypted when encrypted=1)
encrypted INTEGER 0=plaintext, 1=vault-encrypted (Phase 45.3)
attachments TEXT JSON array
timestamp TEXT ISO 8601
metadata TEXT JSON object

INDEX(project_name, worker_id, timestamp DESC)

recovery_keys — Recovery keys (Phase 45.4)

Column Type Description
id INTEGER PK Auto
user_id TEXT FK→users Owner
encrypted_key TEXT Encrypted master key
key_hint TEXT Hint (first 4 characters)
created_at TEXT ISO 8601
used_at TEXT When it was used
revoked INTEGER 0=active, 1=revoked

INDEX(user_id)

github_links — GitHub repo bindings (Phase 49.3)

Column Type Description
id INTEGER PK Auto
project_name TEXT Arc OS project
owner TEXT GitHub repo owner
repo TEXT GitHub repo name
webhook_secret TEXT UNIQUE 32-byte hex for HMAC-SHA256 validation
created_at TEXT ISO 8601
created_by TEXT FK→users Who created the link

UNIQUE(project_name, owner, repo) — multi-repo per project allowed. INDEX(project_name), INDEX(webhook_secret)

github_events — GitHub event log (Phase 49.3.1)

Column Type Description
id INTEGER PK Auto
link_id INTEGER FK→github_links CASCADE on link delete
project_name TEXT Arc OS project (denormalized for query speed)
event_type TEXT push, pull_request, workflow_run, issues
action TEXT sub-action (opened/closed/success/failure)
summary TEXT Pre-formatted display string
url TEXT GitHub deep link
actor TEXT GitHub username
created_at TEXT ISO 8601

INDEX(project_name, created_at DESC), INDEX(link_id)

skills_global — Global skills

Column Type Description
id INTEGER PK Auto
name TEXT UNIQUE Name
description TEXT Description
category TEXT Category (default 'general')
content TEXT Skill content (markdown)
triggers TEXT JSON array of triggers
keywords TEXT JSON array of keywords
eval_rules TEXT JSON array of rules
tool_code_ts TEXT TypeScript implementation
version INTEGER Version (auto-increment)
status TEXT 'active', 'draft', 'deprecated', 'archived'
owner_project TEXT NULL=global/marketplace; project name=visible only to the owner (migration 034)

skills_project_forks — Skill forks

Column Type Description
id INTEGER PK Auto
project_name TEXT Project
skill_id INTEGER FK→skills_global Parent skill (CASCADE)
content TEXT Customized content
triggers, keywords, eval_rules, tool_code_ts TEXT Overrides
version INTEGER Fork version

UNIQUE(project_name, skill_id)

skill_evolution_logs — Skill change history

Column Type Description
id INTEGER PK
skill_id INTEGER FK CASCADE
project_name TEXT Project (nullable)
action TEXT 'created', 'modified', 'applied', 'reverted'
diff_summary TEXT Description of changes
author TEXT Author (default 'system')
metadata TEXT JSON

skill_update_requests — Skill PRs (Sage)

Column Type Description
id INTEGER PK
skill_id INTEGER FK CASCADE
proposed_by TEXT 'sage' (default)
status TEXT 'pending', 'approved', 'rejected', 'applied'
current_content TEXT Current version
proposed_content TEXT Proposed version
reason TEXT Reason for the change

skill_benchmarks — Skill A/B tests

Column Type Description
id INTEGER PK
request_id INTEGER FK→skill_update_requests CASCADE
test_scenario TEXT Test scenario
old_output, new_output TEXT Results
score_old, score_new REAL Scores
judgment_reason TEXT Justification

pinned_notes — Pinned notes

Column Type Description
id INTEGER PK
project_name TEXT Project
worker_id TEXT Worker
title TEXT Title
body TEXT Text
source_message_id INTEGER Reference to chat_messages

project_phases — Roadmap phases

Column Type Description
id INTEGER PK
project_name TEXT Project
phase_id INTEGER Phase number
phase_title TEXT Title
status TEXT 'PLANNED', 'ACTIVE', 'COMPLETED', 'CANCELLED'
progress REAL 0.0–1.0
deadline TEXT ISO 8601

UNIQUE(project_name, phase_id)

activity_log — Activity log

Column Type Description
id INTEGER PK
project_name TEXT Project
actor TEXT Worker or user
event_type TEXT Event type
title TEXT Description
metadata TEXT JSON
created_at TEXT ISO 8601

INDEX(created_at DESC), INDEX(project_name, created_at DESC), INDEX(event_type, created_at DESC), INDEX(actor, created_at DESC)

system_configs — System settings

Column Type Description
key TEXT PK Key
value TEXT Value
description TEXT Description

marketplace_analysis_cache — Marketplace cache

Cache of skill compatibility analysis from the marketplace.

ephemeral_tokens — Short-lived tokens (issue #27)

Persistent store for one-time tokens: OAuth state, password reset, email verification. Before migration 022 these tokens lived in an in-memory Map and were lost on every master restart, breaking Google/GitHub login (invalid_state).

Column Type Description
token TEXT PK 32-byte hex (randomBytes(32))
type TEXT NOT NULL oauth_state | password_reset | email_verification
payload TEXT NOT NULL JSON: {provider} for oauth_state, {email} for the rest
expires_at INTEGER NOT NULL ms epoch — TTL: 10 min (oauth) / 30 min (reset) / 24 h (verify)

INDEX(type), INDEX(expires_at). One-time use: consume() reads the payload and deletes the row in a single transaction.

project_issues — Issues per project (issue #53, Phase 53.14)

SQLite SSOT for issues, replacing per-project issues/issues.json. The file remains on disk as a derived export (mirror), gitignored — the DB is authoritative.

Column Type Description
project_name TEXT NOT NULL name from the registry; part of composite PK with id
id INTEGER NOT NULL 1-based per-project sequence (max+1 on insert)
title TEXT NOT NULL issue title
body TEXT NOT NULL DEFAULT '' description, markdown
priority TEXT NOT NULL DEFAULT 'P2' P0 | P1 | P2 | P3
labels TEXT NOT NULL DEFAULT '[]' JSON array of strings
status TEXT NOT NULL DEFAULT 'open' draft | open | in_progress | blocked | deferred | closed (SQL default 'open' is kept — new issues are forced into draft at the app level)
assignee TEXT NULL worker id that took the issue (migration 036)
created_by TEXT NOT NULL DEFAULT 'legacy' worker id of who created the issue (migration 046 / #327) — for audit (who, when, assigned to whom)
created_at TEXT NOT NULL ISO timestamp
updated_at TEXT NOT NULL ISO timestamp
closed_at TEXT NULL ISO timestamp when status=closed
activity TEXT NOT NULL DEFAULT '[]' JSON array {ts, type, author, text} — append-only audit

PK (project_name, id) — each project has its own independent id space. INDEX (project_name, status) — the most frequent filter (open only). Operations live in issueQueries (shared/db.ts): list/get/nextId/insert/upsert/replaceAll/bulkImport. replaceAll — atomic transaction; bulkImportINSERT OR IGNORE for idempotent re-seed.

Migration 036 (#187, 2026-05-23): ALTER TABLE project_issues ADD COLUMN assignee TEXT. New statuses (in_progress, blocked, deferred) are validated at the application level (IssueStatus union in shared/db.ts), not via a CHECK constraint (SQLite does not allow adding constraints through ALTER TABLE).

Migration 046 (#327, 2026-06-03): ALTER TABLE project_issues ADD COLUMN created_by TEXT NOT NULL DEFAULT 'legacy'. The lifecycle is extended with a new draft status. handleCreateIssue forces status='draft' when assignee=null (otherwise immediately 'open'), and requires worker_id (body or auth fallback). handleUpdateIssue auto-promotes draft → open on first assignee assignment and rejects close for a never-assigned issue (FR-ISS-101/102/103). Pre-existing rows are backfilled via DEFAULT as 'legacy'.

onboarding_progress — Post-wizard checklist state (issue #56, Phase 54.1)

Per-user derived cache over the activity_log event stream (SSOT). The UI reads it with a single query instead of aggregating over events. Replayable: dismiss/replay do not change state, only dismissed_at.

Column Type Description
chat_id TEXT PRIMARY KEY user id (registry chat id)
state TEXT NOT NULL DEFAULT '{}' JSON {workers, cli, skill, bot, issue}completed | skipped (missing keys = pending)
completed_count INTEGER NOT NULL DEFAULT 0 derived cache count of completed steps (0–5)
started_at TEXT NOT NULL DEFAULT (datetime('now')) row creation (first event/dismiss/replay)
completed_at TEXT NULL ISO timestamp when all 5 steps are completed
dismissed_at TEXT NULL ISO timestamp when the user dismissed the checklist
source TEXT NOT NULL DEFAULT 'web' web | cli — for funnel attribution (#58 arc tour)
updated_at TEXT NOT NULL DEFAULT (datetime('now')) mutation timestamp

INDEX on completed_at for funnel analytics (#61 Phase 54.6). Operations live in onboardingQueries (shared/db.ts): getProgress/recordEvent/dismiss/replay. recordEvent is idempotent on (chat_id, step, status) — a repeated identical call returns changed=false and does not write to activity_log. Whitelist: 5 steps × 2 statuses (completed, skipped). skipped does NOT increment completed_count. The skipped → completed transition adds to the count. All mutations emit events into activity_log with event_type LIKE 'onboarding_%' — the true SSOT for funnel metrics; the table columns are a derived cache.

platform_audit_log — Super-admin secret rotation trail (Phase 57, Sentinel #103)

Append-only log of mutations on vault platform-secrets via the Platform Settings UI. SSOT for post-incident forensics ("which admin rotated the Anthropic key at 03:14 UTC?"). Vault.json itself does not record this — value-only.

Column Type Description
id INTEGER PRIMARY KEY AUTOINCREMENT temporal order even under clock skew
ts TEXT NOT NULL DEFAULT (datetime('now')) server-side UTC; an attacker cannot backdate
user_chat_id TEXT NOT NULL admin who acted
user_email TEXT NULL snapshot from the users table at action time
action TEXT NOT NULL list | view | rotate | test | restart
key_name TEXT NOT NULL vault entry name (allowlist in platform.ts); * for list-all
ip TEXT NULL from CF-Connecting-IP / X-Real-IP / XFF tail
result TEXT NOT NULL success or fail:<reason>
user_agent TEXT NULL UA string, truncated to 200 chars

INDEXES: idx_platform_audit_ts (recent across keys), idx_platform_audit_user (per-admin trail), idx_platform_audit_key (rotation history per key). Append-only invariant: no UPDATE/DELETE handlers; UI exposes read-only recent() + lastRotated() via platformAuditQueries in shared/db.ts. Every action (success+fail) writes a row.

auth_events — End-user authentication event log (Migration 027, issue #130)

Append-only log of auth events for security auditing and incident investigation. Separate from platform_audit_log (admin-only vault) — this table covers the end-user auth flow with IP.

Column Type Description
id INTEGER PRIMARY KEY AUTOINCREMENT temporal order
ts TEXT NOT NULL DEFAULT (datetime('now')) server-side UTC
event TEXT NOT NULL signup | login | oauth_login | magic_link | device_code_approve
provider TEXT NOT NULL DEFAULT 'password' password | google | github | magic_link | device_code
user_id TEXT NULL NULL for failed attempts where the user is not identified
email TEXT NULL email used in the attempt
ip TEXT NOT NULL DEFAULT 'unknown' CF-Connecting-IP → X-Real-IP → XFF last segment
user_agent TEXT NULL UA string
result TEXT NOT NULL success | failed | rate_limited | invalid_invite | already_registered | 2fa_required | invalid_token
meta TEXT NULL JSON extra context (e.g. {isNew: true} for OAuth)

INDEXES: idx_auth_events_ts (newest first), idx_auth_events_user (per-user history), idx_auth_events_ip (per-IP threat detection), idx_auth_events_event (event+result filter). authEventQueries.insert/recent in shared/db.ts. Logged in master-bot/routes/auth.ts (register/login/oauth/magic-link) and master-bot/routes/cli.ts (device_code_approve).

Migrations

# Name Phase What it adds
001 initial_schema Phase 40 users, projects, account_settings
002 project_protocol Phase 40.7 projects.project_protocol
003 chat_messages Phase 40.10 chat_messages
004 skill_system Phase 41 skills_global, skills_project_forks, skill_evolution_logs, skill_update_requests
005 skill_benchmarks Phase 40.11d skill_benchmarks
006 system_configs system_configs + FIGMA_CACHE_ROOT
007 marketplace_cache Phase 40.12.1 marketplace_analysis_cache
008 harvester Phase 40.13 +is_suggestion, +source_query, +auto_harvest
009 pinned_notes Phase 41.8 pinned_notes
010 project_phases Phase 44 project_phases
011 activity_log Phase 44.1 activity_log
012 performance_indexes Perf Audit +idx_activity_log_actor, +idx_chat_msg_role_ts
013 timeline_timestamps Phase 47 +timestamp on worker messages
014 timeline_events Phase 47 timeline_events table
015 e2ee_chat Phase 45.3 +encrypted column on chat_messages
016 recovery_keys Phase 45.4 recovery_keys table
017 github_links Phase 49.3 github_links table (webhook bindings)
018 github_events Phase 49.3.1 github_events table (event log for the UI feed)
019 trial_credits Phase 50.1 +users.trial_granted_at, +projects.trial_mode, +projects.trial_tokens_remaining
020 subscriptions Phase 51 subscriptions table (plan, stripe_customer_id, status, feature_flags) + stripe_events idempotency log
021 invites Phase 52.1 invites table (closed beta enrollment) — code, created_by, used_by, parent_invite_code, status, note
022 ephemeral_tokens issue #27 ephemeral_tokens table — persistent OAuth state / password reset / email verification, replacing the in-memory Map (fixes invalid_state on master restart)
023 project_issues Phase 53.14 / issue #53 project_issues table (composite PK project_name+id, JSON labels/activity) — moves issues storage from issues/issues.json into SQLite SSOT, JSON becomes a derived mirror; eliminates text-merge drift on deploy
024 context_exports Phase 56.4 + 56.5 / issues #100 + #101 export_audit_log (id PK, project_name, owner_id, exported_at, sections JSON, findings_critical/high/medium/low INT, bytes) + export_preferences (project_name PK, always_include_emails BOOL, auto_redact_critical BOOL DEFAULT 1, notify_on_export BOOL DEFAULT 0, updated_at) — per-project audit history for Project Context Export + per-project policy toggles; alert at ≥3 exports/24h when notify_on_export = true
025 onboarding_progress Phase 54.1 / issue #56 onboarding_progress (chat_id PK, JSON state per 5 steps, completed_count derived, started_at/completed_at/dismissed_at, source web|cli) — derived cache over the activity_log event stream; SSOT = events with event_type LIKE 'onboarding_%'; recordEvent idempotent, dismiss/replay non-destructive
026 platform_audit_log Phase 57 / Sentinel #103 platform_audit_log (id PK AUTOINCREMENT, ts, user_chat_id, user_email, action, key_name, ip, result, user_agent) — append-only audit trail for super-admin Platform Settings UI mutations on vault secrets. 3 indexes: idx_platform_audit_ts/user/key. No UPDATE/DELETE — read-only via platformAuditQueries.recent + lastRotated
027 auth_events issue #130 auth_events (id PK AUTOINCREMENT, ts, event, provider, user_id, email, ip, user_agent, result, meta JSON) — append-only IP + auth event log. Events: signup/login/oauth_login/magic_link/device_code_approve. Results: success/failed/rate_limited/invalid_invite/already_registered/2fa_required/invalid_token. 4 indexes: ts/user_id/ip/event+result
028 managed_containers Phase 60 #135/#136 managed_containers (id TEXT PK (docker container name), user_id, status (provisioning|ready|paused|suspended|deleted), server_ip, internal_port INT, claude_authed INT, github_authed INT, created_at, last_active) — 1-per-user Docker container registry for Standard Cloud. managedContainerQueries.findByUser/insert/updateStatus/setAuthFlag/touch/listActive in shared/db.ts. Endpoints: POST /api/crm/cloud/provision, GET /status, POST /deprovision in shared/routes/cloud.ts
029 cloud_waitlist Phase 60 #134 cloud_waitlist (id INT PK AUTOINCREMENT, user_id TEXT UNIQUE, email, position INT, status (waiting|invited|activated|declined), notes, joined_at, invited_at, activated_at) — Early Access waitlist for Standard Cloud.
030 translation_feedback Phase 59.4 #122 translation_feedback (id INT PK AUTOINCREMENT, locale, message_id, original, current_translation, suggestion, user_id, status (pending|accepted|rejected), created_at, reviewed_at).
031 token_usage_log Phase 63 #148 token_usage_log (id INT PK AUTOINCREMENT, project_name, owner_id, worker_id, input_tokens, output_tokens, cache_tokens, total_tokens, created_at INT) — Per-request Claude token usage. Populated by claude-runner.ts via /api/internal/usage/log.
032 arc_help_usage Phase 61 #147 arc_help_usage (user_id, date PK composite) — Daily rate-limit counter for Arc Help (30 msg/day).
033 arc_help_messages Phase 61 #153 arc_help_messages (id PK AUTOINCREMENT, user_id, role, text, sources JSON, created_at) — Persistent Arc Help chat history per user (last 60).
034 skills_owner_project #157 ALTER TABLE skills_global ADD COLUMN owner_project TEXT — NULL=global, non-null=owned by project.
035 password_version Phase 64 #174 ALTER TABLE users ADD COLUMN password_version INTEGER DEFAULT 0 — incremented on password change; JWT pv claim validated by crmAuthMiddleware to invalidate old tokens.
036 issue_assignee #187 / 2026-05-23 ALTER TABLE project_issues ADD COLUMN assignee TEXT — nullable worker id. Enables arc issue take <id> + in_progress/blocked/deferred extended statuses (validated at app level).
046 issue_draft_and_created_by #327 / 2026-06-03 ALTER TABLE project_issues ADD COLUMN created_by TEXT NOT NULL DEFAULT 'legacy' — audit field "who created it". Lifecycle += draft (app-level union). handleCreateIssue forces draft without assignee; handleUpdateIssue auto-promotes draft→open on assign + rejects close for never-assigned (FR-ISS-101/102/103).
037 waitlist #197 / 2026-05-25 waitlist (id INT PK AUTOINCREMENT, email TEXT UNIQUE, message, status DEFAULT 'pending', created_at, updated_at) — early access request form on login page.
038 plata_billing Phase 65 #202 / 2026-05-26 Adds Plata by mono columns to subscriptions: plata_card_token TEXT, plata_wallet_id TEXT, plata_masked_pan TEXT, next_billing_date TEXT, billing_failures INTEGER DEFAULT 0. Also creates plata_events idempotency table (invoice_id PK, status, processed_at).
039 skill_project_grants #201 / 2026-05-26 skill_project_grants (skill_id INT FK→skills_global.id CASCADE, project_name TEXT, granted_at, PK composite) — many-to-many skill access across projects.
040 drop_stripe #205 / 2026-05-26 DROP stripe_customer_id column + DROP TABLE stripe_events. NB: stripe_subscription_id column survived this migration because SQLite refuses DROP COLUMN on UNIQUE-constrained columns (silently caught by try/catch). See migration 041.
041 rebuild_subscriptions #205 / 2026-05-26 Follow-up to 040 — rebuilds subscriptions table without the orphan stripe_subscription_id column via CREATE TABLE … AS pattern. Idempotent (skipped when column already absent).
042 totp_backup_codes ALTER TABLE users ADD COLUMN totp_backup_codes TEXT — TOTP recovery codes.
043 worker_avatars Phase D #304 / 2026-05-30 worker_avatars (id PK AUTOINCREMENT, project_name, worker_id, file_path, mime_type, created_at; UNIQUE(project_name, worker_id)) — metadata store for uploaded worker avatar images. Files saved to data/worker-avatars/. Serves via GET /api/crm/projects/:name/workers/:id/avatar.
044 worker_templates Phase I #304 / 2026-05-30 worker_templates (id PK AUTOINCREMENT, owner_id, name, description, config JSON, is_public, created_at; UNIQUE(owner_id, name)) — user-saved worker templates. Endpoints: GET/POST /api/crm/workers/templates, DELETE /api/crm/workers/templates/:id.
045 workers_runtime_state Phase F #306 / 2026-05-31 workers_runtime_state (project_name, worker_id, status, status_started_at, current_skill, current_task_id, updated_at; PK(project_name, worker_id), idx on updated_at) — live runtime state per worker. Written by handleSetActiveRole (single active role per project invariant). Read by GET /api/crm/projects/:name/workers/:id/runtime with staleness fallback: status='working' + tmux dead + updated_at > 10 min → coerced to idle (crash detection).
049 embeddings Phase 71.2 #359 / 2026-06-04 embeddings (id PK AUTOINCREMENT, project, doc_type wiki/issue/skill, doc_id, chunk_ix, text, created_at) + embeddings_vec (vec0 virtual table, embedding FLOAT[1024]). Split: vec0 can't hold metadata efficiently; LIST/DELETE workflows run as plain SQL. sqlite-vec extension auto-loaded in initDb before migration runs.
050 drop_notebook_id Phase 71.8 #365 / 2026-06-05 ALTER TABLE projects DROP COLUMN notebook_id. Decommissions NotebookLM bridge schema field; semantic search now runs through self-hosted RAG (Cohere + sqlite-vec). Idempotent via PRAGMA table_info check so fresh DBs with current 001 schema don't trip on missing column.
051 voice_usage Phase 62.4 #373 / 2026-06-05 voice_usage_log (user_id TEXT, date TEXT YYYY-MM-DD, seconds_used INTEGER DEFAULT 0, PRIMARY KEY(user_id, date)) — daily voice transcription quota counter, mirrors migration 032 arc_help_usage shape. Approximate seconds (estimated from upload byte size assuming ~32 kbps voice codec); compared against soft 60 min/day cap in handleVoiceTranscribe (shared/routes/voice.ts) before each transcribe to keep one user from monopolising the shared whisper-server worker pool on Contabo.
052 transcripts Phase 73.1 #377 / 2026-06-05 transcripts (id PK AUTOINCREMENT, project TEXT, owner_id TEXT, source_filename TEXT, source_bytes INTEGER, source_kind TEXT CHECK(audio|video), duration_seconds INTEGER, transcript_text TEXT, frames_json TEXT, summary_json TEXT, embed_to_rag INTEGER DEFAULT 1, status TEXT DEFAULT 'queued', error TEXT, created_at TEXT DEFAULT datetime('now'), completed_at TEXT) — one row per uploaded meeting file. frames_json = JSON array of {ts_ms, description} from Claude vision (Phase 73.4). summary_json = {tldr, key_points, action_items, decisions, topics, model, generated_at} from Claude Sonnet (Phase 73.5). Source file deleted from disk once status='done' (CEO decision D4). Indexes on (project, created_at DESC) and (owner_id, created_at DESC).
056 skill_usage #319 / 2026-06-07 skill_usage (skill_name TEXT, project_name TEXT DEFAULT '_global_', trigger_count INTEGER DEFAULT 0, install_count INTEGER DEFAULT 0, session_count INTEGER DEFAULT 0, last_triggered_at TEXT, updated_at TEXT, PRIMARY KEY(skill_name, project_name)) — cumulative counters per skill per project. trigger_count increments each time context router injects the skill; install_count on save/create; session_count on unique session appearance. Used by GET /api/crm/projects/:name/skills/usage.
057 notes Phase 78.1 #395 / 2026-06-07 4 tables: notes (id PK, project_name, title, description, created_by, created_at, updated_at) — a note as a container of sources. note_sources (id PK, note_id FK→notes, source_type CHECK(video|audio|youtube|web|pdf|docx|txt|image), filename, file_path, file_size, url, title, content_text, duration_seconds, status CHECK(queued|processing|done|error), error, processed_at, created_at) — one source per row, content_text = extracted text for RAG. note_issue_links (note_id FK, issue_id, project_name, linked_at, PK(note_id,issue_id)) — note↔issue link. note_chats (id PK, note_id FK→notes, role CHECK(user|assistant), content, created_at) — chat history with the note. Indexes: notes(project_name,created_at DESC), note_sources(note_id), note_sources(status), note_issue_links(project_name,issue_id), note_chats(note_id,created_at).
053 transcript_jobs Phase 73.1 #377 / 2026-06-05 transcript_jobs (job_id TEXT PK, transcript_id INTEGER, status TEXT DEFAULT 'queued', progress_pct INTEGER DEFAULT 0, step_label TEXT, error TEXT, started_at TEXT DEFAULT datetime('now'), updated_at TEXT DEFAULT datetime('now')) — high-churn progress tracker for the background worker. Isolated from transcripts so frequent UPDATEs (every few seconds during ffmpeg/whisper) don't churn the main table indexes. SSE endpoint streams this table at 1s poll. Status lifecycle: queued → extracting_audio → transcribing → (video: extracting_frames → frames_extracted → vision_analyzing → vision_analyzed) → summarizing → summarized → embedding → done (or failed at any step). Index on (status, updated_at).

Relationships (Foreign Keys)

All CASCADE — deleting a skill removes all of its forks, history, PRs and benchmarks.

ER diagram (textual)

users ─1:N──> projects
            ├── chat_messages
            ├── pinned_notes
            ├── project_phases
            └── skills_project_forks ──> skills_global
                                          ├── skill_evolution_logs
                                          ├── skill_update_requests
                                          │    └── skill_benchmarks
                                          └── (standalone tables)
                                               ├── activity_log
                                               ├── system_configs
                                               └── marketplace_analysis_cache

| 029 | cloud_waitlist | Phase 60 #134 | cloud_waitlist (id INT PK AUTOINCREMENT, user_id TEXT UNIQUE, email, position INT, status (waiting|invited|activated|declined), notes, joined_at, invited_at, activated_at) — Early Access waitlist for Standard Cloud. cloudWaitlistQueries.join/findByUser/list/invite/activate/decline/stats in shared/db.ts. Endpoints: POST /api/crm/cloud/waitlist (join), GET /waitlist/status (user own), GET /waitlist (admin list), POST /waitlist/invite (admin activate + upgrades plan to cloud) | | 030 | translation_feedback | Phase 59.4 #122 | translation_feedback (id INT PK AUTOINCREMENT, locale TEXT, message_id TEXT, original TEXT, current_translation TEXT, suggestion TEXT, user_id TEXT, status (pending|accepted|rejected), created_at, reviewed_at) — Per-locale translation quality feedback. Admin review dashboard at /api/crm/translations/feedback (list/stats/accept/reject). | | 031 | token_usage_log | Phase 63 #148 | token_usage_log (id INT PK AUTOINCREMENT, project_name TEXT NOT NULL, owner_id TEXT NOT NULL, worker_id TEXT, input_tokens INT DEFAULT 0, output_tokens INT DEFAULT 0, cache_tokens INT DEFAULT 0, total_tokens INT DEFAULT 0, created_at INT DEFAULT (unixepoch())) — Per-request Claude token usage log. 3 indexes: owner_id, project_name, created_at. Populated by child-bot/claude-runner.ts via POST /api/internal/usage/log (fire-and-forget). Queried by GET /api/crm/account/usage → returns { rows: [...last 200], totals: { total, input, output } } for BillingPage + UserDropdown usage card. | | 032 | arc_help_usage | Phase 61 #147 | arc_help_usage (id INT PK AUTOINCREMENT, user_id TEXT NOT NULL, date TEXT NOT NULL (YYYY-MM-DD), count INT NOT NULL DEFAULT 0, PRIMARY KEY (user_id, date)) — Daily rate-limit counter for in-app AI Help chat (30 messages/day). Queried by GET /api/crm/help/usage; updated on POST /api/crm/help/chat. | | 033 | arc_help_messages | Phase 61 #153 | arc_help_messages (id INT PK AUTOINCREMENT, user_id TEXT NOT NULL, role TEXT NOT NULL CHECK(user|assistant), text TEXT NOT NULL, sources TEXT (JSON array, nullable), created_at TEXT DEFAULT datetime('now')) — Persistent Arc Help chat history per user. INDEX on (user_id, created_at DESC). arcHelpQueries.save/history(limit=60)/clearHistory in shared/db.ts. GET /api/crm/help/history returns last 60 messages (oldest-first). DELETE /api/crm/help/history clears all for user. Frontend loads on mount. | | 034 | skills_owner_project | #157 | ALTER TABLE skills_global ADD COLUMN owner_project TEXT DEFAULT NULL — NULL=global/marketplace (visible to all), non-null=owned by that project (filtered in listForProject). Backfill: skills with non-generic category (odoo, python, etc.) get owner_project=category. Generic categories: general/frontend/backend/devops/security/testing/database/api/mobile. DB skills now injected into Claude prompts via routeContextFromDb() in child-bot (#158). |

Migration 064 — token_usage_log.model (#562 S5)

Nullable model TEXT column on token_usage_log: which Claude model served the usage row (threaded from claude-runner through POST /api/internal/usage/log). Powers the per-tier token split in GET /api/crm/analytics/cascade so the spec-gated cascade's cost effect is measurable. Legacy rows stay NULL (unattributed).