Skip to content

How-to

Migrating from in-house auth

The hard part of this migration is not the code. It is that your existing users must not notice — no reset email, no lost session, no support queue on the morning of the cutover. That is achievable, and it comes from sequencing rather than cleverness.

Start with an inventory

Before moving anything, write down what you actually have. Most auth systems have accumulated more than their owners remember, and every surprise found now is one not found during the cutover.

  • How many accounts, and how many have signed in during the last year. Dormant accounts are the majority in most products and can often be migrated cold.
  • Which password hashing scheme, with which parameters. This decides whether passwords can move at all.
  • Every way a session can be created: the login form, a mobile app, an API token, a magic link, an admin impersonation path, a legacy endpoint somebody forgot.
  • Every place a user ID appears: foreign keys, external systems, exported reports, support tooling, analytics.
  • Accounts that are not people — service accounts, test accounts, shared logins. They break assumptions during import.

The impersonation path is the one most often forgotten, and it is also the one with the highest blast radius if it silently stops enforcing something after the migration.

What happens to passwords

If your hashes are bcrypt, scrypt or Argon2 with standard parameters, they can be imported as they are. People keep their passwords, sign in normally, and nothing about the migration is visible to them.

If your scheme is unusual, home-grown or unsalted, the honest answer is that those passwords should not move. Import the accounts without credentials and let those users set a password at their next sign-in through a recovery flow — which is a better outcome than preserving a weak hash.

Existing schemeMigratesWhat users experience
bcryptYesNothing — they sign in as usual
scryptYesNothing
Argon2idYesNothing
PBKDF2Yes, with parametersNothing
Unsalted MD5 or SHA-1Not worth itOne-time password set, prompted at sign-in
Custom or unknownNoOne-time password set, prompted at sign-in

Import in batches

Import into a staging environment first, in batches, and reconcile counts after each one. Keep your own identifier on the imported record so that mapping between old and new is a lookup rather than a guess.

scripts/import-users.ts
1import { Paycux } from "@paycux/node";
2
3const paycux = new Paycux(process.env.PAYCUX_API_KEY);
4
5export async function importBatch(rows) {
6 const results = [];
7
8 for (const row of rows) {
9 try {
10 const user = await paycux.userManagement.createUser({
11 email: row.email,
12 firstName: row.firstName,
13 lastName: row.lastName,
14 emailVerified: row.emailVerifiedAt !== null,
15 passwordHash: row.passwordHash,
16 passwordHashType: "bcrypt",
17 externalId: row.id,
18 });
19
20 results.push({ legacyId: row.id, userId: user.id, ok: true });
21 } catch (error) {
22 // once tumunu topla, sonra incele — tek satir icin duraklamayin
23 results.push({ legacyId: row.id, ok: false, reason: error.message });
24 }
25 }
26
27 return results;
28}

Expect a failure rate. Duplicate emails differing only by case, addresses that were never valid, and accounts created by a script years ago will all surface here. Deciding what to do with them is a product question, not a technical one.

Dual-run before you cut over

The safest migration has a period where both systems are authoritative and one of them is silently checked. Authenticate against your existing system as usual, and in parallel verify that Paycux would have reached the same answer.

Run this for long enough to cover a full weekly cycle — including whatever your product does on a Monday morning — and read the mismatch log daily. Every mismatch is a case you had not thought of.

  • Sign-in succeeds in both. Expected.
  • Sign-in succeeds locally and fails in Paycux. Almost always an import gap: a missing account, a hash that did not transfer, a case-sensitivity difference.
  • Sign-in fails locally and succeeds in Paycux. Rare, and worth understanding before proceeding — usually a lockout rule that exists in one system and not the other.
  • Both fail. Expected for a wrong password, and a useful sanity check that the comparison is really running.

The cutover

Once the mismatch log is quiet, the switch itself is small. What makes it safe is that it is reversible for a period, and that sessions are handled deliberately rather than left to chance.

  1. 1Run a final delta import for accounts created or changed since the last batch.
  2. 2Switch the sign-in path to Paycux, keeping the old code behind a flag rather than deleting it.
  3. 3Leave existing sessions alone. Signing everybody out simultaneously is the most visible thing you could do, and it is not necessary — let them expire naturally onto the new path.
  4. 4Watch sign-in success rate, not error count. A drop of two percent is a real problem that an error dashboard will not show you.
  5. 5After a week of clean numbers, remove the old path and the flag. Leaving a second authentication route in place indefinitely is its own risk.

What you get afterwards

The migration is worth doing for what it unlocks rather than for its own sake. With identity in one place, enterprise SSO becomes a configuration record instead of a project, directory provisioning becomes a webhook handler, and the next security questionnaire has answers that are already true.

It is also the right moment to fix things you have been avoiding: a stable external identifier instead of email as a key, MFA that was always going to be next quarter, and an audit trail of authentication events that a customer can read.