Skip to content

fix(platform): make the data-residency settings do what the docs promise - #3262

Merged
larryro merged 10 commits into
mainfrom
fix/deployment-datastores-truth
Sep 6, 2026
Merged

fix(platform): make the data-residency settings do what the docs promise#3262
larryro merged 10 commits into
mainfrom
fix/deployment-datastores-truth

Conversation

@larryro

@larryro larryro commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

Settings > Data residency saved a deployment-wide dataStores section (knowledge Postgres / app Postgres / Convex storage) plus a SOPS secrets sidecar into deployment.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, and main.ts builds every pool from DATABASE_URL / KNOWLEDGE_DATABASE_URL / OBJECT_STORE_*. A compliance control that reported success while inert.

This PR takes the retire lane of the theme README:

  • BackenddataStores, pgConnectionSchema (moved to schemas/knowledge.ts, its only consumer), convexStorageSchema, DEPLOYMENT_SECRET_KEYS and the secrets schema leave schemas/deployment.ts; the file keeps version + sandboxRuntime (the section the sandbox spawner reads at boot). parseDeploymentConfig drops a leftover dataStores section 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/secrets and /deployment/test doors, the secret masks in the view and core/deployment/secret_io.ts are removed.
  • Frontenddeployment-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.
  • Docsself-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_URL read on every boot; OBJECT_STORE_* seeds default/object-storage/connection.json on 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.md no longer says TALE_DEPLOYMENT_CONFIG_ADMINS edits data residency nor that the UI writes a richer per-store config; video-ingestion.md says the allowlist gates deployment.yml writes; tests/manual/data-residency.md describes 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; convexStorage is a Convex-era five-bucket shape with nothing behind it; and a UI-saved appPostgres that 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

  • lib-shared-schemas-1 — deployment.json dataStores saved but never read at boot: section, secrets sidecar, connection probe, UI section, locale keys and docs retired; legacy-file tolerance + strict-save refusal locked by unit tests and the integration check.

Review repair, round 2 (commits 2b831ff, bb9b792, merge 99c69f8)

Blocking item: the Settings overview card for this page, 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. Reworded in en/de/fr to what the page holds today, written natively per locale rather than word-for-word:

  • en: "Where this organization's knowledge base and uploaded files live, and which model embeds them."
  • de: "Wo die Wissensdatenbank und die hochgeladenen Dateien dieser Organisation liegen und welches Embedding-Modell sie verarbeitet."
  • fr: "Où vivent la base de connaissances et les fichiers téléversés de cette organisation, et quel modèle d'embedding les traite."

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 remaining déploiement / Deployment hits are the per-org sections' correct "deployment default / built-in store" copy.

Non-blocking notes applied (bb9b792, comments only): routes/dashboard/$id/settings.tsx header-slot comment names the live registrants (Branding's Reset, Providers' Refresh catalogs) instead of [Save] [Apply & restart]; enterprise-sso-form.tsx drops the "(see deployment stores)" pointer; settings/deployment.tsx says the surface is retired and the route only redirects. Not applied: the once-per-process warn guard in parseDeploymentConfig (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/main moved (#3247, #3248, #3249) and #3248 padded the same Feature-flags table in environment-reference.md (en/de/fr) that this branch edits; merged origin/main in with a merge commit (no history rewrite) keeping main's table with this branch's TALE_DEPLOYMENT_CONFIG_ADMINS row. 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:

  1. data-residency.md File storage bullet + "change one, then roll" sentence — no longer promise that OBJECT_STORE_* + restart relocates the default blob store. Verified against domains/object_storage/bootstrap.ts (ensureDefaultObjectStore: an existing default/object-storage/connection.json is returned as present and never rewritten; the seed writes connection.json + connection.secrets.json, SOPS-encrypted when hasSopsKey()) and lib/object-store.ts (default resolved from the default config 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 to DATABASE_URL / KNOWLEDGE_DATABASE_URL (main.ts, core/knowledge/pool.ts#defaultKnowledgeUrl). Page description unchanged → frontmatter.json regenerated with no diff.
  2. 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).
  3. video-ingestion.md:50 — the allowlist now "gates writes to the deployment config file (deployment.yml)", linking the Environment reference instead of Data residency.
  4. 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) parseDeploymentConfig warns on every read while a legacy dataStores block sits in the file — not changed in this repair (a log line, no behaviour; hand-removing the block is safe and a POST /config re-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 /config doors — 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.tsTest Files 3 passed (3) / Tests 50 passed (50) (parity + usage + the centralized @tale/ui/i18n/tests checks 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 merge Test Files 549 passed (549) / Tests 6366 passed (6366); on the merged tree Test Files 554 passed (554) / Tests 6386 passed (6386).
  • bunx tsc --noEmitTSC_EXIT=0; bunx oxlint --type-awareOXLINT_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-indexbuilt frontmatter manifest: 399 pages, no diff.
  • git merge-tree --write-tree origin/main HEAD — clean after the merge.

Repair pass (HEAD 3a6a7a0; only .md files 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 status clean afterwards (no frontmatter change).
  • bunx tsc --noEmit (services/platform) — TSC_EXIT=0; bunx oxlint --type-awareOXLINT_EXIT=0.
  • oxfmt --check on the touched .md files — 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 own domains/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 typecheckTasks: 29 successful, 29 total.
  • bun run format:check — clean on the rebased tree (at 5127ecd it flagged tasks/reattach.stalled.test.ts, untouched here and fixed on main).
  • bunx vitest --run --project serverTest Files 553 passed | 4 failed (557) under bun run check while 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.tsTest Files 3 passed (3) / Tests 46 passed (46); tests/pii/throughput.test.tsTest Files 1 passed (1) / Tests 5 passed (5) (the known CPU-load timing budget).
  • Targeted: backend/core/deployment/file_utils.test.ts, lib/shared/schemas/deployment.test.ts, lib/shared/schemas/knowledge.test.tsTest 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/docs 31 passed / 201 tests, @tale/shared 22 / 214, @tale/web 23 / 201, @tale/ui 124 / 1181; @tale/sandbox re-run alone → 251 pass / 0 fail (under the concurrent itest it hit backend destroy failed (wedged dockerd)).
  • bun run knip:check — only main's pre-existing items (../../core/chat/turn_store.ts unresolved in domains/chat/store.ts at 5127ecd, and the cron-parser ignoreDependencies hint); nothing from this branch.
  • run-itest.sh[itest] 497/499 checks passed across 135/135 lanes, no RUN 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) and messages: concurrent appends each take their own slot — appends=12, failed=1 (want 0) (conversations lane, not this theme; the run overlapped with bun run check on the same host — please re-run before treating it as real).

Notes for the reviewer

  • Branch was rebased onto origin/main (d9612b3, fix(platform): repair the chat store and audit-lock tests after the merges #3261) once and pushed with --force-with-lease so the gates run without main's chat-store/audit-test noise; three commits, no history rewrite beyond the rebase.
  • The repair commits were appended, not rewritten. The third repair commit's header (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, so main is unaffected. Say so if you want it amended.
  • /api/app/deployment/config GET/POST stay as the operator API for the remaining sandboxRuntime section (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_ADMINS stays: 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 names secret_io in its historical row; left as history.

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
larryro force-pushed the fix/deployment-datastores-truth branch from 1a06e0c to 66b0d40 Compare September 6, 2026 03:07
@larryro
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.
@larryro
larryro merged commit 7d4b564 into main Sep 6, 2026
66 of 67 checks passed
@larryro
larryro deleted the fix/deployment-datastores-truth branch September 6, 2026 06:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant