Skip to content

Unify the developer-facing org identifier: hooks expose session.tenantId while RLS/seed/columns use organizationId (add organizationId as the blessed name) #3280

Description

@os-zhuang

Problem

The same concept — the caller's active organization — is surfaced to third-party app developers under two different names, split by which authoring mode they're in. A developer writing a metadata app + hooks has to internalize the hidden equation tenantId === organizationId to move between the surfaces.

This is a developer-experience / naming issue only. It is not about the internal kernel or driver layer (see Non-goals). It surfaced while tracing the tenant-scope layer for #3249.

Where a developer sees each name today

What the developer is doingName they useValue is
Object metadata (.object.yml)tenancy.enabled / tenancy.tenantFieldmechanism / column knob
Field definitions & query filtersorganization_idthe org
JS hook — read the current orgctx.session.tenantIdthe org (named "tenant")
JS hook — ctx.user shortcut{ id, name, email }no org field at all
RLS / sharing / permission expressionscurrent_user.organizationIdthe org
Seed dataorganizationIdthe org

The value the developer actually cares about is the org, and it is called organization_id / organizationIdeverywhere except the JS-hook session, which calls it tenantId. Worse, the ergonomic ctx.user shortcut has no org field, so a hook author who needs "the current org to filter by" is forced onto ctx.session.tenantId and must know that tenant == org.

Evidence (spec/engine source)

  • packages/objectql/src/engine.tsbuildSession() emits session.tenantId (from execCtx.tenantId); ctx.user shortcut is only { id, name, email }.
  • packages/spec/src/data/hook.zod.tsHookContext.session exposes tenantId; the user object has no org field. Test hook.test.ts asserts context.session?.tenantId.
  • packages/spec/src/identity/eval-user.zod.ts:104 — RLS current_user uses organizationId ("Active organization ID (null = platform/unscoped)").
  • packages/spec/src/identity/identity.zod.ts:268organizationId ("Scoped organization ID").
  • packages/spec/src/data/seed-loader.zod.ts — seed rows use organizationId.
  • packages/spec/src/system/metadata-persistence.zod.ts:103 — literally comments that Tenant ID is an "alias of organizationId in some contexts".
  • packages/spec/src/kernel/execution-context.zod.ts:45ExecutionContext.tenantId is documented as "resolved from session.activeOrganizationId", i.e. it is always the org.

Proposed direction — additive, non-breaking, converge the dev surface on organization

Because org is already the majority term on the developer surface (column, current_user, seed) and matches the DB column organization_id, converge the JS-hook surface onto it. Add the org name; keep the old one as a deprecated alias. Do not rename internals.

  1. Add organizationId to the hook ctx.user shortcut (highest value). buildSession() already holds execCtx.tenantId — also expose it as user.organizationId. A hook author then writes ctx.user.organizationId, matching current_user.organizationId (RLS) and organization_id (filters) with zero relearning.
  2. Add organizationId to ctx.session as the blessed name; keep session.tenantId as a deprecated alias. Both carry the same value; new docs/examples teach only organizationId.
  3. Leave the tenancy.* object-metadata knob as-is, but document it clearly: tenancy is the isolation mechanism (on/off + custom tenantField); the value you read/write is your organization_id column.

Non-goals (do NOT touch)

  • ExecutionContext.tenantId, DriverOptions.tenantId, SqlDriver.applyTenantScope / tenantFieldByTable, TenancyConfig.tenantField. These are the generic tenancy abstraction at the driver layer — the isolation column is configurable (tenant_id / workspace_id / organization_id), and the same DriverOptions.tenantId legitimately carries an environment id in per-environment kernels (e.g. service-ai tools stamp tenantId: ctx.environmentId). Renaming these would (a) bake an "org" assumption into a generic layer and (b) be a wide public-API break. Keep them generic; only the developer-facing hook surface changes.

Acceptance criteria

  • ctx.user.organizationId is available in JS hooks (all hook events that today populate session), typed in HookContext (spec).
  • ctx.session.organizationId is available; ctx.session.tenantId still works but is marked deprecated in its TSDoc.
  • Both fields always equal execCtx.tenantId (the resolved active org). Unit test covering a populated org and the anonymous/undefined case.
  • Hook authoring docs/examples updated to use organizationId (drop session.tenantId from examples).
  • A short note (docs or ADR) distinguishing the two isolation axes: org row-scoping (organization_id, shared DB) vs environment/database-per-tenant (service-tenant, driver-turso — where "tenant" = environment).
  • No behavior change and no breaking rename; changeset added for the additive spec surface.

Notes

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions