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):
- 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, migration0362_knowledge_library_write_outbox, OpenAPI and this runbook. Contains no Rulebase UI change. - UI cutover: PR #11940
(
chidi/eng-3048-knowledge-library-ui-cutover), based on the server branch, retargeted tomainonly 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:
| Route | Rails equivalent |
|---|---|
POST /knowledge-library/documents | KnowledgeBaseDocumentsController#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/sources | KnowledgeBaseSourcesController#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_typeinclusion, manual/URL presence rules, section must belong to the tenant, tags normalized (trimmed, blanks dropped, unique). pg_search_documentsrow refreshed on create/update and removed on discard.- Chunk rows deleted and
chunks_synced_atcleared 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_countandknowledge_base_sources.knowledge_base_documents_countrecounted the same waycounter_culturedoes (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-memoryupdated/discardedevents 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(prefixconnection) 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 = pendingand hands off toknowledgeBaseSyncQueue(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_tasksand nullifiesknowledge_base_documents.knowledge_base_source_idbefore deleting the row. - Sync re-enqueues through the same pipeline; Intercom is marked
pendingfirst. 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_effectsin 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
failedwith 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-deliverspending/failedrows older than 60s, oldest first, 500 per run. Each attempt is recorded.- Delivery is deterministic: sync and chunk jobs use the ENG-2940
knowledgeBaseSyncJobOptions/knowledgeBaseChunksJobOptionsjob ids (deduplicated per document/source, finished jobs replaced); Intercom jobs usejobId = intercom_kb_source_kl_effect_<effect id>plus the existing per-source deduplication; organization-memory jobs usejobId = org-memory-kl-effect-<effect id>. Re-delivering a row after a crash betweenqueue.addand the status update therefore cannot enqueue a second job, and adeliveredrow is never re-sent. - Creates accept an
Idempotency-Keyheader. API2 stores(api2 organization, operation, key, sha256(body), resource id)inapi2.knowledge_library_write_requestsin 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 422idempotency_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.
- Server-first PR #11935 merges alone. It adds the routes, the outbox,
migration
0362_knowledge_library_write_outboxand the maintenance sweep. Nothing calls the routes yet; Rails keeps serving the UI. Rollback: revert the PR (the two newapi2.*tables are unused and can stay or be dropped in a follow-up migration). - API2 readiness criteria (both regions, after the
release-produpdate containing #11935 has deployed on Flightcontrol):GET https://<api2 host>/openapi.json(public) listspaths["/knowledge-library/documents"].postwith anidempotency-keyheader parameter and the eight write operations above. This pins the exact revision: the parameter only exists on or after #11935’s head.drizzle.__drizzle_migrationscontains0362_knowledge_library_write_outboxandapi2.knowledge_library_write_effectsexists.- The worker logs
deliver-knowledge-library-write-effectssweeps. - 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
deliveredand the document shows chunks. No customer traffic reaches the routes before step 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 tomain, 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 nextrelease-produpdate; because API2 is already serving the routes there is no window in which the UI calls unavailable routes. - 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.