users table into Databunker Pro. The whole user record goes into the vault; your application database keeps nothing but a token.
For the phases and mechanics shared by every migration, see the migration overview.
Rather than altering users in place, you build a parallel newusers table, backfill it while the application keeps running, verify it, and then swap the two tables in a single transaction. Nothing is destructive until that swap, and the swap itself is reversible.
Timing comes from the performance benchmarks: bulk writes sustain roughly 5,800 records/sec on a single instance, so 10 M records take about 28 minutes and 100 M around 2.3 hours. At tens of millions of rows the database is the limit, not Databunker Pro — see the sizing guide before starting a large backfill.
Before you start
- A running Databunker Pro instance and a root or admin access token.
- A license whose record cap covers your row count. Trial mode stops at 1,000 records, and the cap is checked when a bulk request begins — see when the record cap is reached.
- The Python SDK:
pip install databunkerpro psycopg2-binary - A backup of the users table.
Step 1: Create the newusers table
Two columns: your existing primary key, and the token Databunker Pro returns.users goes into the encrypted vault profile — not just the personal data, but every application field as well: created_at, status, plan, and the rest. A 120-field profile is entirely normal; that is what the benchmarks were run against.
Keeping the record whole is the point. Split it across two stores and you are back to reconciling two sources of truth for the same user, with some attributes protected and some not.
Step 2: Decide how tokens map back to rows
UserCreateBulk returns the original profile alongside each token, so the mapping never depends on response ordering. To use that, carry your primary key inside the profile.
The custom field is the best place for it. It is one of the four indexed fields and is unique within a tenant, which buys you two things:
- A re-run cannot create duplicate tokens. A second attempt for the same
idis rejected withDuplicate custom valueinstead of silently minting a second token. - You can look a user up by legacy id later with
UserGetusingmode=custom.
custom is then spoken for. If you need it for something else, use a plain unindexed field such as legacy_userid — the mapping still works, but you lose the duplicate protection above.
Step 3: Backfill in chunks
The script counts the rows, walks the table in chunks of 4,000, sends each chunk toUserCreateBulk, and writes id plus the returned token into newusers. Failures are written to a CSV for review.
Each chunk prints its offset before processing it, so if a run is interrupted you can see exactly where it stopped and resume from there:
ON CONFLICT DO NOTHING guards newusers, but it re-sends millions of profiles and fills the error log with duplicate reports that bury the genuine conflicts you need to act on.
Step 4: Verify
1
Every row made it across
migration-errors.csv.2
Spot-check detokenization
Confirm the vault returns what the old table holds:
3
Confirm the vault count
SystemGetSystemStats — totalnumrecords should have grown by the number you migrated. See checking your usage.Recover from an interrupted run
If the counts do not match, some rows reached the vault but their tokens were never written locally — the script died between the two writes. Becausecustom holds the legacy id, those tokens can be looked up again in one batch call:
BulkListUsers evaluates every criterion you send and is not paginated, so the whole missing set resolves in a single request. Ids with no vault record are simply absent from rows — those never reached the vault, so find them in migration-errors.csv and resolve the conflict there.
This call requires the
list_users configuration flag to be enabled, otherwise it returns 403 BulkListUsers is disabled. The unlockuuid is short-lived, so request it immediately before the lookup — and for a very large missing set, work in batches with a fresh unlock for each.Step 5: Point the application at the vault
Do this before swapping anything.- Dual-write. On user creation, call
UserCreateand insert the returned token intonewusersin the same transaction as theusersrow, so new users appear in both tables. - Switch reads to fetch the profile from the vault by token. Joins that used
emailcan join onusertoken— tokens are stable within a tenant, so this needs no round trip to the vault. - Run in this mode for a full business cycle. Any code still reading a column directly surfaces here, while the data is present and recovery is free.
Step 6: Swap the tables
Once step 5 has run clean. In PostgreSQL, DDL is transactional, so both renames commit together and the cutover is effectively instant:users_old until you are satisfied, then drop it:
Dropping
users_old removes the personal data from the live table, but not from disk or from history. The values remain in dead tuples, the write-ahead log, replicas, and every backup taken before this point. Treat the PII as still present until VACUUM FULL has run and those backups have expired.Rolling back
Next steps
- Performance and sizing — size the database before a large backfill
- Licensing and limits — what counts as a record
- Clean the users table — reset a test vault between rehearsals