Skip to Content
Internal docs are powered by Nextra Docs Theme.
SystemsPlatformAPI2Rails to API2 migrationRails table ownership

Rails-owned table ownership map

rulebase-api2/src/db/rails-schema.ts declares 166 public.* tables that Rails owns. Every declaration is a persistence dependency that keeps API2 coupled to Rails DDL and, for many tables, to Rails write paths. This page classifies each table and sequences the work to make API2’s persistence independent.

Methodology

For each declared table we measured, on origin/main (2026-09-09):

  • API2 files — files importing the table’s rails* const from src/db/rails-schema.ts, plus files with raw SQL touching public.<table>. Note that several api2 files import api2-native tables aliased as railsX from src/db/schema.ts (e.g. dashboards as railsDashboards); those are not public-schema dependencies and were excluded.
  • API2 ops — Drizzle insert/update/delete calls and raw INSERT INTO/UPDATE/DELETE against the table.
  • Rails refs / Rails write files — files under rulebase-api/app|lib referencing the classified model class, and files containing Model.create/insert_all/upsert/find_or_create write calls.

Static analysis cannot see callbacks, acts_as_tenant associations, after_commit hooks, or writes issued through save/update on fetched records, so Rails write evidence is a floor, not a ceiling. Treat “Rails-coupled” (a live model exists) as dual-written until the write paths are verified — the safe default is to assume both services can write.

Classes

ClassMeaning
Read-only projectionAPI2 only reads; Rails is the sole writer.
API2 writerAPI2 writes; no Rails model or write path found.
Rails writerAPI2 does not write; Rails writes (API2 may read).
Dual-writtenBoth services have write paths — or API2 writes while a live Rails model couples the table (callbacks/associations make the table unsafe to move until verified).
Retirement candidateDead table or dead declaration: neither service needs it, or API2’s copy already owns the data.
FrameworkGem/framework-owned (flipper_*, active_storage_*, pg_search_documents); never migrates.

Summary

ClassTables
Dual-written (incl. Rails-coupled api2 writes)107
Rails writer (API2 read-only)44
Retired in this PR (deprecated_dashboard_*)8
Framework5
Rails-owned live catalog (dashboard_widget_types, dashboard_widget_type_groups)2
Total declared166

After this PR the declared surface drops to 156 tables.

Domain ranking: traffic Ă— coupling

Traffic tier reflects request-path reads and ingest-write volume (structural signals: api2 file coupling, write ops, and the GoodJob traffic evidence in goodjob-traffic); each work package must confirm actual production counts (pg_stat_user_tables, GoodJob execution counts) before cutover. Coupling = dual-write or live-Rails-model surface.

RankDomainTablesAPI2 writersDual-writtenTierWhy
1Conversations (conversations, conversation_parts, part details, threads, assignments, tags)292424T0 — ingest pathHighest volume; every provider sync and UI read hits these.
2QA & evaluations (qa_agent_evaluations, scorecard results, evaluation batches/contests, scorecards)322121T0 — QA pipelineFan-out per conversation × criterion; heaviest per-row write volume.
3Identity & access (organizations, accounts, memberships, roles, workspaces, api_keys)131111T0 — every requestRead on nearly every authenticated call; writes are rare but auth-critical.
4Integrations & sync (organization_data_sources, data_sources, zendesk_*, jira_*, slack_*)161212T0 — sync pathProvider webhooks/polls write continuously.
5Workforce (employees, teams, groups, partners, work_schedules)966T1Directory reads everywhere; writes are admin/sync driven.
6Knowledge base (knowledge_base_*)444T1API2 sync worker is primary writer; Rails reads.
7Customers & complaints (customer_*, complaint*)1044T1Moderate volume.
8Sheets & tests (sheets, sheet_*, test_*, cell_*)777T1API2 writes; Rails models remain for sheet surface.
9CRM & clients (clients, client_integration_profiles, disputes)333T1Compliance-adjacent writes.
10Compliance (compliance_*, audit_events, frameworks)711T2Mostly Rails-written reporting.
11SLA & schedules (sla_policies, conditions, targets)300T2Config; Rails-written.
12Notifications & digests311T2Low volume.
13Billing (credits)111T2Low volume, sensitive.
14Framework (flipper_*, active_storage_*, pg_search_documents)530T0 I/O but infra-ownedNever migrate; API2’s flipper/active-storage reads are intentional shared-infra use.
15Dashboards (public.dashboard_*)1000DeadRetired/kept-catalog — see below.

Retired in this PR: public.dashboard_* instance tables

Evidence for retirement:

  • Rails deleted all custom-dashboard models, controllers, blueprints, and factories in #7095 (2026-05-30). Only the widget-type catalog (dashboard_widget_types, dashboard_widget_type_groups) remains live in Rails routes.
  • API2 dashboard code uses only the api2.dashboard_* copies created and backfilled by drizzle migration 0060. The railsDashboard* names in src/routes/dashboards.ts and src/lib/homepage-dashboard.ts alias api2-native tables, not the public ones.
  • Zero api2 imports of the public-table consts; zero raw SQL touches public.dashboard_*; all FKs are intra-family (verified live — no FK from outside the set points into it).

Shipped here:

  1. rulebase-api migration 20260909225243_deprecate_legacy_dashboard_tables renames the 8 dead instance tables to deprecated_dashboard_* — a forward-only, non-destructive, instantly reversible cut (rename back on rollback). safety_assured is justified in-file: no code references the tables, so nothing live can break; any hidden consumer fails fast instead of reading stale data.
  2. All 10 dead rails* declarations removed from src/db/rails-schema.ts (the two catalog consts too — API2 never used them; the catalog stays Rails-owned).
  3. scripts/verify-rails-table-retirement.ts (pnpm db:verify-table-retirement(:local|:infisical)) — read-only audit that a table set is dead in code and in the expected physical state; reusable for every package below.

No runtime flag is needed: the change removes no live behavior. The flag equivalent is the staged rollout itself — rename now, observe, drop later. Rollback: db:rollback (or a follow-up rename migration per the forward-only rule). Follow-up: after a ≥2-week bake with no Sentry noise, drop deprecated_dashboard_* in a second migration; re-run the verifier with --expect=absent first.

Known blind spot (pre-merge sign-off): confirm no BI/warehouse or data-sync job reads public.dashboard_* — external consumers are not visible in the repo.

Work packages

Each package is a sequence of PR-sized steps following the existing package convention: API2 takes writes behind a PostHog rollout flag (fail-closed) or Flipper kill switch where safe-state-on applies, with an outbound_delivery_claims-style single-winner contract where Rails still initiates. Generic steps per table family:

  1. Verify — production pg_stat + GoodJob counts; run pnpm db:verify-table-retirement / write-path audit on both services.
  2. Single writer — cut one write edge at a time behind a flag; keep dual-write or read-repair during the window.
  3. Backfill/reconcile — a reconciliation job or script; compare counts.
  4. Bake — observe ≥1 week at 100%; then remove the flag and the dead path. Remove the rails* declaration last.

WP-1 — zendesk_webhook_ticket_syncs (smallest true dual-write)

Rails ZendeskWebhook.record_event! inserts on ingest, then ZendeskWebhookTicketSync#handoff_to_api2 hands off synchronously via Api2ZendeskWebhookTicketSyncEnqueuer (no GoodJob since ENG-2506); API2 reads/updates/deletes rows and its recovery sweep re-arms stranded ones. Target: API2 ingest creates and owns the rows; Rails stops inserting.

  • PostHog api2_owns_zendesk_webhook_ticket_syncs (owner: platform; review ≤90 days) gating the Rails insert; API2-side creates the row inside its enqueue path.
  • Rollback: flag off → Rails resumes creating rows; API2 already tolerates pre-existing rows.

WP-2 — QA result persistence (qa_agent_evaluations, qa_agent_evaluation_scorecard_results, qa_agent_evaluation_scorecard_criterion_results, ..._check_results, ..._check_kb_citations, ..._kb_citations, qa_evaluation_requests, evaluation_scorecard_criterion_result_comments)

API2 is the primary writer (QA agent pipeline); Rails keeps models for reads/serialization. Highest per-row volume after conversations.

  • Per family: enumerate Rails write sites (mostly backfill/contest paths), move each behind its own flag (api2_owns_qa_*); Rails models convert to read-only.
  • Verify row parity per org before deleting any Rails write path.

WP-3 — Conversation ingest persistence (conversation_parts + detail tables, conversation_threads/thread_items, conversation_field_values, conversation_part_attachments, assignments, conversation_teams, tags, conversation_tags)

Highest-traffic domain; most dual-write edges. Split per part-type (attachments, notes, chat details, email details, call details, assignments) — each is its own flag-gated PR inside the package.

WP-4 — Provider config & catalog (organization_data_sources, data_sources, zendesk_brands, integrations, connections, jira_* catalog)

Mixed admin-write (Rails UI) + sync-write (API2). Requires an explicit write-arbitration decision per table before cutover — flag this to product if sync-time upserts conflict with admin edits.

WP-5 — Identity read graph (organizations, accounts, memberships, roles, permissions, membership_scopes, workspaces, api_keys, invitations, sessions, workos_* events)

Formalize as Rails-owned read-only projections: API2 never writes (the current raw writes are auth/session flows being ported — finish those ports under the native-session packages, then freeze). Do not move the tables.

WP-6 — Long tail (knowledge_base_*, sheets/test_*/cell_*, employees/teams/groups/partners, customers/complaints, clients, disputes, audit_events, SLA tables, credits, digests)

Same recipe per family, ordered by write volume measured in WP-1..4.

WP-7 — Never migrate

flipper_features/flipper_gates (shared kill-switch store, read via raw SQL in src/lib/flipper.ts), pg_search_documents (pg_search gem), active_storage_blobs/attachments (ActiveStorage), data_migrations, schema_migrations, ar_internal_metadata, versions, and dashboard_widget_types/dashboard_widget_type_groups (live Rails catalog).

Appendix: per-table classification

Rails refs/writes are heuristic file counts — a nonzero count means “verify before touching”, zero means “no evidence found”. retired-this-pr rows are renamed to deprecated_* by this PR’s migration.

TableDomainClassAPI2 filesAPI2 opsRails write filesRails refs
accountsidentity & accessDual-written61raw-write469
active_storage_attachmentsframeworkFramework24delete,insert,raw-write00
active_storage_blobsframeworkFramework22delete,insert,raw-write00
agent_integration_profilesagents & botsDual-written (Rails: model/reads only)18insert,raw-write,update016
agent_performance_report_citationsagents & botsDual-written (Rails: model/reads only)3raw-write01
agent_performance_reportsagents & botsDual-written7raw-write215
api_keysidentity & accessDual-written (Rails: model/reads only)3raw-write,update04
assignment_queue_membershipsotherDual-written (Rails: model/reads only)4insert,update05
assignment_queuesotherDual-written (Rails: model/reads only)8insert010
audit_eventscomplianceDual-written7insert,raw-write26
call_script_guideline_stepsotherRails writer1-02
call_script_guidelinesotherRails writer1-04
cell_reasoning_citationssheets & testsDual-written (Rails: model/reads only)2delete,insert02
client_integration_profilesCRM & clientsDual-written (Rails: model/reads only)2insert02
clientsCRM & clientsDual-written49insert,raw-write,update1493
companiesotherDual-written43insert,raw-write,update620
company_field_valuesotherDual-written (Rails: model/reads only)6delete,insert,raw-write,update02
company_fieldsotherDual-written (Rails: model/reads only)10insert,raw-write08
complaints_jira_request_typescustomers & complaintsRails writer1-01
compliance_alertscomplianceRails writer2-25
compliance_casescomplianceRails writer2-28
compliance_criterion_groupscomplianceRails writer5-29
compliance_rule_documentscomplianceRails writer3-18
compliance_testscomplianceRails writer6-09
connection_logsintegrations & syncDual-written (Rails: model/reads only)2raw-write03
conversation_agentsconversationsDual-written36insert,raw-write116
conversation_assignmentsconversationsDual-written3insert,update212
conversation_customer_sentiment_summariesconversationsRails writer8-18
conversation_customersconversationsDual-written (Rails: model/reads only)31insert,raw-write03
conversation_field_valuesconversationsDual-written28raw-write15
conversation_fieldsconversationsDual-written25insert,raw-write,update318
conversation_filtersconversationsRails writer2-02
conversation_groupsconversationsRails writer5-01
conversation_part_assignmentsconversationsDual-written (Rails: model/reads only)11raw-write,update06
conversation_part_attachmentsconversationsDual-written (Rails: model/reads only)13insert,raw-write,update06
conversation_part_attribute_updated_by_adminsconversationsDual-written (Rails: model/reads only)10raw-write06
conversation_part_call_detail_transcript_messagesconversationsDual-written (Rails: model/reads only)22raw-write013
conversation_part_call_detailsconversationsDual-written (Rails: model/reads only)31insert,raw-write028
conversation_part_chat_detailsconversationsDual-written (Rails: model/reads only)23insert,raw-write,update012
conversation_part_email_detailsconversationsDual-written (Rails: model/reads only)22insert,raw-write,update09
conversation_part_notesconversationsDual-written (Rails: model/reads only)19insert,raw-write,update07
conversation_partnersconversationsDual-written11insert,raw-write13
conversation_partsconversationsDual-written (Rails: model/reads only)73insert,raw-write,update037
conversation_resolution_assignment_rule_assignee_accountsconversationsRails writer2-02
conversation_resolution_assignment_rule_assignee_rolesconversationsDual-written (Rails: model/reads only)3raw-write02
conversation_resolution_assignment_rule_conditionsconversationsDual-written (Rails: model/reads only)4raw-write04
conversation_resolution_assignment_rulesconversationsDual-written (Rails: model/reads only)4raw-write05
conversation_tagsconversationsDual-written (Rails: model/reads only)6delete,insert,update06
conversation_teamsconversationsDual-written13insert,raw-write212
conversation_thread_itemsconversationsDual-written (Rails: model/reads only)22delete,insert,raw-write06
conversation_threadsconversationsDual-written15delete,insert,raw-write,update29
conversationsconversationsDual-written215insert,raw-write,update9281
creditsbillingDual-written7raw-write113
customer_accountscustomers & complaintsRails writer1-012
customer_companiescustomers & complaintsDual-written (Rails: model/reads only)14delete,insert,raw-write,update01
customer_complaint_assignee_accountscustomers & complaintsRails writer2-01
customer_complaint_categoriescustomers & complaintsDual-written12raw-write411
customer_complaint_category_keywordscustomers & complaintsDual-written (Rails: model/reads only)4raw-write03
customer_complaintscustomers & complaintsDual-written11raw-write224
customer_fieldscustomers & complaintsRails writer1-08
customer_issue_conversation_fieldscustomers & complaintsRails writer3-01
customer_issuescustomers & complaintsRails writer11-121
dashboard_accessesdashboardsRetired (this PR)0-00
dashboard_alertsdashboardsRetired (this PR)0-00
dashboard_filter_rulesdashboardsRetired (this PR)0-00
dashboard_groupsdashboardsRetired (this PR)0-00
dashboard_preferencesdashboardsRetired (this PR)0-00
dashboard_sectionsdashboardsRetired (this PR)0-00
dashboard_widget_type_groupsdashboardsRails-owned0-08
dashboard_widget_typesdashboardsRails-owned0-095
dashboard_widgetsdashboardsRetired (this PR)1-00
dashboardsdashboardsRetired (this PR)1-02
data_sourcesintegrations & syncDual-written60insert,raw-write553
digest_emailsnotifications & digestsDual-written2raw-write226
disputesCRM & clientsDual-written13insert,raw-write,update217
employee_field_valuesotherDual-written (Rails: model/reads only)5delete,insert,update02
employee_fieldsotherDual-written6insert,update29
employeesworkforceDual-written151insert,raw-write,update15136
evaluation_assignmentsQA & evaluationsDual-written8delete,insert,update29
evaluation_batch_accountsQA & evaluationsDual-written (Rails: model/reads only)1delete,insert01
evaluation_batch_assigneesQA & evaluationsDual-written1delete,insert,update14
evaluation_batch_conversationsQA & evaluationsDual-written5delete,insert,raw-write,update111
evaluation_batchesQA & evaluationsDual-written (Rails: model/reads only)5delete,insert,update016
evaluation_contest_criterion_feedbacksQA & evaluationsDual-written (Rails: model/reads only)7insert,update02
evaluation_contest_feedbacksQA & evaluationsDual-written (Rails: model/reads only)7insert05
evaluation_contest_scorecard_criterion_resultsQA & evaluationsDual-written (Rails: model/reads only)4insert,update02
evaluation_contestsQA & evaluationsDual-written11insert,update115
evaluation_sampling_rule_countersQA & evaluationsDual-written (Rails: model/reads only)4insert,raw-write,update06
evaluation_sampling_rule_targetsQA & evaluationsRails writer3-02
evaluation_sampling_rulesQA & evaluationsRails writer8-015
evaluation_scorecard_criterion_result_commentsQA & evaluationsDual-written (Rails: model/reads only)9raw-write09
flipper_featuresframeworkFramework1-00
flipper_gatesframeworkFramework2-00
group_employeesworkforceDual-written (Rails: model/reads only)11delete,insert09
group_membershipsworkforceRails writer3-02
groupsworkforceDual-written20insert,raw-write,update774
guiding_questionsQA & evaluationsRails writer3-01
invitation_scopesidentity & accessRails writer2-01
invitationsidentity & accessDual-written8raw-write,update110
jira_issue_commentsintegrations & syncDual-written4insert,update12
jira_issue_draftsintegrations & syncRails writer1-15
jira_issue_request_field_valuesintegrations & syncDual-written (Rails: model/reads only)2insert02
jira_issuesintegrations & syncDual-written16insert,update16
jira_request_type_fieldsintegrations & syncDual-written (Rails: model/reads only)6insert02
jira_request_typesintegrations & syncDual-written (Rails: model/reads only)5insert09
jira_service_desksintegrations & syncDual-written2insert24
knowledge_base_crawl_tasksknowledge baseDual-written2insert,update17
knowledge_base_document_scorecard_criteriaQA & evaluationsRails writer1-01
knowledge_base_documentsknowledge baseDual-written25insert,raw-write,update333
knowledge_base_sectionsknowledge baseDual-written4insert,update410
knowledge_base_sourcesknowledge baseDual-written (Rails: model/reads only)10insert,update010
lines_of_businessworkforceRails writer1-04
macrosotherDual-written3raw-write27
membership_scopesidentity & accessDual-written (Rails: model/reads only)9raw-write01
membershipsidentity & accessDual-written59raw-write980
notification_alert_rulesnotifications & digestsRails writer1-02
organization_data_sourcesintegrations & syncDual-written79insert,raw-write,update573
organization_notification_settingsidentity & accessDual-written (Rails: model/reads only)2raw-write02
organization_risk_level_configurationsidentity & accessDual-written8raw-write14
organization_weekly_digest_configsnotifications & digestsRails writer1-48
organizationsidentity & accessDual-written102raw-write,update5155
partnersworkforceDual-written12insert,update323
permissionsidentity & accessDual-written10raw-write115
pg_search_documentsframeworkFramework5delete,insert,raw-write,update00
pinned_conversationsconversationsRails writer1-04
qa_agent_evaluation_scorecard_criterion_check_kb_citationsQA & evaluationsDual-written (Rails: model/reads only)3raw-write02
qa_agent_evaluation_scorecard_criterion_check_resultsQA & evaluationsDual-written (Rails: model/reads only)26delete,insert,raw-write013
qa_agent_evaluation_scorecard_criterion_resultsQA & evaluationsDual-written (Rails: model/reads only)52delete,insert,raw-write,update030
qa_agent_evaluation_scorecard_resultsQA & evaluationsDual-written (Rails: model/reads only)51delete,insert,raw-write,update018
qa_agent_evaluationsQA & evaluationsDual-written91insert,raw-write,update375
qa_evaluation_requestsQA & evaluationsDual-written19raw-write240
qa_evaluationsQA & evaluationsDual-written17insert,raw-write,update124
redaction_rulesotherDual-written (Rails: model/reads only)3raw-write02
role_permissionsidentity & accessDual-written (Rails: model/reads only)9raw-write03
rolesidentity & accessDual-written22raw-write331
scorecard_cap_rulesQA & evaluationsRails writer5-01
scorecard_conditionsQA & evaluationsRails writer7-17
scorecard_criteriaQA & evaluationsDual-written (Rails: model/reads only)50raw-write031
scorecard_criterion_check_conditionsQA & evaluationsRails writer4-02
scorecard_criterion_checksQA & evaluationsRails writer21-015
scorecard_criterion_score_option_justification_optionsQA & evaluationsRails writer25-019
scorecard_criterion_score_optionsQA & evaluationsRails writer35-018
scorecard_groupsQA & evaluationsRails writer6-01
scorecardsQA & evaluationsDual-written40raw-write371
scoring_guidesQA & evaluationsDual-written3raw-write19
sheet_cellssheets & testsDual-written (Rails: model/reads only)5insert,update04
sheet_changessheets & testsDual-written (Rails: model/reads only)2insert04
sheet_columnssheets & testsDual-written (Rails: model/reads only)7insert,update05
sheet_rowssheets & testsDual-written (Rails: model/reads only)4insert,update07
sheet_versionssheets & testsDual-written (Rails: model/reads only)2insert02
sheetssheets & testsDual-written (Rails: model/reads only)14insert,raw-write,update021
sla_policiesSLA & schedulesRails writer8-15
sla_policy_conditionsSLA & schedulesRails writer2-02
sla_policy_targetsSLA & schedulesRails writer4-02
slack_channelsintegrations & syncRails writer3-15
slack_incoming_webhooksintegrations & syncRails writer3-02
slack_messagesintegrations & syncDual-written3insert23
tagsconversationsDual-written6delete,insert414
team_leadsworkforceRails writer16-01
teamsworkforceDual-written48insert,raw-write,update461
test_risk_scenarioscomplianceRails writer0-03
work_schedulesworkforceDual-written (Rails: model/reads only)7raw-write05
workspacesidentity & accessRails writer2-05
zendesk_brandsintegrations & syncDual-written19delete,insert,raw-write,update334
zendesk_webhook_ticket_syncsintegrations & syncDual-written (verified: ZendeskWebhook.record_event! + api2 sync lib)2delete,insert,update03
zendesk_webhooksintegrations & syncRails writer2-016
Last updated on