Skip to Content
Internal docs are powered by Nextra Docs Theme.
SystemsPlatformAPI2Rails to API2 migrationPackagesKnowledge Library Rails client retirement

Knowledge Library Rails client retirement

ENG-3048  removes the remaining Rails client from the Rulebase UI Knowledge Library boundary and moves the document and source mutations those files used to API2. It builds on the ENG-3003 reads (/knowledge-library/documents, /knowledge-library/sources) and the ENG-2940 native sync/chunk primitives in rulebase-api2/src/lib/knowledge-base-sync/.

Ownership: issue ENG-3048 (owner: Chidi), Devin session https://app.devin.ai/sessions/af11e7253fda46b4853f4dc7beeedb6e, independent reviewer Ronald (Iamronaldosunmu). The package ships as two PRs that must land in order (see Rollout):

  1. Server-first: PR #11935  “ENG-3048: remove remaining Knowledge Library Rails client” on branch chidi/eng-3048-remove-knowledge-library-rails-client — API2 routes, durable outbox, migration 0362_knowledge_library_write_outbox, OpenAPI and this runbook. Contains no Rulebase UI change.
  2. UI cutover: PR #11940  (chidi/eng-3048-knowledge-library-ui-cutover), based on the server branch, retargeted to main only after the server revision is live in both regions. Removes the Rails client from the ten boundary files.

Ownership boundary

API2 now owns, under knowledge_base:manage, for the authenticated tenant:

RouteRails equivalent
POST /knowledge-library/documentsKnowledgeBaseDocumentsController#create (also used for duplicate)
PATCH /knowledge-library/documents/{documentId}#update (also used for tag edits)
DELETE /knowledge-library/documents/{documentId}#destroy (discard)
POST /knowledge-library/documents/{documentId}/sync#sync
POST /knowledge-library/sourcesKnowledgeBaseSourcesController#create
PATCH /knowledge-library/sources/{sourceId}#update
DELETE /knowledge-library/sources/{sourceId}#destroy
POST /knowledge-library/sources/{sourceId}/sync#sync

Behavior lives in rulebase-api2/src/lib/knowledge-library-writes/ (documents.ts, sources.ts, organization-data-sources.ts, outbox.ts, errors.ts); src/routes/knowledge-library-writes.ts only decodes prefixed IDs, resolves the Rails organization/actor, applies the permission, maps RecordNotFound/RecordInvalid to 404/422 and composes the write with its outbox. There is no API2-to-Rails HTTP call and no GoodJob bridge.

Rails keeps the Knowledge Library routes for now (other clients, the import task controller, folder mutations and Active Storage image handling are not in this package). The Rulebase UI no longer calls them from the Knowledge Library boundary.

Side-effect inventory (callback-equivalent effects)

Documents:

  • Prefixed ID decoding for document, section and source references; foreign or malformed IDs are 404 and leave rows untouched.
  • Validations reproduce the model: source_type inclusion, manual/URL presence rules, section must belong to the tenant, tags normalized (trimmed, blanks dropped, unique).
  • pg_search_documents row refreshed on create/update and removed on discard.
  • Chunk rows deleted and chunks_synced_at cleared when content changes, so the ENG-2940 chunk worker resyncs; provider documents get a source-identifier resync request instead of a local chunk job.
  • knowledge_base_sections.knowledge_base_documents_count and knowledge_base_sources.knowledge_base_documents_count recounted the same way counter_culture does (discarded rows stay counted; section moves recount both sides).
  • Discard sets discarded_at; documents are never hard-deleted.
  • Memory documents (document_kind = memory) emit organization-memory updated/discarded events with the acting account after the transaction commits, through the API2 organization-memory queue.
  • Manual sync enqueues the ENG-2940 document sync request; replaying it is idempotent because the request is deduplicated per document.

Sources:

  • type, url, organization_data_source_id (prefix connection) resolved within the tenant; provider types require a connection of the matching provider, URL sources require a URL, Intercom sources are unique per connection.
  • Create inserts with sync_status = pending and hands off to knowledgeBaseSyncQueue (URL, Zendesk, Notion, Confluence, Scribe) or the Intercom knowledge-base queue (deduplicated per source, three attempts).
  • Update changes attributes only and does not re-sync, as Rails does.
  • Delete removes dependent knowledge_base_crawl_tasks and nullifies knowledge_base_documents.knowledge_base_source_id before deleting the row.
  • Sync re-enqueues through the same pipeline; Intercom is marked pending first. Duplicate sync requests collapse on the queue deduplication key.

Durable delivery and idempotency

Rails ran these effects from after_commit / perform_later; API2 cannot enqueue to Redis inside a Postgres transaction, so the queue handoff is an outbox rather than a direct post-commit enqueue:

  • Every write commits its effects as rows in api2.knowledge_library_write_effects in the same transaction as the Rails-owned rows (kind ∈ knowledge_base_sync, intercom_source_sync, organization_memory; subject_type/subject_id; the job payload; status ∈ pending/delivered/failed; attempts; last_error). If the write rolls back, so do its effects. Nothing else enqueues these jobs.
  • After commit the route delivers the new rows inline. Delivery is best-effort: a Redis failure marks the row failed with the error and the request still returns 2xx, because the write is durable and will be delivered.
  • deliver-knowledge-library-write-effects (maintenance queue, every minute, sweepKnowledgeLibraryWriteEffects) re-delivers pending/failed rows older than 60s, oldest first, 500 per run. Each attempt is recorded.
  • Delivery is deterministic: sync and chunk jobs use the ENG-2940 knowledgeBaseSyncJobOptions/knowledgeBaseChunksJobOptions job ids (deduplicated per document/source, finished jobs replaced); Intercom jobs use jobId = intercom_kb_source_kl_effect_<effect id> plus the existing per-source deduplication; organization-memory jobs use jobId = org-memory-kl-effect-<effect id>. Re-delivering a row after a crash between queue.add and the status update therefore cannot enqueue a second job, and a delivered row is never re-sent.
  • Creates accept an Idempotency-Key header. API2 stores (api2 organization, operation, key, sha256(body), resource id) in api2.knowledge_library_write_requests in the create transaction; a replay with the same key and body returns the existing document/source (201, no new row, no new effects), the same key with a different body is 422 idempotency_key has already been used with a different request, and keys are scoped per tenant. The UI keeps one key per create intent (useIdempotencyKey) until the server acknowledges it, so retries after a timeout or 5xx never duplicate a document or source. Requests without the header behave as Rails did (each request creates a row).

Failure semantics covered by knowledge-library-writes.db.test.ts (“durable delivery”): commit-then-Redis-failure keeps the row and a failed effect; the sweep retries, records attempts, delivers once Redis is back and is a no-op on delivered rows; Intercom effects stay on the Intercom queue; document and source create replay under one key; cross-tenant key reuse; auth/tenant/validation failures write neither rows nor effects.

UI boundary

The ten ENG-3048 files no longer import ~/lib/rulebase-api-client, the Rails generated schema, VITE_API_BASE_URL, api.rulebase.co, or a wrapper that does. Mutations go through app/features/knowledge-base/api/mutations.ts and types through app/features/knowledge-base/api/types.ts, both derived from the API2 OpenAPI schema. Query keys (["knowledge-base-documents", ...], ["knowledge-base-sources"]), invalidation and navigation after create/update/duplicate are unchanged. The dead import-task poll in import-dropdown-menu.tsx (taskId was never assigned) is removed. The three shared consumers of the Rails document types (knowledge-base-document-multi-select.tsx, coaching attach-document-dialog.tsx, workloads evaluation-mention-list.tsx) now use the feature-local types.

app/features/knowledge-base/__tests__/rails-client-source-gate.test.ts fails if any of the ten files, the feature helpers, or a Knowledge Library module they import regains Rails reachability, and asserts the seven former runtime importers are zero.

Exact source delta: the rulebase-ui direct rulebase-api-client/client importer census drops from 240 source/test files to 233 and from 229 runtime files to 222. Beam stays at 0/0.

Verification

Before merge: rulebase-api2 knowledge-library-writes.db.test.ts (PGlite, org 25/org 41 shapes, real routes and orchestrator, queue edges faked), rulebase-ui knowledge-base tests, API2 typecheck, UI typecheck, UI lint, pnpm openapi:export + pnpm rulebase-api2-openapi:generate with no diff, Prettier and git diff --check.

Rollout: API2 before UI, enforced by PR order

Cloudflare (UI) and Flightcontrol (API2) deploy independently from the same release-prod update, so a single PR cannot order them. The package is therefore split, and the UI cutover PR is only mergeable to main after the server revision is verified live — not on build timing.

  1. Server-first PR #11935 merges alone. It adds the routes, the outbox, migration 0362_knowledge_library_write_outbox and the maintenance sweep. Nothing calls the routes yet; Rails keeps serving the UI. Rollback: revert the PR (the two new api2.* tables are unused and can stay or be dropped in a follow-up migration).
  2. API2 readiness criteria (both regions, after the release-prod update containing #11935 has deployed on Flightcontrol):
    • GET https://<api2 host>/openapi.json (public) lists paths["/knowledge-library/documents"].post with an idempotency-key header parameter and the eight write operations above. This pins the exact revision: the parameter only exists on or after #11935’s head.
    • drizzle.__drizzle_migrations contains 0362_knowledge_library_write_outbox and api2.knowledge_library_write_effects exists.
    • The worker logs deliver-knowledge-library-write-effects sweeps.
    • Canary: one manual document create and one source sync from an internal tenant through the routes (curl with a session cookie), then the effect rows are delivered and the document shows chunks. No customer traffic reaches the routes before step 3.
  3. UI cutover PR #11940 (chidi/eng-3048-knowledge-library-ui-cutover) is opened against the server branch so it cannot merge before #11935; after #11935 merges GitHub retargets it to main, and it is merged only once step 2 is recorded in the PR. It contains the ten boundary files, the feature helpers, the source gate and the seven-importer census reduction. Cloudflare deploys it on the next release-prod update; because API2 is already serving the routes there is no window in which the UI calls unavailable routes.
  4. Rollback of the cutover is a UI code revert (Rails routes still exist, and the UI files return to the Rails client); the API2 routes and outbox stay in place. There is no runtime flag: a default-off flag would require keeping the Rails client in the ten files, which is what ENG-3048 removes, so ordering is enforced by the PR dependency instead.

Evidence gate

Production retirement credit for KnowledgeBaseDocumentsController, KnowledgeBaseSourcesController and ImportKnowledgeBaseDocumentsTasksController requires, in addition to the source delta above, a clean and meaningful first_party_client:rulebase-ui Sentry window after both regional releases of the UI cutover: zero first-party transactions on those controllers over a full business-hours window with confirmed API2 write traffic (delivered outbox rows) in the same period. Source changes alone do not retire the Rails controllers, and this package does not remove them.

Last updated on