← Back to blog

Keep OAuth account identity stable during schema changes


An OAuth login finds an account by provider identity, not by the display email shown on a profile. In Own Auth, the stable lookup is the provider plus its provider account identifier. A schema or provider upgrade that changes either value can leave valid users unable to sign in even when no request throws an application error.

Define the identity key before changing data

For OpenID Connect providers, the subject is meaningful within the issuer namespace. Record the exact old and new rule that creates the Own Auth provider and providerAccountId values. Do not substitute email as the durable key. Email can change, may be absent, and can identify a different account after reassignment.

Own Auth owns the own_auth_accounts table and its ordered migrations. Do not add an application migration that rewrites those records independently. Upgrade through the package migration that defines the new identity rule. Use read only queries before and after the migration to prove that every affected identity remains unique and maps to the intended user.

oauth-identity-preflight.sql
select provider, provider_account_id, count(*) as matches
from own_auth_accounts
where provider in ('google', 'github', 'apple')
group by provider, provider_account_id
having count(*) > 1;

select provider, count(*) as accounts
from own_auth_accounts
where provider in ('google', 'github', 'apple')
group by provider
order by provider;

The duplicate query must return no rows before a unique constraint is added or replaced. Save the provider totals and compare them after the backfill. Totals alone are not enough, but an unexpected change is a clear stop signal.

Backfill before enforcing the new key

A safe package migration first adds any new nullable identity field or mapping record. It then backfills in bounded batches, records rows that cannot be mapped, verifies uniqueness under the new rule, and only then makes the field required or changes the unique index. Application traffic should continue reading the old identity until the backfill is complete, or read both forms during a deliberately bounded transition supplied by the package.

Do not resolve an ambiguous row automatically. Two old identities that collapse to one new key need a product decision and an account recovery path. Guessing can attach a provider account to the wrong Own Auth user. Stop the rollout, preserve both records, and resolve the ownership conflict with verified account evidence.

Exercise the verified identity path

A migration check should use a credential verified by the real provider adapter or a controlled test adapter with the same issuer and subject rules. Pass the normalized identity to auth.signInWithVerifiedExternalIdentity. Narrow the returned status because a user with MFA enabled receives a challenge instead of a session.

oauth-login-check.ts
const identity = await provider.verifyCredential(testCredential);

const result = await auth.signInWithVerifiedExternalIdentity({
  provider: "google",
  providerAccountId: identity.subject,
  email: identity.email,
  emailVerified: identity.emailVerified,
  name: identity.name,
});

if (result.status === "mfa_required") {
  assert(result.methods.length > 0);
} else {
  assert.equal(result.user.id, expectedUserId);
  await auth.signOut({ sessionToken: result.sessionToken });
}

The assertion must compare the returned user ID with the user ID recorded before migration. A successful HTTP response is not enough. The failure mode in an identity migration is often a new account, an account linking prompt, or a lookup miss that still returns a handled response.

Release the change in reversible stages

Rehearse the package upgrade against a restored production shaped database. Record the migration duration, unmapped rows, duplicate results, and login results for each enabled provider. Deploy schema support before code that requires it. Keep the previous application artifact available until the new login journey passes in production.

Rollback depends on the stage. Before the final constraint, application code can usually return to the old read rule while the additive data remains. After identities are rewritten or old fields are removed, a code rollback may no longer be safe. The release plan must name the last reversible point and the database restore or forward repair needed after it.

Include linked accounts in the rehearsal

A user can have several provider identities and more than one identity from the same provider. The rehearsal should include users with password plus OAuth, two OAuth providers, an account created through automatic verified email linking, and an identity that previously returned an account linking conflict. Confirm that the migration preserves each relationship without merging users.

Exercise unlinking after migration as well as sign in. The exact provider account identifier selected on the account security screen must remove the intended identity and leave other methods untouched. A data change that preserves login but makes management target the wrong record is still defective.

Canary the complete callback

Run one controlled login for every enabled provider after release. Start at the public sign in screen, complete the real provider callback, verify the expected Own Auth user ID, load one harmless authenticated page, and sign out. Use a dedicated synthetic account whose provider identity follows the same rules as customer accounts.

Monitor provider starts, callbacks, completed sessions, MFA challenges, account linking requirements, and identity conflicts by provider. Never log authorization codes, provider tokens, session tokens, or raw identity assertions. Alert on a fall in completed sessions even when callback error counts remain flat, because a silent lookup change can fail without a thrown exception.