Skip to content

Phase 1 identity substrate: users table, owner user, terminal user binding#147

Open
ClaydeCode wants to merge 5 commits into
mainfrom
feature/oidc-phase1-users
Open

Phase 1 identity substrate: users table, owner user, terminal user binding#147
ClaydeCode wants to merge 5 commits into
mainfrom
feature/oidc-phase1-users

Conversation

@ClaydeCode

@ClaydeCode ClaydeCode commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Phase 1 of the multi-user identity rollout (design docs in the KB, PoC verdict 2026-06-12): the invisible identity substrate that the embedded OIDC provider (phase 2, follow-up PR) builds on.

What this does

  • New users table (numeric BIGSERIAL ids, user_role enum, User model in data_model/user.py) — every shard now has an owner user derived from the default identity. The stringified numeric id becomes the OIDC sub in phase 2a; the shard's identity hash id stays what it is — the shard's identity, not the person's.
  • terminals.user_id (NOT NULL) — every terminal session is bound to a user. The migration itself creates the owner and binds existing terminals; new pairings bind at creation.
  • ensure_owner_user() runs at every startup (after init_default_identity(), before serving): creates the owner on fresh shards (where the migration ran against empty tables) and backfills the synthesized email for migration-created owners. Idempotent.
  • Email: owner email = identity email, or synthesized owner@<shard-domain> — OIDC clients (e.g. Immich) require an email-shaped identifier to auto-provision accounts (PoC finding).

No UX or API behavior change. Existing sessions stay valid (cookie/JWT mechanism untouched).

Not in this PR

  • OIDC provider endpoints (phase 2a, next PR — stacked on this one)
  • Client registration at app install + template vars (phase 2b)
  • Login page, members, grants (phases 3–4)

Recommended reading order

  1. migrations/shard-core-0002-users.sql — schema
  2. shard_core/database/users.py — new DB module (conn-first pattern)
  3. shard_core/database/terminals.py — insert gains user_id, backfill helper
  4. shard_core/data_model/terminal.pyTerminal.user_id (optional, backward compatible)
  5. shard_core/service/user.pyensure_owner_user()
  6. shard_core/web/public/pair.py — new terminals bind to owner
  7. shard_core/app_factory.py — lifespan wiring
  8. tests/conftest.py, tests/test_users.py — fixture wiring + coverage

🤖 Generated with Claude Code

Test status

Full suite on the stacked branch (this + phase 2a): 190 passed, 1 failedtest_app_lifecycle.py::test_app_starts_and_stops, which fails identically on clean main on this machine (pre-existing, container-start timing), not a regression from this branch.


Review round 1 addressed: numeric ids, role enum, NOT NULL binding, User data model, strict owner lookup in pairing.

Comment thread migrations/shard-core-0002-users.sql Outdated
Comment thread shard_core/database/users.py Outdated
Comment thread migrations/shard-core-0002-users.sql Outdated
Comment thread migrations/shard-core-0002-users.sql Outdated
Comment thread shard_core/service/user.py Outdated
Comment thread shard_core/web/public/pair.py Outdated
ClaydeCode and others added 4 commits July 26, 2026 22:36
…minal user binding

Adds the users table (id doubles as the future OIDC sub) and binds every
terminal session to a user. The owner user is derived from the default
identity: same id (single source of truth for identity), email taken from
the identity or synthesized as owner@<domain> since OIDC clients require an
email-shaped identifier to auto-provision accounts.

ensure_owner_user() runs on every startup after init_default_identity():
creates the owner row if missing and backfills user_id on any terminal
that predates this migration. New pairings bind to the owner directly.
No behavior or API change otherwise; existing sessions stay valid.

Phase 1 of the multi-user identity rollout (embedded OIDC provider).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nding, User model

- users.id is now BIGSERIAL: the owner user is its own entity, decoupled
  from the shard's identity hash id (which identifies the shard, not the
  person). The OIDC sub becomes the stringified numeric id in phase 2a.
- role is a proper Postgres enum (user_role), mirrored by Role in the new
  data_model/user.py; ensure_owner_user returns a User model.
- terminals.user_id is NOT NULL: the 0002 migration itself creates the
  owner user and binds existing terminals (existing shards have their
  identity at migration time; fresh shards have empty tables and get the
  owner at startup before any pairing). The Python-side backfill helper is
  gone; ensure_owner_user only creates the owner on fresh shards and
  backfills the synthesized email for migration-created owners.
- pair.py drops the owner-missing fallback — the owner always exists once
  the app serves requests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The legacy-JSON migration inserted terminals without user_id, violating the
new NOT NULL constraint (caught by CI). The owner user is created from the
just-migrated default identity, mirroring the 0002 backfill.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@max-tet
max-tet force-pushed the feature/oidc-phase1-users branch from 703fa17 to 7d761f7 Compare July 26, 2026 20:36
… at them

The 0002 migration makes terminals.user_id a NOT NULL foreign key to users.
restore_db_snapshot iterated the snapshot in dumped order, which is alphabetical
(_list_data_tables orders by table_name), so terminals was inserted before users
existed and the restore aborted with

  psycopg.errors.ForeignKeyViolation: insert or update on table "terminals"
  violates foreign key constraint "terminals_user_id_fkey"

On a fresh shard that means the whole snapshot restore fails and the shard comes
up with a brand-new identity — the exact outcome the snapshot exists to prevent
(issue #122). The first FK in the schema arrives with this branch, so nothing on
main exercised the ordering.

Derive the insert order from the live schema's FK graph at restore time rather
than trusting the order in the file, so snapshots written by older cores restore
correctly too. Seed the snapshot test with the owner user and a bound terminal;
without the ordering fix that test reproduces the ForeignKeyViolation above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

2 participants