docs(spec,drivers): managed-datasource read-only is a database privilege, not a platform gate (#4584) - #7241
Conversation
…ege, not a platform gate (#4584) #4583 removed `datasource.capabilities.readOnly` — a key that read as a safety property and gated nothing — and left the gap it exposed pointing at #4584: `external.allowWrites: false` is the one enforced datasource-wide write gate and it covers only FEDERATED datasources, so a managed datasource had no read-only gate at all. #4584 ruled 方案 B: that stays so on purpose, and the docs say so. An ObjectQL-level flag would stop writes on one path and leave a direct `psql` session, a migration, a `syncSchema()` DDL statement and any process sharing the connection string untouched. A boundary that holds in one path is not a boundary, and one that merely looks like a boundary is worse than none because it gets trusted — the exact defect #4583 removed. Read-only belongs to the database account (`GRANT SELECT`), where there is no bypass surface. Docs-only; no schema shape changes. - content/docs/data-modeling/drivers.mdx: two new sections under Multi-Datasource. "Read-only: grant it at the database, not in metadata" — a worked `GRANT SELECT` role, the managed datasource that carries its credentials in `config` (an `external` block is rejected there), the DDL / schema-sync consequence of a read-only account, why the platform declines the flag, and a table of what actually enforces what. "Read replicas: the platform does not route" — the #4479 dual conclusion: no query path separates reads from writes, so put the replicas behind pgpool / ProxySQL / an RDS reader endpoint and point `config` there; that is the correct answer, not a stopgap. - content/docs/data-modeling/external-datasources.mdx: the double opt-in write gate now says plainly that it is federation-only, and links across. - packages/spec/src/data/datasource.zod.ts: the `capabilities.readOnly` tombstone carried "Tracked in #4584". It now carries the answer. Prose only — no key, shape or default changed, and `check:docs` confirms no generated page moves. - examples/app-crm: the `crm_analytics` header comment recorded the ruling instead of waiting on it. Closes #4584
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 7 release-owned page(s) also reference the affected code. These are read-only:
|
…#4584) The ADR-0090 D3 reserved-word ratchet (`check:role-word`) rejected two new uses of "role" in the read-only section. Both are avoidable rather than genuine boundaries, so this drops the word instead of taking a baseline waiver: - prose: "at a role that can only read" → "at an account that can only read"; - SQL: `CREATE ROLE analytics_ro LOGIN PASSWORD …` → `CREATE USER analytics_ro PASSWORD …`, which in PostgreSQL is exactly the same statement — `CREATE USER` is `CREATE ROLE` with `LOGIN` implied — so the example is unchanged in effect. `check:role-word` is green (44 baselined files, no new occurrences), as are check:quick-reference-counts / adr-anchors / org-identifier / release-notes / release-body and eslint over the changed sources.
Executes the standing #4584 ruling verbatim. Docs-only — no platform gate is built, and no schema shape changes.
The ruling
From comment 5163028174 (2026-08-03), quoted untranslated:
Premise check (verified on fresh
origin/main,f40c5b4)GRANT SELECT/ database-privilege statement anywhere incontent/docs/data-modeling/; the only read-only mentions are the federation gate and a UI-readonly row.crm.datasource.tsstill waits on #4584Whether a managed datasource should have a read-only gate at all is #4584 — until that is answered…ObjectQLEngine.assertWriteAllowed(packages/objectql/src/engine.ts:3397) returns early on!ds.schemaMode || ds.schemaMode === 'managed', before it readsexternal.allowWrites.One thing the premise check turned up that the dispatch did not anticipate:
packages/spec/src/data/driver/common.zod.ts(READ_ONLY_BELONGS_ON_DATASOURCE) already carries the ruled sentence — "grant the connection SELECT-only at the database instead, which is a real boundary rather than an application-layer flag (#4584)". The spec-side prose was ahead of the docs; only the docs were missing it, which is what this PR fixes.Changes
content/docs/data-modeling/drivers.mdx— two new sections under Multi-Datasource:GRANT SELECTrole, the matchingdefineDatasource(credentials inconfig— the parse rejects anexternalblock on amanageddatasource), the DDL consequence a read-only account carries (no boot-time schema sync, no migrations), Why the platform does not offer the flag citing the exactcapabilities.readOnlyshape datasource 账本判定的 20 条死键至今无人处置:三个块整块无人读,其中 readOnly 让一个 shipped 示例的「只读副本」可写(ADR-0049 enforce-or-remove) #4583 removed, The one enforced write gate is federation-only, and a table of what actually enforces what.configthere — the correct answer, not a stopgap.content/docs/data-modeling/external-datasources.mdx— §5 Writes (double opt-in) now states that the gate is federation-only and cross-links; See also links both new sections.packages/spec/src/data/datasource.zod.ts— thecapabilities.readOnlytombstone saidTracked in #4584.It now carries the answer. Prose inside a guidance string only — no key, shape, default or validation behaviour changed, nothing pins the string, andcheck:docsconfirms no generated page moves. Flagged here because the dispatch scoped this PR away from.zod.tsedits beyond prose; leaving a resolved issue described as "tracked" would have contradicted the docs this PR adds.examples/app-crm/src/datasources/crm.datasource.ts— thecrm_analyticsheader comment records the ruling instead of waiting on it..changeset/managed-datasource-readonly-documented.md—@objectstack/specpatch +@objectstack/example-crmpatch. Added because the diff changes a published package's author-facing rejection message; the repo'schangeset-checkenforces on every unlabelled PR.Gates
pnpm --filter @objectstack/spec buildpnpm --filter @objectstack/spec check:docsnode scripts/check-doc-authoring.mjsvitest run src/data/datasource(spec)vitest run src/conversions(spec)vitest run lint-liveness-properties(lint)tsc --noEmit(@objectstack/example-crm)check-empty-changeset/check-changeset-no-major/check-changeset-fixedNot touched
content/docs/releases/**anddocs/adr/**— untouched, per the standing rule. No schema shape change, no acceptance change, no platform gate built.Closes #4584
Generated by Claude Code