Skip to content

Who Owns the User Row?

You already have a users table. Two dozen foreign keys point at it, half your queries JOIN against it, and your auth middleware turns its rows into principals. Now you’re adopting Auther, a centralized identity provider — and suddenly a third party owns identity. So who owns the row?

That’s the question this whole part of the course turns on. When you adopt a centralized IdP, you stop owning authentication: Auther runs sign-up, sign-in, password hashes, 2FA, session tokens, and key rotation. Your app gets a signed JWT with claims, and that’s it. (If you haven’t yet, the Auther overview explains what the server itself does.)

But your app database is not a blank slate, and you can’t hand it a JWT and walk away. The one decision that makes the entire integration tractable is this: for every piece of data on the user row, decide who is allowed to be the source of truth. Get that right and schema, sync, and failure handling all fall out of it.

In the running reference example — the hono-node-template-2026 monorepo — the schema you start with looks like this. The auth-related tables come straight from better-auth; the rest are ordinary domain tables with foreign keys back to users.id.

packages/db/src/schema/auth.ts
users (id, email, name, emailVerified, image, status, roleSlugs, ...)
sessions (user_id → users.id ON DELETE CASCADE, platform, activeOrganizationId, ...)
accounts (user_id → users.id ON DELETE CASCADE, password, providerId, ...)
verifications (identifier, value, expiresAt)
two_factors (user_id → users.id ON DELETE CASCADE)
jwks (public_key, private_key, ...)
packages/db/src/schema/*
notifications (user_id → users.id ON DELETE CASCADE)
push_tokens (user_id → users.id ON DELETE CASCADE)
notification_preferences (user_id → users.id ON DELETE CASCADE)
audit_logs (actor_id → users.id)
organizations (owner_id → users.id)
members (user_id, org_id)
auth_relations (subject_id, object_id, relation) -- plain text

Every one of those arrows assumes the row on the other end exists locally. And your application code is riddled with references that assume the same — filters, lookups, cascading deletes, and the helper that turns a user into a principal:

apps/server/src/modules/notifications/service.ts
const conditions: SQL[] = [eq(notifications.userId, userId)];
// apps/server/src/modules/users/service.ts
db.query.users.findFirst({ where: eq(users.id, id) });
// apps/server/src/modules/auth/instance.ts
await db.delete(schema.sessions).where(eq(schema.sessions.userId, session.userId));
// apps/server/src/auth/middleware.ts
const principal = principalFromUser(c.get("user"));

Now Auther wants to run identity. Faced with that, every engineer reaches for one of two shortcuts first — and both are traps.

The way out is neither “all Auther” nor “all local” — it’s both, decided one column at a time. The correct framing is ownership: for each column on the user row, pick a single source of truth. Some fields are pure identity and belong to Auther; some are pure product state and stay in your app; a few are genuinely split.

Done for the whole row, that decision is the centerpiece of the entire integration — this one table is the contract everything else is built on:

ColumnAuthoritative inNotes
idAutherLocal row uses Auther’s ID verbatim as PK
email, emailVerifiedAutherYour app mirrors for joins/display
name, imageAutherYour app mirrors
password / argon2id hashAutherYour app never stores
2FA secret / backup codesAutherYour app never stores
status (active/inactive)AutherAuther’s admin plugin (banned + banReason)
failedLoginAttemptsAutherAuther’s login-security plugin
lockedUntilAutherSame
twoFactorEnabledAutherInferable from Auther’s userinfo endpoint
onboardingCompletedAtYour appPure product state; Auther doesn’t know about it
notificationPreferencesYour appYour app
roleSlugsSplitPlatform roles → Auther; app-domain roles → your app
activeOrganizationIdYour appTenant selection is an app concern (see the Edge Cases Catalog)

With ownership decided, the next question is structural: should a local users table exist at all, and if so, what lives in it? That’s the subject of the four integration archetypes.