Skip to content

feat(ownership): auto-provision canonical owner_id + hand seeded records to first admin#2021

Merged
os-zhuang merged 6 commits into
mainfrom
feat/seed-ownership-handoff
Jun 18, 2026
Merged

feat(ownership): auto-provision canonical owner_id + hand seeded records to first admin#2021
os-zhuang merged 6 commits into
mainfrom
feat/seed-ownership-handoff

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Problem

Record ownership was opt-in and the boot order made it impossible to seed ownership to the eventual human admin:

  • Data seed runs in app-plugin.start()before any human user exists (the login admin is minted later, on kernel:ready). So os.user is bound to a deterministic, non-loginable usr_system, and seeded rows land owned by usr_system or NULL.
  • The human admin's id is random (better-auth), so it can't be referenced from static seed.
  • Apps "almost never declare owner_id" (the platform's own RLS comment), so they reinvent a custom owner lookup that the platform's ownership machinery (auto-stamp, RLS, "My" views) can't see.

Net effect: seeded demo data is owned by nobody a human can log in as → My-views / owner reports / owner notifications are empty out of the box, and author-written objects silently ship with no working ownership at all. This is the worst failure mode for AI authors: it compiles, seeds and renders, but the feature is silently dead.

Fix — ownership correct-by-default

1. applySystemFields (objectql) auto-injects a canonical, reassignable owner_id lookup (→ sys_user) on user-authored business objects, next to the existing tenant/audit fields. Unlike the audit *_by lookups it is NOT readonly — ownership transfers. Withheld for managedBy / sys_* tables and for objects that opt out via ownership: 'org' | 'none' (Dataverse-style). Once present, the existing machinery engages automatically (insert auto-stamp at security step 3.5, owner RLS, owner-keyed views/reports).

Safe default direction: forgetting the opt-out leaves a harmless spare nullable column; the old opt-IN model let authors ship broken ownership.

2. claimSeedOwnership (plugin-security) — invoked from bootstrapPlatformAdmin right after the first human is promoted to platform admin — transfers ownership of seeded rows (owner_id NULL or usr_system) to that admin. The ownership twin of org-scoping's claimOrphanOrgRows. Idempotent; skips managedBy / sys_*. Authors write plain seed records (no owner_id); the platform performs the handoff — nothing to remember or mistype.

3. Hardens bootstrapPlatformAdmin against a latent dts typecheck error (defensive read of the untyped description on seed permission sets, surfaced when the cache invalidated).

Why this shape (AI-authored code)

The design optimizes for correct-by-default, loud-on-mistake because the code — schemas and seeds — is largely AI-written:

  • Zero-config default: write a normal object → ownership just works. Nothing to declare/forget.
  • Platform-decided exclusions: managedBy / sys_* auto-excluded; only the rare catalog/junction needs one line ownership: 'org'.
  • Handoff, not author-supplied owner: seeds stay plain; no cel\os.user.id`` to mistype.

Tests

  • registry.test.ts: +5 owner_id injection cases (default inject, managedBy/sys_ exclusion, ownership opt-out, no-overwrite).
  • claim-seed-ownership.test.ts: +6 (NULL & usr_system claim, human-owned untouched, managed/sys_ skip, idempotent, system-target no-op).
  • Full framework suite green: 128/128 tasks.

Follow-ups (not in this PR)

  • Multi-tenant: mirror the handoff in claimOrphanOrgRows / ensureDefaultOrganization so per-org seed clones are owned by the org's admin.
  • A compile-time lint that flags author-declared custom owner-ish lookups (now redundant alongside the auto owner_id).
  • App migration: HotCRM's custom owner → canonical owner_id.

🤖 Generated with Claude Code

…rds to first admin

Make record ownership correct-by-default (a mistake-proof design for AI/human
authors) instead of the opt-in model that silently shipped objects with no
working ownership and left seeded demo data owned by nobody a human can log in
as.

- objectql/applySystemFields: auto-inject a canonical, REASSIGNABLE `owner_id`
  lookup on user-authored business objects, next to the tenant/audit fields.
  Withheld for managedBy/sys_* and for `ownership: 'org' | 'none'` opt-outs
  (Dataverse-style). Once present, insert auto-stamp + owner RLS + owner-keyed
  views engage automatically.
- plugin-security/claimSeedOwnership: invoked from bootstrapPlatformAdmin after
  the first human is promoted to platform admin, transfers ownership of seeded
  rows (owner_id NULL or usr_system) to that admin. Ownership twin of
  org-scoping's claimOrphanOrgRows; idempotent; skips managedBy/sys_*.
- bootstrapPlatformAdmin: defensive read of untyped permission-set `description`
  (fixes a latent dts typecheck error surfaced by the cache invalidation).

Tests: registry owner_id injection cases (+5), claimSeedOwnership suite (+6).
Full framework suite green (128/128 tasks).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jun 18, 2026 6:52am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jun 18, 2026
@github-actions

github-actions Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/objectql, @objectstack/plugin-security, @objectstack/runtime, @objectstack/spec.

98 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/cloud-artifact-api.mdx (via packages/runtime, packages/spec)
  • content/docs/concepts/cluster-semantics.mdx (via @objectstack/spec)
  • content/docs/concepts/core/services.mdx (via @objectstack/objectql)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/implementation-status.mdx (via @objectstack/objectql, @objectstack/plugin-security, @objectstack/runtime, @objectstack/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/runtime, packages/spec)
  • content/docs/concepts/packages.mdx (via @objectstack/objectql, @objectstack/plugin-security, @objectstack/runtime, @objectstack/spec)
  • content/docs/concepts/setup-app.mdx (via @objectstack/spec)
  • content/docs/concepts/skills.mdx (via @objectstack/spec)
  • content/docs/concepts/webhook-delivery.mdx (via @objectstack/spec)
  • content/docs/getting-started/architecture.mdx (via @objectstack/spec)
  • content/docs/getting-started/cli.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/getting-started/core-concepts.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/guides/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/guides/ai-capabilities.mdx (via @objectstack/spec)
  • content/docs/guides/airtable-dashboard-analysis.mdx (via @objectstack/spec)
  • content/docs/guides/analytics-datasets.mdx (via @objectstack/spec)
  • content/docs/guides/api-reference.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/guides/authentication.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/guides/business-logic.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/error-catalog.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/field-type-gallery.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/field-validation-rules.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/permissions-matrix.mdx (via packages/plugins/plugin-security, @objectstack/spec)
  • content/docs/guides/cheatsheets/protocol-diagram.mdx (via packages/spec)
  • content/docs/guides/cheatsheets/query-cheat-sheet.mdx (via @objectstack/spec)
  • content/docs/guides/cheatsheets/quick-reference.mdx (via @objectstack/spec)
  • content/docs/guides/client-sdk.mdx (via @objectstack/spec)
  • content/docs/guides/cloud-deployment.mdx (via @objectstack/runtime)
  • content/docs/guides/common-patterns.mdx (via @objectstack/spec)
  • content/docs/guides/contracts/auth-service.mdx (via packages/spec)
  • content/docs/guides/contracts/cache-service.mdx (via packages/spec)
  • content/docs/guides/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/guides/contracts/index.mdx (via @objectstack/spec)
  • content/docs/guides/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/guides/contracts/storage-service.mdx (via packages/spec)
  • content/docs/guides/data-modeling.mdx (via @objectstack/spec)
  • content/docs/guides/deployment-vercel.mdx (via @objectstack/objectql, @objectstack/runtime, @objectstack/spec)
  • content/docs/guides/driver-configuration.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/guides/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/guides/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/guides/formula.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/guides/hook-bodies.mdx (via @objectstack/runtime, packages/spec)
  • content/docs/guides/kernel-services.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/guides/metadata/dashboard.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/guides/metadata/field.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/flow.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/index.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/object.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/validation.mdx (via @objectstack/spec)
  • content/docs/guides/metadata/workflow.mdx (via @objectstack/spec)
  • content/docs/guides/objectql-migration.mdx (via @objectstack/objectql)
  • content/docs/guides/packages.mdx (via @objectstack/objectql, @objectstack/plugin-security, @objectstack/runtime, @objectstack/spec)
  • content/docs/guides/plugin-chatbot-integration.mdx (via @objectstack/runtime)
  • content/docs/guides/plugin-development.mdx (via @objectstack/spec)
  • content/docs/guides/plugins.mdx (via @objectstack/objectql, @objectstack/plugin-security, @objectstack/spec)
  • content/docs/guides/production-readiness.mdx (via @objectstack/runtime)
  • content/docs/guides/project-scoping.mdx (via @objectstack/spec)
  • content/docs/guides/public-forms.mdx (via @objectstack/spec)
  • content/docs/guides/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/guides/runtime-services/index.mdx (via packages/spec)
  • content/docs/guides/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/guides/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/guides/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/guides/security.mdx (via @objectstack/plugin-security, @objectstack/spec)
  • content/docs/guides/seed-data.mdx (via @objectstack/spec)
  • content/docs/guides/single-project-mode.mdx (via @objectstack/runtime)
  • content/docs/guides/skills.mdx (via @objectstack/spec)
  • content/docs/guides/standards.mdx (via @objectstack/spec)
  • content/docs/guides/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/http-protocol.mdx (via @objectstack/runtime)
  • content/docs/protocol/objectos/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/index.mdx (via @objectstack/objectql, @objectstack/runtime)
  • content/docs/protocol/objectos/lifecycle.mdx (via @objectstack/runtime, @objectstack/spec)
  • content/docs/protocol/objectos/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/objectos/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via packages/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via packages/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/objectql, @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

os-zhuang and others added 2 commits June 18, 2026 14:21
…user.id

The default ownership model leaves seed `owner_id` NULL and lets the first-admin
handoff (claimSeedOwnership) re-own those rows, so most bundles never reference
`os.user`. Provision the non-loginable `usr_system` placeholder lazily — only
when a seed dataset actually embeds `cel`os.user.id`` (detected via the
serialized CEL source) — so a typical app never creates it. `os.org` is
unaffected (derived from organizationId in the loader).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…eed owner

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@os-zhuang

Copy link
Copy Markdown
Contributor Author

Follow-up: usr_system demoted to a lazy fallback

Pushed 3 more commits addressing "if the human admin owns everything via handoff, what is usr_system even for?"

Audit across the whole framework (non-test): usr_system is created once (to bind os.user during seed CEL eval) and only ever excluded everywhere else (3 guards that stop it from being mistaken for a human admin). No runtime code ever acts as usr_system — system operations use the { isSystem: true } context flag, not that userId. And no real seed dataset embeds cel\os.user.id``.

So its sole job had collapsed to "seed-time os.user binding." With the NULL-owner + handoff model, that job disappears. Change:

  • ensureSeedIdentity is now invoked lazily — only when a seed dataset actually embeds cel\os.user.id`(detected via the serialized CELsource). A typical bundle leaves owner_idNULL, sousr_systemis **never created**. It remains purely as a backward-compatible fallback for any seed that still referencesos.user. os.orgis unaffected (derived fromorganizationId`).
  • Docs on SystemUserId.SYSTEM and ensureSeedIdentity updated to describe the lazy-fallback role.

Net: the default boot no longer creates a placeholder system user at all; seeded rows are NULL-owned → handed to the first real admin. Same end result, one fewer magic identity.

Build green (22 tasks); runtime 388 + plugin-security 97 tests pass.

os-zhuang and others added 2 commits June 18, 2026 14:45
…d time

Eliminates the usr_system placeholder entirely (not just lazily). The seed
loader now binds os.user to a NULL identity when none is supplied, so
`owner_id: cel`os.user.id`` resolves to NULL instead of crashing or dropping the
record — semantically "owned by whoever becomes the first admin", which the
first-admin handoff (claimSeedOwnership) then fills in.

- objectql/seed-loader: bind `user: identity?.user ?? { id: null }`; the
  unresolved-expression error message no longer tells authors to provision a
  system user (os.user.id now resolves to null).
- runtime/app-plugin: remove `ensureSeedIdentity` (the only code that inserted a
  usr_system row) and its SystemUserId import; pass no seed identity.
- spec: SystemUserId.SYSTEM redocumented as a reserved id that is NO LONGER
  auto-provisioned — kept only so legacy DBs' exclusion guards / ownership
  handoff still recognize a pre-existing usr_system row.

Tests: new seed-loader-os-user test (os.user.id → null under null identity, →
real id when supplied); updated the runtime seed-loader test that asserted the
old fail-loud-on-unbound contract. objectql 665, runtime 388, plugin-security 97
all green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions github-actions Bot added size/l and removed size/m labels Jun 18, 2026
@os-zhuang

Copy link
Copy Markdown
Contributor Author

usr_system eliminated entirely (not just lazy)

Per feedback — don't create usr_system at all; scheme for seeds that use cel\os.user.id``:

Resolve os.user.id to NULL at seed time. It semantically means "the user installing this seed" — who doesn't exist yet — so the honest value is unknown → NULL → resolved later by the first-admin handoff.

  • Seed loader binds os.user to a NULL identity ({ id: null }) → cel\os.user.id`evaluates tonull(no crash, no dropped record). Row seeds NULL-owned;claimSeedOwnership` re-owns it to the promoted admin.
  • ensureSeedIdentity (the only code that inserted a usr_system row) is removed — a fresh boot never creates the placeholder.
  • SystemUserId.SYSTEM kept only as a reserved id so legacy DBs' exclusion guards / handoff still recognize a pre-existing row.
  • Edge: os.user.id on a required non-owner field now fails loudly at validation (correct).

Tests green: objectql 665 · runtime 388 · plugin-security 97.

@os-zhuang
os-zhuang merged commit 1707fe5 into main Jun 18, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the feat/seed-ownership-handoff branch June 18, 2026 06:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:system size/l tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant