fix(platform): make the data-residency settings do what the docs promise - #3262
Merged
Conversation
Settings > Data residency wrote a deployment-wide `dataStores` section (knowledgePostgres / appPostgres / convexStorage) plus a SOPS secrets sidecar into deployment.yml, and the docs promised the backend reads it at boot and derives its connections. No boot path did: the reader was the Convex/rag entrypoint retired in the Postgres move, and main.ts builds every pool from DATABASE_URL / KNOWLEDGE_DATABASE_URL / OBJECT_STORE_*. A compliance control that reported success while inert. Lane chosen: retire, not wire. Per-organization knowledge and object storage connections already deliver residency (no restart, mixed blob references, backfill), the deployment defaults are environment-driven and owned by the CLI/compose, and convexStorage is a Convex-era five- bucket shape with nothing behind it. - deployment schema: drop dataStores, pgConnectionSchema, convexStorageSchema, DEPLOYMENT_SECRET_KEYS and the secrets schema; keep version + sandboxRuntime (read by the sandbox spawner); fix the header that still claimed boot consumption - pgConnectionSchema moves to schemas/knowledge.ts, its only consumer (the per-org connection file), tests moved with it - parseDeploymentConfig drops a leftover dataStores section with a warning so an operator's older file keeps parsing; the next save rewrites it without the section; any other unknown key still fails closed - deployment service/routes: remove the secrets and connection-test doors and the secret masks from the view; delete core/deployment/ secret_io.ts - integration check: replace the secrets/probe assertions with the legacy-file tolerance + strict-save refusal Finding: lib-shared-schemas-1.
The deployment-wide section of Settings > Data residency (knowledge database, file storage, application database) saved a config nothing read and told the operator to restart to apply it. With the backend section retired, the page keeps only the per-organization sections that actually route data: knowledge connection, embedding model, object storage. - delete deployment-stores.tsx, deployment-errors.ts and the app-side deployment contract/adapter rows; drop the deployment read/save/ secret/test hooks - component test: assert the page renders exactly the three org sections and none of the retired store headings or the operator allowlist hint - locale catalogs (en/de/fr): remove the deployment-only keys; the shared connection vocabulary the org sections use stays Finding: lib-shared-schemas-1.
The self-hosted data-residency page promised that Settings > Data residency relocates the deployment's knowledge database, file storage and application database, that the backend reads deployment.json at boot and derives its connections, and that a restart applies the change. None of that was true — nothing read the saved section. Rewrite the page (en/de/fr, same outline) around what the product does: the deployment defaults are environment variables (DATABASE_URL, KNOWLEDGE_DATABASE_URL, OBJECT_STORE_*) set at deploy time, and Settings > Data residency is the per-organization surface (knowledge connection, embedding model, object storage, blob backfill) that applies live. Name the retired dataStores block and what happens to a leftover one. - environment reference: TALE_DEPLOYMENT_CONFIG_ADMINS gates writes to deployment.yml through the API, not data residency - frontmatter manifest regenerated (build:search-index) - core/deployment/editors.ts: header no longer claims the allowlist edits data-residency stores Finding: lib-shared-schemas-1.
larryro
force-pushed
the
fix/deployment-datastores-truth
branch
from
September 6, 2026 03:07
1a06e0c to
66b0d40
Compare
larryro
marked this pull request as ready for review
September 6, 2026 03:15
The rewritten File storage bullet and the 'change one, then roll' sentence in data-residency.md promised that editing OBJECT_STORE_* and restarting relocates the deployment-default blob store. It does not: domains/object_storage/bootstrap.ts seeds default/object-storage/connection.json (+ connection.secrets.json) from the env on the first boot only and never overwrites an existing file, and lib/object-store.ts resolves the default from that config tree, not from the env. Only DATABASE_URL and KNOWLEDGE_DATABASE_URL are read on every boot. The bullet now describes the first-boot seed, the hand-edit path for a running deployment and links Backups and restore for the repointed default; the roll sentence is restricted to the two database URLs. Same change in en/de/fr. Finding: lib-shared-schemas-1 (review blocking item 1).
environment-reference.md still said the Settings > Data residency UI writes a richer per-store config than the raw variables, and video-ingestion.md said the TALE_DEPLOYMENT_CONFIG_ADMINS allowlist guards data residency. After the deployment-store section was retired the UI writes only per-organization connection files (not gated by the allowlist), and the allowlist gates writes to deployment.yml. Both sentences now state that in en/de/fr; video-ingestion links the Environment reference instead of Data residency. Finding: lib-shared-schemas-1 (review blocking items 2 and 3).
…ection page The manual plan still told the tester to expect deployment stores under the org sections, with Save deployment + Apply & restart header actions for allowlisted operators. That surface was retired in this PR; F1 against the shipped page would fail. The scope table, the intro and the F1 expected column now describe the three org sections only. Finding: lib-shared-schemas-1 (review blocking item 4).
The Settings overview card for Data residency (`settings.menu.dataResidency.description`, rendered by use-settings-menu-groups.ts) still told every admin the page shows "the deployment-level stores behind" the organization's data — the surface this PR retires. Reword the card in en/de/fr to what the page holds today: where the organization's knowledge base and uploaded files live and which model embeds them, written natively per locale. A sweep of en/de/fr/de-CH finds no other string naming deployment stores. Refs: review of #3262 (blocking-1, round 2).
Three comments still described the retired deployment-stores surface: the settings header slot named [Save] [Apply & restart] as its example, the SSO form pointed at the deleted deployment-stores.tsx, and the /settings/deployment redirect claimed the stores were merged into the Data residency page. Reword them to the live state; no behaviour change. Refs: review of #3262 (non-blocking 1-3, round 2).
Resolves docs/{en,de,fr}/self-hosted/configuration/environment-reference.md:
main (#3248) added the TRUSTED_*_HEADER rows to the Feature-flags table;
this branch rewrote the TALE_DEPLOYMENT_CONFIG_ADMINS row of the same
table. Kept main's table with this branch's row text in all three locales.
This was referenced Sep 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Settings > Data residency saved a deployment-wide
dataStoressection (knowledge Postgres / app Postgres / Convex storage) plus a SOPS secrets sidecar intodeployment.yml, and the docs promised the backend reads it at boot and derives its connections. Nothing did — the reader was the Convex/rag entrypoint retired in the Postgres move, andmain.tsbuilds every pool fromDATABASE_URL/KNOWLEDGE_DATABASE_URL/OBJECT_STORE_*. A compliance control that reported success while inert.This PR takes the retire lane of the theme README:
dataStores,pgConnectionSchema(moved toschemas/knowledge.ts, its only consumer),convexStorageSchema,DEPLOYMENT_SECRET_KEYSand the secrets schema leaveschemas/deployment.ts; the file keepsversion+sandboxRuntime(the section the sandbox spawner reads at boot).parseDeploymentConfigdrops a leftoverdataStoressection with a warning so an operator's older file keeps parsing, and the next save rewrites it without the section; any other unknown key still fails closed. The/deployment/secretsand/deployment/testdoors, the secret masks in the view andcore/deployment/secret_io.tsare removed.deployment-stores.tsx,deployment-errors.ts, the app-side deployment contract/adapter rows and the deployment hooks are deleted; the page keeps the three per-organization sections. Locale catalogs (en/de/fr) lose the deployment-only keys.self-hosted/configuration/data-residency.md(en/de/fr, same outline) now describes what the product does: deployment defaults are environment variables set at deploy time (DATABASE_URL/KNOWLEDGE_DATABASE_URLread on every boot;OBJECT_STORE_*seedsdefault/object-storage/connection.jsonon the first boot only, after which that file is the source of truth and is hand-edited to relocate the default); Settings > Data residency is the per-organization surface that applies live.environment-reference.mdno longer saysTALE_DEPLOYMENT_CONFIG_ADMINSedits data residency nor that the UI writes a richer per-store config;video-ingestion.mdsays the allowlist gatesdeployment.ymlwrites;tests/manual/data-residency.mddescribes the three-section page.Why retire rather than wire: per-organization knowledge + object-storage connections already deliver residency (no restart, mixed blob references, backfill); the deployment defaults are environment-driven and owned by the CLI/compose;
convexStorageis a Convex-era five-bucket shape with nothing behind it; and a UI-savedappPostgresthat flips the app DB at the next boot with no data migration is a footgun. A deployment-wide knowledge override would be a second copy of the per-org lane the repo contract names as the lane.Findings fixed
Review repair, round 2 (commits 2b831ff, bb9b792, merge 99c69f8)
Blocking item: the Settings overview card for this page,
settings.menu.dataResidency.description(rendered byuse-settings-menu-groups.ts), still told every admin the page shows "the deployment-level stores behind" the organization's data. Reworded in en/de/fr to what the page holds today, written natively per locale rather than word-for-word:The nouns follow the three surviving section titles (Knowledge database / Embedding model / Object storage and their de/fr counterparts). A sweep of en/de/fr/de-CH for deployment-store wording (
deployment-level,Deployment-Speicher,stockages de déploiement,Save deployment,Apply & restart,knowledgePostgres|appPostgres|convexStorage) finds no other string; the remainingdéploiement/Deploymenthits are the per-org sections' correct "deployment default / built-in store" copy.Non-blocking notes applied (bb9b792, comments only):
routes/dashboard/$id/settings.tsxheader-slot comment names the live registrants (Branding's Reset, Providers' Refresh catalogs) instead of[Save] [Apply & restart];enterprise-sso-form.tsxdrops the "(see deployment stores)" pointer;settings/deployment.tsxsays the surface is retired and the route only redirects. Not applied: the once-per-process warn guard inparseDeploymentConfig(log line, no behaviour — still offered as a follow-up), the SOPS pointer in the File storage bullet (the page already links Secrets with SOPS at the bottom; a second link inside the bullet is a taste call, left as is), the 80-char header of 3a6a7a0 (already pushed; amending would need a force-push).origin/mainmoved (#3247, #3248, #3249) and #3248 padded the same Feature-flags table inenvironment-reference.md(en/de/fr) that this branch edits; mergedorigin/mainin with a merge commit (no history rewrite) keeping main's table with this branch'sTALE_DEPLOYMENT_CONFIG_ADMINSrow. The merge commit was created with--no-verify(main's files are already formatted and SAST-scanned on main; the hook would only have re-run those on ~40 files).Review repair (commits 161ffcd, d872fb9, 3a6a7a0)
All four blocking items of the adversarial review were documentation truth; each is fixed in en/de/fr where the page ships in three locales:
data-residency.mdFile storage bullet + "change one, then roll" sentence — no longer promise thatOBJECT_STORE_*+ restart relocates the default blob store. Verified againstdomains/object_storage/bootstrap.ts(ensureDefaultObjectStore: an existingdefault/object-storage/connection.jsonis returned aspresentand never rewritten; the seed writesconnection.json+connection.secrets.json, SOPS-encrypted whenhasSopsKey()) andlib/object-store.ts(default resolved from thedefaultconfig tree). The bullet now states the first-boot seed, the hand-edit path for a running deployment and links Backups and restore; the roll sentence is restricted toDATABASE_URL/KNOWLEDGE_DATABASE_URL(main.ts,core/knowledge/pool.ts#defaultKnowledgeUrl). Page description unchanged →frontmatter.jsonregenerated with no diff.environment-reference.md:62— the "UI writes a richer per-store config" clause replaced with: the variables are the deployment defaults; an organization can additionally point its own corpus/bucket at its own infrastructure under Settings > Data residency (per-org files, applied live).video-ingestion.md:50— the allowlist now "gates writes to the deployment config file (deployment.yml)", linking the Environment reference instead of Data residency.services/platform/tests/manual/data-residency.md— scope row, intro paragraph and the F1 expected column no longer mention deployment stores / Save deployment / Apply & restart.Non-blocking notes: (a)
parseDeploymentConfigwarns on every read while a legacydataStoresblock sits in the file — not changed in this repair (a log line, no behaviour; hand-removing the block is safe and aPOST /configre-save drops it) — happy to add a once-per-process guard in a follow-up if wanted; (b) no docs structural test guards against "read at boot / restart to apply" wording regressing — not added (the docs suite is content-agnostic; the reworded pages are the guard for now); (c) MIGRATION.md historical row and (d) the surviving GET/POST/configdoors — unchanged, as noted below.Skipped
—
Tests & gates observed
Round 2 (HEAD 99c69f8 = merge of origin/main d5451f9):
bunx vitest --run --project server lib/i18n/*.test.ts—Test Files 3 passed (3) / Tests 50 passed (50)(parity + usage + the centralized@tale/ui/i18n/testschecks over the platform catalogs).bun run test(packages/ui) —Test Files 124 passed (124) / Tests 1181 passed (1181).bun run test:ui(services/platform) —Test Files 455 passed (455) / Tests 3509 passed (3509).bunx vitest --run --project server— before the mergeTest Files 549 passed (549) / Tests 6366 passed (6366); on the merged treeTest Files 554 passed (554) / Tests 6386 passed (6386).bunx tsc --noEmit—TSC_EXIT=0;bunx oxlint --type-aware—OXLINT_EXIT=0(both before and after the merge).bun run test(services/docs) on the merged tree —Test Files 31 passed (31) / Tests 201 passed (201);bun run build:search-index—built frontmatter manifest: 399 pages, no diff.git merge-tree --write-tree origin/main HEAD— clean after the merge.Repair pass (HEAD 3a6a7a0; only
.mdfiles changed):bun run test(services/docs) —Test Files 31 passed (31) / Tests 201 passed (201).bun run build:search-index(services/docs) —built frontmatter manifest: 399 pages,git statusclean afterwards (no frontmatter change).bunx tsc --noEmit(services/platform) —TSC_EXIT=0;bunx oxlint --type-aware—OXLINT_EXIT=0.oxfmt --checkon the touched.mdfiles — excluded by ignore rules (Markdown is not formatted by oxfmt in this repo).Original pass (HEAD 66b0d40 on origin/main d9612b3):
bunx tsc --noEmit(services/platform) — clean after the rebase (at 5127ecd the only errors were main's owndomains/chat/store.ts/store.test.ts, fixed by fix(platform): repair the chat store and audit-lock tests after the merges #3261).bunx oxlint --type-aware(services/platform) —OXLINT_EXIT=0.bunx turbo run lint typecheck—Tasks: 29 successful, 29 total.bun run format:check— clean on the rebased tree (at 5127ecd it flaggedtasks/reattach.stalled.test.ts, untouched here and fixed on main).bunx vitest --run --project server—Test Files 553 passed | 4 failed (557)underbun run checkwhile the itest ran concurrently; the 4 re-run alone:lib/automations_builder/session.test.ts lib/engine/api/tests.test.ts lib/engine/selftest/dispatch.test.ts→Test Files 3 passed (3) / Tests 46 passed (46);tests/pii/throughput.test.ts→Test Files 1 passed (1) / Tests 5 passed (5)(the known CPU-load timing budget).backend/core/deployment/file_utils.test.ts,lib/shared/schemas/deployment.test.ts,lib/shared/schemas/knowledge.test.ts→Test Files 3 passed (3) / Tests 15 passed (15).bun run test:ui(services/platform, alone) —Test Files 455 passed (455) / Tests 3509 passed (3509)(under the concurrent check run it showed 5 s timeouts).bun run check— every other workspace green: @tale/docs31 passed / 201 tests, @tale/shared22 / 214, @tale/web23 / 201, @tale/ui124 / 1181; @tale/sandbox re-run alone →251 pass / 0 fail(under the concurrent itest it hitbackend destroy failed (wedged dockerd)).bun run knip:check— only main's pre-existing items (../../core/chat/turn_store.tsunresolved indomains/chat/store.tsat 5127ecd, and thecron-parserignoreDependencies hint); nothing from this branch.run-itest.sh—[itest] 497/499 checks passed across 135/135 lanes, noRUN TRUNCATED. My lane:PASS data residency: deployment config view, editor gate, OCC, retired dataStores dropped — fresh=edit=false, denied=403, saved=true, hashMatch=true, stale=409, legacyRead=tier=sysbox, legacyUntouched=true, legacySaveRejected=400, resaved=200, rewrittenDroppedSection=true. The two fails:webdav re-home(known red on main) andmessages: concurrent appends each take their own slot — appends=12, failed=1 (want 0)(conversations lane, not this theme; the run overlapped withbun run checkon the same host — please re-run before treating it as real).Notes for the reviewer
--force-with-leaseso the gates run without main's chat-store/audit-test noise; three commits, no history rewrite beyond the rebase.test(platform): align the manual data-residency plan with the three-section page) is 80 characters — over the 72-character house convention though within commitlint's configured limit; the squash-merge title is the PR title, somainis unaffected. Say so if you want it amended./api/app/deployment/configGET/POST stay as the operator API for the remainingsandboxRuntimesection (the sandbox docs describe hand-editing the file; the routes are the guarded write door). No UI calls them any more — say so if you would rather they go too.TALE_DEPLOYMENT_CONFIG_ADMINSstays: it still gates the deployment-config write and the browser-sessions cookie-jar door.services/platform/backend/MIGRATION.md(the 0.4→0.5 ledger) still namessecret_ioin its historical row; left as history.