Skip to content

fix(drizzle-kit): reference authUsers for FKs into Supabase auth.users#5979

Open
dmitriy-bty wants to merge 1 commit into
drizzle-team:mainfrom
dmitriy-bty:fix/4482-supabase-authusers-fk
Open

fix(drizzle-kit): reference authUsers for FKs into Supabase auth.users#5979
dmitriy-bty wants to merge 1 commit into
drizzle-team:mainfrom
dmitriy-bty:fix/4482-supabase-authusers-fk

Conversation

@dmitriy-bty

Copy link
Copy Markdown

Summary

Fixes the primary defect in #4482: drizzle-kit pull against a Supabase Postgres database
generates a schema.ts that does not compile, because every foreign key that references
Supabase's managed auth.users.id is emitted as a reference to a local table that was never
declared
.

drizzle-orm already ships the canonical auth.users definition as authUsers in the
drizzle-orm/supabase entrypoint. The pull emitter simply never referenced it for cross-schema
foreign keys into the auth schema. This PR makes the emitter render such FKs as the imported
authUsers and add the corresponding import.

This fixes both files a pull writes — schema.ts and relations.ts — which were broken by the
same root cause.

Before (actual output of drizzle-kit pull, verified against a live Postgres)

import { pgTable, foreignKey, uuid } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"

export const profiles = pgTable("profiles", {
	id: uuid().defaultRandom().primaryKey().notNull(),
	userId: uuid("user_id").notNull(),
}, (table) => [
	foreignKey({
		columns: [table.userId],
		foreignColumns: [users.id],   // ❌ `users` is never declared / imported → TS error
		name: "profiles_user_id_fkey"
	}),
]);

After

import { pgTable, foreignKey, uuid } from "drizzle-orm/pg-core"
import { sql } from "drizzle-orm"
import { authUsers } from "drizzle-orm/supabase"

export const profiles = pgTable("profiles", {
	id: uuid().defaultRandom().primaryKey().notNull(),
	userId: uuid("user_id").notNull(),
}, (table) => [
	foreignKey({
		columns: [table.userId],
		foreignColumns: [authUsers.id],     // ✅ references the shipped table
		name: "profiles_user_id_fkey"
	}),
]);

Root cause

  • The FK introspection query records the target schema (pgSerializer.ts:1275,
    fnsp.nspname AS foreign_table_schema; surfaced as schemaTo at pgSerializer.ts:1317), so the FK
    correctly carries { tableTo: 'users', schemaTo: 'auth' }.
  • But pull only introspects the requested schema(s) — public by default (the schema filter is
    built in fromDatabase at pgSerializer.ts:991, n.nspname = '<schema>'), so the auth.users
    table is never emitted as a local declaration.
  • The TypeScript emitter still renders the FK. In createTableFKs (drizzle-kit/src/introspect-pg.ts)
    the reference is built via paramNameFor(it.tableTo, schemas[it.schemaTo]). Because the auth
    schema was not pulled it is absent from the schemas map, so schemas['auth'] is undefined and
    paramNameFor('users', undefined) returns the bare users (the In<Schema> suffix is only added
    for a defined non-public schema — paramNameFor, introspect-pg.ts:304-305). The result references
    a users variable that does not exist in the generated file.

authUsers is defined at drizzle-orm/src/supabase/rls.ts:14 (auth.table('users', { id: uuid()... }))
and re-exported from the drizzle-orm/supabase entrypoint — exactly what a Supabase user is expected
to reference.

Implementation

schema.ts emitter (drizzle-kit/src/introspect-pg.ts):

  1. New predicate isSupabaseAuthUsersFk(fk)fk.schemaTo === 'auth' && fk.tableTo === 'users'.
  2. In schemaToTypeScript, compute usesSupabaseAuthUsers: true when at least one FK targets
    auth.users and auth.users is not itself part of the introspected tables (so users who
    explicitly include the auth schema in their filter keep the previous local-table behavior).
  3. createTableFKs takes a supabaseAuthUsers flag; when set and the FK matches, it renders
    authUsers (instead of the bare, undeclared users). This Supabase branch is evaluated before
    the existing self-FK check — that check compares table names only, so a local table also named
    users (a common public.usersauth.users mirror) would otherwise be mis-emitted as a
    self-reference. The reorder is safe because usesSupabaseAuthUsers is only true when auth.users
    was not emitted, so a referencing table named users is necessarily public.users, never a
    genuine self-reference into an emitted auth.users.
  4. The import block conditionally emits import { authUsers } from "drizzle-orm/supabase".

relations.ts emitter (drizzle-kit/src/cli/commands/introspect.ts):

A full pull also writes relations.ts via a separate emitter with the same defect — it rendered the
FK target as usersInAuth imported from "./schema", a name the fixed schema.ts no longer declares,
so relations.ts failed to compile too. Applying the same guarded mapping (identical auth.users
detection), it now renders authUsers and routes that single import to drizzle-orm/supabase, while
every other table still imports from ./schema.

Both changes are additive and guarded, so non-Supabase schemas and schemas that explicitly pull the
auth schema are unaffected (byte-identical output).

On the detection signal: this keys on the auth.users schema/table-name heuristic. drizzle-kit
also exposes an explicit Supabase marker (roles.provider === 'supabase', pgSerializer.ts:940); I
kept the name heuristic because many Supabase projects don't introspect roles, but I'm happy to gate
on/combine with the provider signal if you'd prefer. The only downside of the pure heuristic is a
non-Supabase database that has its own auth.users and is pulled without the auth schema would get
a spurious authUsers import — a narrow case that does not regress compiling output (that pull
already emitted a dangling users).

Validation

  • Two introspect tests added in drizzle-kit/tests/introspect/pg.test.ts:
    1. introspect FK to Supabase auth.users references authUsers… — seeds an auth schema +
      auth.users and a public.profiles table with a FK into auth.users(id), introspects only the
      public schema (the real Supabase workflow), and asserts the generated file contains
      import { authUsers } from "drizzle-orm/supabase" and foreignColumns: [authUsers.id] and no
      longer contains the undeclared foreignColumns: [users.id]. It also asserts the separately-emitted
      relations.ts imports authUsers from drizzle-orm/supabase and no longer references usersInAuth.
    2. …a local table also named "users" is not treated as a self-reference — the public.users
      auth.users mirror case; asserts the FK resolves to authUsers.id and is not collapsed to a
      bogus foreignColumns: [table.id].
  • Reproduced live against Postgres 16 (Docker) with the published drizzle-kit: pull emits
    foreignColumns: [users.id] referencing an undeclared, un-imported users — confirming the issue.
    Copy-pasteable SQL seed + pull command are in the repro notes.
  • Note on local test execution: I could not run the suite locally — the monorepo install is
    currently blocked by an unrelated upstream issue (drizzle-kit dev-depends on a drizzle-kit
    version whose tarball 404s on npm, and drizzle-kit consumes drizzle-orm as a built dist). The
    change is therefore verified by code inspection plus the live reproduction above; please run CI /
    dprint fmt on the added tests. Happy to adjust formatting if the linter flags the reordered
    ternary.

Adjacent issues noted (not fixed here)

The report lists two further defects that are genuinely separate emitter bugs. I root-caused them but
scoped them out; happy to open follow-ups:

  • .default([RAY]) garbled array default (Update README docs #2 in the report). A separate emitter bug in
    array-default rendering (buildArrayDefault, introspect-pg.ts:657), which strips two characters
    from each end of curly-form array-literal defaults. The exact input that produces the reported
    [RAY] needs confirming against the reporter's column default before I'd propose a fix — noted for
    a follow-up, not addressed here.
  • Missing .enableRLS() (Feature/last updates #3 in the report). Introspection captures RLS state
    (pgSerializer.ts:1663, isRLSEnabled: row.rls_enabled) but the introspect-pg.ts emitter has no
    enableRLS/isRLSEnabled references, so the flag is dropped. Fix direction: append .enableRLS()
    in the table statement builder when table.isRLSEnabled.

When pulling a Supabase database, foreign keys that target the managed
auth.users table were rendered as a reference to an undeclared local
table, because the auth schema is excluded from the schema filter and
never emitted (the FK reference resolves to a bare "users" that is
neither declared nor imported). drizzle-orm already ships this table as
authUsers in the drizzle-orm/supabase entrypoint.

The pg introspection emitter now renders such foreign keys as authUsers
and adds the import { authUsers } from "drizzle-orm/supabase" statement,
but only when auth.users itself was not introspected.
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.

1 participant