Skip to content

feat(database): unify resource ID generation in storage dialects - #663

Merged
adlerhurst merged 25 commits into
mainfrom
cursor/dialect-owned-id-generation-a541
Aug 3, 2026
Merged

feat(database): unify resource ID generation in storage dialects#663
adlerhurst merged 25 commits into
mainfrom
cursor/dialect-owned-id-generation-a541

Conversation

@adlerhurst

@adlerhurst adlerhurst commented Jul 30, 2026

Copy link
Copy Markdown
Member

TL;DR

Every resource PK is a dialect-minted prefix_<opaque> string (Postgres ULID, SQLite ULID, Spanner UUID v4). SQL has no IDENTITY / uuid / AUTOINCREMENT defaults. Create APIs reject client resource PKs. Documented as ADR 047.

Also cleaned review leftovers: sessions.token_id is nullable (retired "0" sentinel), redundant Create-supplied DEFAULTs dropped across dialects, dead project_secret/preview_secret removed.

Follow-up review pass: prefix selection rules + keep-any ceremony note in ADR 047; statements-only ID minting in root/storage AGENTS.md; ResourcePrefix comment softened; v2database aliases dropped; usr_/htok_ fixtures aligned to user_/handoff_.

Synced main: ListTeams + team statement suites in stmttest (#728/#733); dialect Spanner team_test.go removed; empty-ID Create asserted in stmttest (dialect minting).

How to review (order)

  1. Contractdocs/adrs/047-dialect-id-generation.md (+ amended 011/012/028)
  2. Generatorsinternal/storage/v2/dialect/idgen/ + dialect managed_id.go (postgres/sqlite ULID, spanner UUID)
  3. DDL — goose SQL under postgres/, spanner/, sqlite/ (TEXT PKs; nullable sessions.token_id; no redundant array/JSON/TTL DEFAULTs)
  4. Create paths — mint-before-insert; session insert binds NULL token_id until token mint
  5. API — OpenAPI sess_/att_/ch_ patterns; KEK pre-mint for JWE kid
  6. Agent contract — root + internal/storage/v2/AGENTS.md identifier minting
  7. Skimapi/generated/**, stmttest HasPrefix fixtures

Behavior changes

  • Alpha-breaking: wipe local/CI DBs.
  • Wire shape: always prefix_…; Spanner bodies are UUID v4.
  • Local default dialect is SQLite; ID rules match Postgres (ULID).
  • sessions.token_id is NULL until the real token is linked (no "0" placeholder).
  • Secrets (handoff / session token material) unchanged — not row PKs.

Validation

  • go test ./internal/service/ ./internal/domain/ — PASS
  • moon run server:test-sqlite — PASS (includes stmttest team Create empty-ID)
  • CI: re-check ci / full-pr after this tip

Release notes / changeset

.changeset/dialect-owned-id-generation.md — minor @zitadel/server

Review notes

  • Prefer squash on merge.
  • livio-a ID-prefix / import / wrapError threads addressed and resolved (wrapError deferred to ADR 030).

Notes

  • Cross-dialect tests assert HasPrefix, not ULID-only regex.
  • team_id = '' remains the intentional project-scope uniqueness sentinel (not nullable).
Open in Web Open in Cursor 

@vercel

vercel Bot commented Jul 30, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
nextgen Ready Ready Preview Aug 3, 2026 8:53pm
nextgen-docs Ready Ready Preview Aug 3, 2026 8:53pm
nextgen-mock-zitadel Ready Ready Preview Aug 3, 2026 8:53pm

Request Review

@github-actions

github-actions Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

🦋 Changeset detected

Latest commit: da4e548

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 20 packages
Name Type
@zitadel/server Minor
@zitadel/cli Minor
@zitadel/testing Minor
@zitadel/server-linux-x64 Minor
@zitadel/server-linux-arm64 Minor
@zitadel/server-darwin-x64 Minor
@zitadel/server-darwin-arm64 Minor
@zitadel/server-win32-x64 Minor
@zitadel/api Minor
@zitadel/config Minor
@zitadel/components Minor
@zitadel/sdk-core Minor
@zitadel/sdk-next Minor
@zitadel/sdk-nuxt Minor
@zitadel/sdk-react Minor
@zitadel/sdk-vue Minor
@zitadel/sdk-angular Minor
@zitadel/sdk-solid Minor
@zitadel/sdk-svelte Minor
@zitadel/sdk-qwik Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor cursor Bot changed the title feat(database): unify ID generation in storage dialects feat(database): unify resource ID generation in storage dialects Jul 30, 2026
@cursor
cursor Bot force-pushed the cursor/dialect-owned-id-generation-a541 branch from 055e574 to 89db301 Compare July 30, 2026 12:23
Comment thread internal/domain/flow_state_machine.go Outdated
Comment thread internal/service/flow_test.go Outdated
Comment thread internal/storage/v2/dialect/idgen/ensure.go
CreateSigningKey left ID empty after domain stopped minting, which broke
project create. Mirror encryption-key Ensure, and restore CreateToken's
reject-if-set check before dialect minting.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Purpose-scoped JWEs store the KEK id as kid; without a pre-minted KEK
id, GetProjectCrypter failed with enc_key.not_found after project create.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Main claimed ADR 046 for claim lifecycle v2. Keep that number and move
dialect-owned ID generation to ADR 047, updating cross-references.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Remove coerce*Identity aliases, IsNumeric/int Scan leftovers, and
narrative comments; share prefix normalization; mint Spanner challenge
IDs only on insert.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
cursoragent and others added 2 commits August 3, 2026 11:54
Resolve conflicts keeping dialect-owned string IDs and SQLite as the
unconfigured local default. Drop embedded-postgres default path.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
…IDs (ULID)

Replace all INTEGER AUTOINCREMENT primary/foreign keys in the SQLite
dialect with TEXT ULIDs minted by ensureManagedID, matching the
Postgres dialect pattern.

Changes:
- session.go: ensureManagedID for session + user-agent IDs; scan all
  IDs as strings; database.CoerceString in sessionSchema; remove
  strconv dependency
- auth_attempt.go: ensureManagedID for attempt + challenge IDs;
  ON CONFLICT sets id = excluded.id; scan IDs as strings
- token.go: ensureManagedID for token_id (TokenPrefix); string session
  FKs; CoerceString in tokenSchema; remove coerceTokenInt64
- user_password.go: ensureManagedID with PrefixUserPassword
- user_totp.go: ensureManagedID with PrefixUserTOTP
- user_recovery_codes.go: ensureManagedID with PrefixUserRecoveryCodes
- user_passkey.go: ensureManagedID with PrefixUserPasskey
- crypto_keys.go: ensureManagedID with PrefixEncryptionKey / PrefixSigningKey
- project.go: ensureManagedID with PrefixProject
- team.go: replace empty-ID error with ensureManagedID; drop errors import
- user.go: ensureManagedID with PrefixUser
- branding.go: ensureManagedID with PrefixBranding
- flow_definition.go: ensureManagedID with PrefixFlowDefinition
- passkey_registration.go: ensureManagedID with PrefixPasskeyRegistration
- identity.go: deleted (parseIdentity had no remaining callers)

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Rewrite the SQLite init migration so resource PKs/FKs are TEXT (no
AUTOINCREMENT), mint JSON schema URLs via ensureManagedID, and document
SQLite ULID alongside Postgres in ADR 047 / 028 and the changeset.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Comment thread internal/storage/v2/dialect/sqlite/migration/sql/000001_init.sql Outdated
Comment thread internal/storage/v2/dialect/sqlite/migration/sql/000001_init.sql Outdated
Comment thread internal/storage/v2/dialect/sqlite/migration/sql/000001_init.sql Outdated
Comment thread internal/storage/v2/dialect/sqlite/migration/sql/000001_init.sql Outdated
Comment thread internal/storage/v2/dialect/sqlite/migration/sql/000001_init.sql Outdated
Comment thread internal/storage/v2/dialect/sqlite/migration/sql/000001_init.sql Outdated
cursoragent and others added 2 commits August 3, 2026 12:38
Make sessions.token_id nullable across dialects and retire the "0"
sentinel. Drop redundant Create-supplied SQL DEFAULTs, remove unused
project_secret/preview_secret columns, and add a non-empty recovery
codes check on SQLite.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
SeedProjectsTiedAt for Postgres/Spanner omitted preview_origins after the
column DEFAULT was removed, so ListCursorTie failed with NOT NULL. Match
the SQLite seed and always insert an empty origins value.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>

@livio-a livio-a left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — zero-exception dialect-minted PKs (ADR 047), SQLite/Postgres ULID + Spanner UUID, ceremony pre-mint, create-user reject, and the token_id/DEFAULT cleanup all look coherent. Happy to merge after a rebase onto current main (still a few commits behind). Squash is fine.

Nits below are non-blocking. On ID prefixes: I'm fine deferring renames of existing tokens (att, upk, enc_key, brnd, …). What I don't want is merging without a short "how we pick prefixes" rule and then breaking the wire again a week later when the next u*/consonant prefix lands. So: document + freeze new additions now; rename legacy later in one intentional pass.

Also (no good diff anchor): FlowDefinitionPrefix and PrefixFlowDefinition are both "flowdef" — collapse to one name (dialects currently disagree on which they call).

Optional sanity note: only create-user needs an explicit client-id reject (open map). Other creates have closed bodies — looks correct.

Before merge: rebase onto main. Squash OK.

Comment thread docs/adrs/047-dialect-id-generation.md
Comment thread internal/domain/resource.go Outdated
Comment thread internal/service/flow.go Outdated
Comment thread internal/service/branding_test.go Outdated
Comment thread internal/storage/v2/dialect/idgen/ensure.go
Comment thread internal/storage/v2/dialect/postgres/managed_id.go
cursoragent and others added 3 commits August 3, 2026 20:35
Add ADR 047 prefix selection rules, statements-only ID minting in
AGENTS.md, soften ResourcePrefix docs, drop v2database aliases, and
align usr_/htok_ fixtures with user_/handoff_.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Align Create empty-id with project_test now that team suites live in
stmttest after the main merge.

Co-authored-by: Silvan <adlerhurst@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants