Found while documenting the X-Tenant-ID edge contract for #5279. Filed rather than fixed: skills/** is a published surface outside that card's fence, and edits there carry their own budget rules.
The text
skills/objectui/guides/auth-permissions.md, "Multi-tenancy":
There is no client-side tenancy layer. Tenant scoping is server-enforced:
createAuthenticatedFetch (@object-ui/auth) injects the active
organization as the X-Tenant-ID header on every API call, and the backend
applies row-level isolation.
The first sentence is right. The second joins two facts with a colon that reads as cause and effect — the client sends the header, therefore the backend isolates rows — and that implication is false.
What is actually true
- Row scoping comes from the session, not the header.
resolveAuthzContext (framework, packages/core/src/security/resolve-authz-context.ts) derives tenantId from the API-key principal or from session.activeOrganizationId, and from no request header. The framework pins it: packages/verify/src/harness.org-context.test.ts — "session.activeOrganizationId is the ONE field resolveAuthzContext reads into tenantId". - The header is an edge routing hint, consumed by the cloud edge to pick a tenant database / environment. It is one of six configurable identification sources in
TenantRoutingConfigSchema (@objectstack/spec/cloud), and by default it ranks second, behind subdomain. - Trusting the header as identity is a recorded vulnerability. The framework's
plugin-sharing documents that its old default trusted x-user-id / x-tenant-id and that this "let a client forge attribution and enumerate/revoke other users' links"; the secure default reads no identity from headers. - "on every API call" is inaccurate in both directions. The stamp is skipped entirely until
AuthProvider's async organization chain resolves (the unstamped-first-request window), and it is not gated on the URL being an API call the way Authorization is.
Why it matters more than a wording nit
This is a published skill — it is what an agent or integrator reads before wiring multi-tenancy. The belief it plants ("the header is what enforces tenancy") is exactly the belief that leads someone to trust x-tenant-id server-side, which is the failure plugin-sharing already had to remove once. It also points the wrong way when the header is missing: a reader who thinks isolation depends on it will treat the first-boot window as a security hole rather than what it is, a routing-input gap.
Where the correct text now lives
packages/auth/README.md gains "The X-Tenant-ID edge contract" in the #5279 PR — what the header means, who stamps it, who reads it, what a reader may and may not assume, and the unstamped-first-request window. The skill's paragraph can be repointed at that section; the correction is roughly a sentence, not an expansion.
Found while documenting the
X-Tenant-IDedge contract for #5279. Filed rather than fixed:skills/**is a published surface outside that card's fence, and edits there carry their own budget rules.The text
skills/objectui/guides/auth-permissions.md, "Multi-tenancy":The first sentence is right. The second joins two facts with a colon that reads as cause and effect — the client sends the header, therefore the backend isolates rows — and that implication is false.
What is actually true
resolveAuthzContext(framework,packages/core/src/security/resolve-authz-context.ts) derivestenantIdfrom the API-key principal or fromsession.activeOrganizationId, and from no request header. The framework pins it:packages/verify/src/harness.org-context.test.ts— "session.activeOrganizationIdis the ONE fieldresolveAuthzContextreads intotenantId".TenantRoutingConfigSchema(@objectstack/spec/cloud), and by default it ranks second, behindsubdomain.plugin-sharingdocuments that its old default trustedx-user-id/x-tenant-idand that this "let a client forge attribution and enumerate/revoke other users' links"; the secure default reads no identity from headers.AuthProvider's async organization chain resolves (the unstamped-first-request window), and it is not gated on the URL being an API call the wayAuthorizationis.Why it matters more than a wording nit
This is a published skill — it is what an agent or integrator reads before wiring multi-tenancy. The belief it plants ("the header is what enforces tenancy") is exactly the belief that leads someone to trust
x-tenant-idserver-side, which is the failureplugin-sharingalready had to remove once. It also points the wrong way when the header is missing: a reader who thinks isolation depends on it will treat the first-boot window as a security hole rather than what it is, a routing-input gap.Where the correct text now lives
packages/auth/README.mdgains "TheX-Tenant-IDedge contract" in the #5279 PR — what the header means, who stamps it, who reads it, what a reader may and may not assume, and the unstamped-first-request window. The skill's paragraph can be repointed at that section; the correction is roughly a sentence, not an expansion.