Repository files navigation

Clerk User Migration Tool

Description

This repository contains a tool that takes a JSON file as input, containing a list of users, and creates a user in Clerk using Clerk's backend API. The tool respects rate limits and handles errors.

Table of Contents

Documentation

Getting Started

Clone the repository and install the dependencies.

git clone git@github.com:clerk/migration-tool
cd migration-tool
bun install

Users file

The tool is designed to import from multiple sources, including moving users from one Clerk instance to another. You may need to edit the transformer for your source. Please see below for more information on that.

The tool will import from a CSV or JSON. It accounts for empty fields in a CSV and will remove them when converting from CSV to a javascript object.

The only required fields are userId and an identifier (one of email, phone or username).

Samples

The samples/ folder contains some samples you can test with. The samples include issues that will produce errors when running the import.

Some sample users have passwords. The password is Kk4aPMeiaRpAs2OeX1NE.

Secret Key

You have several options for providing your Clerk secret key:

Option 1: Create a .env file (recommended for repeated use)

CLERK_SECRET_KEY=your-secret-key

Option 2: Pass via command line (useful for automation/AI agents)

bun migrate --clerk-secret-key sk_test_xxx

Option 3: Set environment variable

export CLERK_SECRET_KEY=sk_test_xxx
bun migrate

Option 4: Enter interactively

If no key is found, the interactive CLI will prompt you to enter one and optionally save it to a .env file.

You can find your secret key in the Clerk Dashboard under API Keys.

Run the tool

bun migrate

The tool will begin processing users and attempting to import them into Clerk. The tool respects rate limits for the Clerk Backend API. If the tool hits a rate limit, it will wait 10 seconds and retry (up to 5 times). Any errors will be logged to timestamped log files in the ./logs folder.

The tool can be run on the same data multiple times. Clerk automatically uses the email as a unique key so users won't be created again.

Error Handling & Resuming: If the migration stops for any reason (error, interruption, etc.), the tool will display the last processed user ID. You can resume the migration from that point by providing the user ID when prompted, or by using:

bun migrate --resume-after="user_xxx"

CLI Reference

The migration tool supports both interactive and non-interactive modes.

Usage

bun migrate [OPTIONS]

Options

OptionDescription
-t, --transformer <transformer>Source transformer (clerk, auth0, authjs, firebase, supabase)
-f, --file <path>Path to the user data file (JSON or CSV)
-r, --resume-after <userId>Resume migration after this user ID
--require-passwordOnly migrate users who have passwords (by default, users without passwords are migrated)
-y, --yesNon-interactive mode (skip all confirmations)
-h, --helpShow help message

Authentication Options

OptionDescription
--clerk-secret-key <key>Clerk secret key (alternative to .env file)

Firebase Options

Required when --transformer is firebase:

OptionDescription
--firebase-signer-key <key>Firebase hash signer key (base64)
--firebase-salt-separator <sep>Firebase salt separator (base64)
--firebase-rounds <num>Firebase hash rounds
--firebase-mem-cost <num>Firebase memory cost

Examples

# Interactive mode (default)
bun migrate
# Non-interactive mode with required options
bun migrate -y -t auth0 -f users.json
# Non-interactive with secret key (no .env needed)
bun migrate -y -t clerk -f users.json --clerk-secret-key sk_test_xxx
# Resume a failed migration
bun migrate -y -t clerk -f users.json -r user_abc123
# Firebase migration with hash config
bun migrate -y -t firebase -f users.csv \
--firebase-signer-key "abc123..." \
--firebase-salt-separator "Bw==" \
--firebase-rounds 8 \
--firebase-mem-cost 14

Non-Interactive Mode

For automation and AI agent usage, use the -y flag with required options:

bun migrate -y \
--transformer clerk \
--file users.json \
--clerk-secret-key sk_test_xxx

Required in non-interactive mode:

  • --transformer (or -t)
  • --file (or -f)
  • CLERK_SECRET_KEY (via --clerk-secret-key, environment variable, or .env file)

Exporting Users

Some platforms require exporting users directly from their database before migrating. See the Exporting Users guide for setup, CLI options, and troubleshooting.

bun export:supabase

Migrating OAuth Connections

OAuth connections can not be directly migrated. The creation of the connection requires the user to consent, which can't happen on a migration like this. Instead you can rely on Clerk's Account Linking to handle this.

Handle Existing User IDs and Foreign Key Constraints

When migrating from another authentication system, you likely have data in your database tied to your previous system's user IDs. To maintain data consistency as you move to Clerk, you'll need a strategy to handle these foreign key relationships. Below are several approaches.

Custom session claims

Our sessions allow for conditional expressions. This would allow you add a session claim that will return either the externalId (the previous id for your user) when it exists, or the userId from Clerk. This will result in your imported users returning their externalId while newer users will return the Clerk userId.

In your Dashboard, go to Sessions -> Edit. Add the following:

{
"userId": "{{user.externalId || user.id}}"
}

You can now access this value using the following:

const{ sessionClaims }=auth();console.log(sessionClaims.userId);

You can add the following for typescript:

// types/global.d.tsexport{};declareglobal{interfaceCustomJwtSessionClaims{userId?: string;}}

Other options

You could continue to generate unique ids for the database as done previously, and then store those in externalId. This way all users would have an externalId that would be used for DB interactions.

You could add a column in your user table inside of your database called ClerkId. Use that column to store the userId from Clerk directly into your database.

Configuration

The tool can be configured through the following environment variables:

VariableDescription
CLERK_SECRET_KEYYour Clerk secret key
RATE_LIMITRate limit in requests/second (auto-configured: 100 for prod, 10 for dev)
CONCURRENCY_LIMITNumber of concurrent requests (auto-configured: ~9 for prod, ~1 for dev)

The tool automatically detects production vs development instances from your CLERK_SECRET_KEY and sets appropriate rate limits and concurrency:

  • Production (sk_live_*):
    • Rate limit: 100 requests/second (Clerk's limit: 1000 requests per 10 seconds)
    • Concurrency: 9 concurrent requests (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~35 seconds
  • Development (sk_test_*):
    • Rate limit: 10 requests/second (Clerk's limit: 100 requests per 10 seconds)
    • Concurrency: 1 concurrent request (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~350 seconds

You can override these values by setting RATE_LIMIT or CONCURRENCY_LIMIT in your .env file.

Tuning Concurrency: If you want faster migrations, you can increase CONCURRENCY_LIMIT (e.g., CONCURRENCY_LIMIT=15 for ~150 req/s). Note that higher concurrency may trigger rate limit errors (429), which are automatically retried.

Commands

Run migration

bun migrate

Delete users

bun delete

This will delete all migrated users from the instance. It should not delete pre-existing users, but it is not recommended to use this with a production instance that has pre-existing users. Please use caution with this command.

Clean logs

bun clean-logs

All migrations and deletions will create logs in the ./logs folder. This command will delete those logs.

Convert logs from NDJSON to JSON

bun convert-logs

Convert Logs Utility

Converts NDJSON (Newline-Delimited JSON) log files to standard JSON array format for easier analysis in spreadsheets, databases, or other tools.

Usage

bun convert-logs

The utility will:

  1. List all .log files in the ./logs directory
  2. Let you select which files to convert
  3. Create corresponding .json files with the converted data

Example

Input (migration-2026-01-27T12:00:00.log):

{"userId":"user_1","status":"success","clerkUserId":"clerk_abc123"}
{"userId":"user_2","status":"error","error":"Email already exists"}
{"userId":"user_3","status":"fail","error":"invalid_type for required field.","path":["email"],"row":5}

Output (migration-2026-01-27T12:00:00.json):

[
{
"userId": "user_1",
"status": "success",
"clerkUserId": "clerk_abc123"
},
{
"userId": "user_2",
"status": "error",
"error": "Email already exists"
},
{
"userId": "user_3",
"status": "fail",
"error": "invalid_type for required field.",
"path": ["email"],
"row": 5
}
]

Why NDJSON for Logs?

The tool uses NDJSON for log files because:

  • Streaming: Can append entries as they happen without rewriting the file
  • Crash-safe: If the process crashes, all entries written so far are valid
  • Memory efficient: Can process line-by-line without loading entire log
  • Scalable: Works efficiently with thousands or millions of entries
  • Real-time: Can monitor with tail -f and see entries as they're written

When to Convert

Convert logs to JSON arrays when you need to:

  • Import into Excel, Google Sheets, or other spreadsheet tools
  • Load into a database for analysis
  • Process with tools that expect JSON arrays
  • Share logs with team members less familiar with NDJSON

Analyzing Logs

With NDJSON (original format)

# Count successful imports
grep '"status":"success"' logs/migration-2026-01-27T12:00:00.log | wc -l
# Find all errors
grep '"status":"error"' logs/migration-2026-01-27T12:00:00.log
# Get specific user
grep '"userId":"user_123"' logs/migration-2026-01-27T12:00:00.log

With JSON Arrays (converted format)

// Load in Node.js/JavaScriptconstlogs=require('./logs/migration-2026-01-27T12:00:00.json');// Filter successful importsconstsuccessful=logs.filter((entry)=>entry.status==='success');// Count errors by typeconsterrorCounts=logs.filter((entry)=>entry.status==='error').reduce((acc,entry)=>{acc[entry.error]=(acc[entry.error]||0)+1;returnacc;},{});
# Load in Pythonimportjsonwithopen('logs/migration-2026-01-27T12:00:00.json') asf:
logs=json.load(f)
# Count by statusfromcollectionsimportCounterstatus_counts=Counter(entry['status'] forentryinlogs)

About

No description, website, or topics provided.

Resources

Stars

61 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all \u003cpre\u003e\u003ccode\u003e blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks"); } } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); } })(); (function(){ try { var __m = "github.com"; var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Clerk User Migration Tool

Description

This repository contains a tool that takes a JSON file as input, containing a list of users, and creates a user in Clerk using Clerk's backend API. The tool respects rate limits and handles errors.

Table of Contents

Documentation

Getting Started

Clone the repository and install the dependencies.

git clone git@github.com:clerk/migration-tool
cd migration-tool
bun install

Users file

The tool is designed to import from multiple sources, including moving users from one Clerk instance to another. You may need to edit the transformer for your source. Please see below for more information on that.

The tool will import from a CSV or JSON. It accounts for empty fields in a CSV and will remove them when converting from CSV to a javascript object.

The only required fields are userId and an identifier (one of email, phone or username).

Samples

The samples/ folder contains some samples you can test with. The samples include issues that will produce errors when running the import.

Some sample users have passwords. The password is Kk4aPMeiaRpAs2OeX1NE.

Secret Key

You have several options for providing your Clerk secret key:

Option 1: Create a .env file (recommended for repeated use)

CLERK_SECRET_KEY=your-secret-key

Option 2: Pass via command line (useful for automation/AI agents)

bun migrate --clerk-secret-key sk_test_xxx

Option 3: Set environment variable

export CLERK_SECRET_KEY=sk_test_xxx
bun migrate

Option 4: Enter interactively

If no key is found, the interactive CLI will prompt you to enter one and optionally save it to a .env file.

You can find your secret key in the Clerk Dashboard under API Keys.

Run the tool

bun migrate

The tool will begin processing users and attempting to import them into Clerk. The tool respects rate limits for the Clerk Backend API. If the tool hits a rate limit, it will wait 10 seconds and retry (up to 5 times). Any errors will be logged to timestamped log files in the ./logs folder.

The tool can be run on the same data multiple times. Clerk automatically uses the email as a unique key so users won't be created again.

Error Handling & Resuming: If the migration stops for any reason (error, interruption, etc.), the tool will display the last processed user ID. You can resume the migration from that point by providing the user ID when prompted, or by using:

bun migrate --resume-after="user_xxx"

CLI Reference

The migration tool supports both interactive and non-interactive modes.

Usage

bun migrate [OPTIONS]

Options

OptionDescription
-t, --transformer <transformer>Source transformer (clerk, auth0, authjs, firebase, supabase)
-f, --file <path>Path to the user data file (JSON or CSV)
-r, --resume-after <userId>Resume migration after this user ID
--require-passwordOnly migrate users who have passwords (by default, users without passwords are migrated)
-y, --yesNon-interactive mode (skip all confirmations)
-h, --helpShow help message

Authentication Options

OptionDescription
--clerk-secret-key <key>Clerk secret key (alternative to .env file)

Firebase Options

Required when --transformer is firebase:

OptionDescription
--firebase-signer-key <key>Firebase hash signer key (base64)
--firebase-salt-separator <sep>Firebase salt separator (base64)
--firebase-rounds <num>Firebase hash rounds
--firebase-mem-cost <num>Firebase memory cost

Examples

# Interactive mode (default)
bun migrate
# Non-interactive mode with required options
bun migrate -y -t auth0 -f users.json
# Non-interactive with secret key (no .env needed)
bun migrate -y -t clerk -f users.json --clerk-secret-key sk_test_xxx
# Resume a failed migration
bun migrate -y -t clerk -f users.json -r user_abc123
# Firebase migration with hash config
bun migrate -y -t firebase -f users.csv \
--firebase-signer-key "abc123..." \
--firebase-salt-separator "Bw==" \
--firebase-rounds 8 \
--firebase-mem-cost 14

Non-Interactive Mode

For automation and AI agent usage, use the -y flag with required options:

bun migrate -y \
--transformer clerk \
--file users.json \
--clerk-secret-key sk_test_xxx

Required in non-interactive mode:

  • --transformer (or -t)
  • --file (or -f)
  • CLERK_SECRET_KEY (via --clerk-secret-key, environment variable, or .env file)

Exporting Users

Some platforms require exporting users directly from their database before migrating. See the Exporting Users guide for setup, CLI options, and troubleshooting.

bun export:supabase

Migrating OAuth Connections

OAuth connections can not be directly migrated. The creation of the connection requires the user to consent, which can't happen on a migration like this. Instead you can rely on Clerk's Account Linking to handle this.

Handle Existing User IDs and Foreign Key Constraints

When migrating from another authentication system, you likely have data in your database tied to your previous system's user IDs. To maintain data consistency as you move to Clerk, you'll need a strategy to handle these foreign key relationships. Below are several approaches.

Custom session claims

Our sessions allow for conditional expressions. This would allow you add a session claim that will return either the externalId (the previous id for your user) when it exists, or the userId from Clerk. This will result in your imported users returning their externalId while newer users will return the Clerk userId.

In your Dashboard, go to Sessions -> Edit. Add the following:

{
"userId": "{{user.externalId || user.id}}"
}

You can now access this value using the following:

const{ sessionClaims }=auth();console.log(sessionClaims.userId);

You can add the following for typescript:

// types/global.d.tsexport{};declareglobal{interfaceCustomJwtSessionClaims{userId?: string;}}

Other options

You could continue to generate unique ids for the database as done previously, and then store those in externalId. This way all users would have an externalId that would be used for DB interactions.

You could add a column in your user table inside of your database called ClerkId. Use that column to store the userId from Clerk directly into your database.

Configuration

The tool can be configured through the following environment variables:

VariableDescription
CLERK_SECRET_KEYYour Clerk secret key
RATE_LIMITRate limit in requests/second (auto-configured: 100 for prod, 10 for dev)
CONCURRENCY_LIMITNumber of concurrent requests (auto-configured: ~9 for prod, ~1 for dev)

The tool automatically detects production vs development instances from your CLERK_SECRET_KEY and sets appropriate rate limits and concurrency:

  • Production (sk_live_*):
    • Rate limit: 100 requests/second (Clerk's limit: 1000 requests per 10 seconds)
    • Concurrency: 9 concurrent requests (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~35 seconds
  • Development (sk_test_*):
    • Rate limit: 10 requests/second (Clerk's limit: 100 requests per 10 seconds)
    • Concurrency: 1 concurrent request (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~350 seconds

You can override these values by setting RATE_LIMIT or CONCURRENCY_LIMIT in your .env file.

Tuning Concurrency: If you want faster migrations, you can increase CONCURRENCY_LIMIT (e.g., CONCURRENCY_LIMIT=15 for ~150 req/s). Note that higher concurrency may trigger rate limit errors (429), which are automatically retried.

Commands

Run migration

bun migrate

Delete users

bun delete

This will delete all migrated users from the instance. It should not delete pre-existing users, but it is not recommended to use this with a production instance that has pre-existing users. Please use caution with this command.

Clean logs

bun clean-logs

All migrations and deletions will create logs in the ./logs folder. This command will delete those logs.

Convert logs from NDJSON to JSON

bun convert-logs

Convert Logs Utility

Converts NDJSON (Newline-Delimited JSON) log files to standard JSON array format for easier analysis in spreadsheets, databases, or other tools.

Usage

bun convert-logs

The utility will:

  1. List all .log files in the ./logs directory
  2. Let you select which files to convert
  3. Create corresponding .json files with the converted data

Example

Input (migration-2026-01-27T12:00:00.log):

{"userId":"user_1","status":"success","clerkUserId":"clerk_abc123"}
{"userId":"user_2","status":"error","error":"Email already exists"}
{"userId":"user_3","status":"fail","error":"invalid_type for required field.","path":["email"],"row":5}

Output (migration-2026-01-27T12:00:00.json):

[
{
"userId": "user_1",
"status": "success",
"clerkUserId": "clerk_abc123"
},
{
"userId": "user_2",
"status": "error",
"error": "Email already exists"
},
{
"userId": "user_3",
"status": "fail",
"error": "invalid_type for required field.",
"path": ["email"],
"row": 5
}
]

Why NDJSON for Logs?

The tool uses NDJSON for log files because:

  • Streaming: Can append entries as they happen without rewriting the file
  • Crash-safe: If the process crashes, all entries written so far are valid
  • Memory efficient: Can process line-by-line without loading entire log
  • Scalable: Works efficiently with thousands or millions of entries
  • Real-time: Can monitor with tail -f and see entries as they're written

When to Convert

Convert logs to JSON arrays when you need to:

  • Import into Excel, Google Sheets, or other spreadsheet tools
  • Load into a database for analysis
  • Process with tools that expect JSON arrays
  • Share logs with team members less familiar with NDJSON

Analyzing Logs

With NDJSON (original format)

# Count successful imports
grep '"status":"success"' logs/migration-2026-01-27T12:00:00.log | wc -l
# Find all errors
grep '"status":"error"' logs/migration-2026-01-27T12:00:00.log
# Get specific user
grep '"userId":"user_123"' logs/migration-2026-01-27T12:00:00.log

With JSON Arrays (converted format)

// Load in Node.js/JavaScriptconstlogs=require('./logs/migration-2026-01-27T12:00:00.json');// Filter successful importsconstsuccessful=logs.filter((entry)=>entry.status==='success');// Count errors by typeconsterrorCounts=logs.filter((entry)=>entry.status==='error').reduce((acc,entry)=>{acc[entry.error]=(acc[entry.error]||0)+1;returnacc;},{});
# Load in Pythonimportjsonwithopen('logs/migration-2026-01-27T12:00:00.json') asf:
logs=json.load(f)
# Count by statusfromcollectionsimportCounterstatus_counts=Counter(entry['status'] forentryinlogs)

About

No description, website, or topics provided.

Resources

Stars

61 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Clerk User Migration Tool

Description

This repository contains a tool that takes a JSON file as input, containing a list of users, and creates a user in Clerk using Clerk's backend API. The tool respects rate limits and handles errors.

Table of Contents

Documentation

Getting Started

Clone the repository and install the dependencies.

git clone git@github.com:clerk/migration-tool
cd migration-tool
bun install

Users file

The tool is designed to import from multiple sources, including moving users from one Clerk instance to another. You may need to edit the transformer for your source. Please see below for more information on that.

The tool will import from a CSV or JSON. It accounts for empty fields in a CSV and will remove them when converting from CSV to a javascript object.

The only required fields are userId and an identifier (one of email, phone or username).

Samples

The samples/ folder contains some samples you can test with. The samples include issues that will produce errors when running the import.

Some sample users have passwords. The password is Kk4aPMeiaRpAs2OeX1NE.

Secret Key

You have several options for providing your Clerk secret key:

Option 1: Create a .env file (recommended for repeated use)

CLERK_SECRET_KEY=your-secret-key

Option 2: Pass via command line (useful for automation/AI agents)

bun migrate --clerk-secret-key sk_test_xxx

Option 3: Set environment variable

export CLERK_SECRET_KEY=sk_test_xxx
bun migrate

Option 4: Enter interactively

If no key is found, the interactive CLI will prompt you to enter one and optionally save it to a .env file.

You can find your secret key in the Clerk Dashboard under API Keys.

Run the tool

bun migrate

The tool will begin processing users and attempting to import them into Clerk. The tool respects rate limits for the Clerk Backend API. If the tool hits a rate limit, it will wait 10 seconds and retry (up to 5 times). Any errors will be logged to timestamped log files in the ./logs folder.

The tool can be run on the same data multiple times. Clerk automatically uses the email as a unique key so users won't be created again.

Error Handling & Resuming: If the migration stops for any reason (error, interruption, etc.), the tool will display the last processed user ID. You can resume the migration from that point by providing the user ID when prompted, or by using:

bun migrate --resume-after="user_xxx"

CLI Reference

The migration tool supports both interactive and non-interactive modes.

Usage

bun migrate [OPTIONS]

Options

OptionDescription
-t, --transformer <transformer>Source transformer (clerk, auth0, authjs, firebase, supabase)
-f, --file <path>Path to the user data file (JSON or CSV)
-r, --resume-after <userId>Resume migration after this user ID
--require-passwordOnly migrate users who have passwords (by default, users without passwords are migrated)
-y, --yesNon-interactive mode (skip all confirmations)
-h, --helpShow help message

Authentication Options

OptionDescription
--clerk-secret-key <key>Clerk secret key (alternative to .env file)

Firebase Options

Required when --transformer is firebase:

OptionDescription
--firebase-signer-key <key>Firebase hash signer key (base64)
--firebase-salt-separator <sep>Firebase salt separator (base64)
--firebase-rounds <num>Firebase hash rounds
--firebase-mem-cost <num>Firebase memory cost

Examples

# Interactive mode (default)
bun migrate
# Non-interactive mode with required options
bun migrate -y -t auth0 -f users.json
# Non-interactive with secret key (no .env needed)
bun migrate -y -t clerk -f users.json --clerk-secret-key sk_test_xxx
# Resume a failed migration
bun migrate -y -t clerk -f users.json -r user_abc123
# Firebase migration with hash config
bun migrate -y -t firebase -f users.csv \
--firebase-signer-key "abc123..." \
--firebase-salt-separator "Bw==" \
--firebase-rounds 8 \
--firebase-mem-cost 14

Non-Interactive Mode

For automation and AI agent usage, use the -y flag with required options:

bun migrate -y \
--transformer clerk \
--file users.json \
--clerk-secret-key sk_test_xxx

Required in non-interactive mode:

  • --transformer (or -t)
  • --file (or -f)
  • CLERK_SECRET_KEY (via --clerk-secret-key, environment variable, or .env file)

Exporting Users

Some platforms require exporting users directly from their database before migrating. See the Exporting Users guide for setup, CLI options, and troubleshooting.

bun export:supabase

Migrating OAuth Connections

OAuth connections can not be directly migrated. The creation of the connection requires the user to consent, which can't happen on a migration like this. Instead you can rely on Clerk's Account Linking to handle this.

Handle Existing User IDs and Foreign Key Constraints

When migrating from another authentication system, you likely have data in your database tied to your previous system's user IDs. To maintain data consistency as you move to Clerk, you'll need a strategy to handle these foreign key relationships. Below are several approaches.

Custom session claims

Our sessions allow for conditional expressions. This would allow you add a session claim that will return either the externalId (the previous id for your user) when it exists, or the userId from Clerk. This will result in your imported users returning their externalId while newer users will return the Clerk userId.

In your Dashboard, go to Sessions -> Edit. Add the following:

{
"userId": "{{user.externalId || user.id}}"
}

You can now access this value using the following:

const{ sessionClaims }=auth();console.log(sessionClaims.userId);

You can add the following for typescript:

// types/global.d.tsexport{};declareglobal{interfaceCustomJwtSessionClaims{userId?: string;}}

Other options

You could continue to generate unique ids for the database as done previously, and then store those in externalId. This way all users would have an externalId that would be used for DB interactions.

You could add a column in your user table inside of your database called ClerkId. Use that column to store the userId from Clerk directly into your database.

Configuration

The tool can be configured through the following environment variables:

VariableDescription
CLERK_SECRET_KEYYour Clerk secret key
RATE_LIMITRate limit in requests/second (auto-configured: 100 for prod, 10 for dev)
CONCURRENCY_LIMITNumber of concurrent requests (auto-configured: ~9 for prod, ~1 for dev)

The tool automatically detects production vs development instances from your CLERK_SECRET_KEY and sets appropriate rate limits and concurrency:

  • Production (sk_live_*):
    • Rate limit: 100 requests/second (Clerk's limit: 1000 requests per 10 seconds)
    • Concurrency: 9 concurrent requests (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~35 seconds
  • Development (sk_test_*):
    • Rate limit: 10 requests/second (Clerk's limit: 100 requests per 10 seconds)
    • Concurrency: 1 concurrent request (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~350 seconds

You can override these values by setting RATE_LIMIT or CONCURRENCY_LIMIT in your .env file.

Tuning Concurrency: If you want faster migrations, you can increase CONCURRENCY_LIMIT (e.g., CONCURRENCY_LIMIT=15 for ~150 req/s). Note that higher concurrency may trigger rate limit errors (429), which are automatically retried.

Commands

Run migration

bun migrate

Delete users

bun delete

This will delete all migrated users from the instance. It should not delete pre-existing users, but it is not recommended to use this with a production instance that has pre-existing users. Please use caution with this command.

Clean logs

bun clean-logs

All migrations and deletions will create logs in the ./logs folder. This command will delete those logs.

Convert logs from NDJSON to JSON

bun convert-logs

Convert Logs Utility

Converts NDJSON (Newline-Delimited JSON) log files to standard JSON array format for easier analysis in spreadsheets, databases, or other tools.

Usage

bun convert-logs

The utility will:

  1. List all .log files in the ./logs directory
  2. Let you select which files to convert
  3. Create corresponding .json files with the converted data

Example

Input (migration-2026-01-27T12:00:00.log):

{"userId":"user_1","status":"success","clerkUserId":"clerk_abc123"}
{"userId":"user_2","status":"error","error":"Email already exists"}
{"userId":"user_3","status":"fail","error":"invalid_type for required field.","path":["email"],"row":5}

Output (migration-2026-01-27T12:00:00.json):

[
{
"userId": "user_1",
"status": "success",
"clerkUserId": "clerk_abc123"
},
{
"userId": "user_2",
"status": "error",
"error": "Email already exists"
},
{
"userId": "user_3",
"status": "fail",
"error": "invalid_type for required field.",
"path": ["email"],
"row": 5
}
]

Why NDJSON for Logs?

The tool uses NDJSON for log files because:

  • Streaming: Can append entries as they happen without rewriting the file
  • Crash-safe: If the process crashes, all entries written so far are valid
  • Memory efficient: Can process line-by-line without loading entire log
  • Scalable: Works efficiently with thousands or millions of entries
  • Real-time: Can monitor with tail -f and see entries as they're written

When to Convert

Convert logs to JSON arrays when you need to:

  • Import into Excel, Google Sheets, or other spreadsheet tools
  • Load into a database for analysis
  • Process with tools that expect JSON arrays
  • Share logs with team members less familiar with NDJSON

Analyzing Logs

With NDJSON (original format)

# Count successful imports
grep '"status":"success"' logs/migration-2026-01-27T12:00:00.log | wc -l
# Find all errors
grep '"status":"error"' logs/migration-2026-01-27T12:00:00.log
# Get specific user
grep '"userId":"user_123"' logs/migration-2026-01-27T12:00:00.log

With JSON Arrays (converted format)

// Load in Node.js/JavaScriptconstlogs=require('./logs/migration-2026-01-27T12:00:00.json');// Filter successful importsconstsuccessful=logs.filter((entry)=>entry.status==='success');// Count errors by typeconsterrorCounts=logs.filter((entry)=>entry.status==='error').reduce((acc,entry)=>{acc[entry.error]=(acc[entry.error]||0)+1;returnacc;},{});
# Load in Pythonimportjsonwithopen('logs/migration-2026-01-27T12:00:00.json') asf:
logs=json.load(f)
# Count by statusfromcollectionsimportCounterstatus_counts=Counter(entry['status'] forentryinlogs)

About

No description, website, or topics provided.

Resources

Stars

61 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length \u003e 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Clerk User Migration Tool

Description

This repository contains a tool that takes a JSON file as input, containing a list of users, and creates a user in Clerk using Clerk's backend API. The tool respects rate limits and handles errors.

Table of Contents

Documentation

Getting Started

Clone the repository and install the dependencies.

git clone git@github.com:clerk/migration-tool
cd migration-tool
bun install

Users file

The tool is designed to import from multiple sources, including moving users from one Clerk instance to another. You may need to edit the transformer for your source. Please see below for more information on that.

The tool will import from a CSV or JSON. It accounts for empty fields in a CSV and will remove them when converting from CSV to a javascript object.

The only required fields are userId and an identifier (one of email, phone or username).

Samples

The samples/ folder contains some samples you can test with. The samples include issues that will produce errors when running the import.

Some sample users have passwords. The password is Kk4aPMeiaRpAs2OeX1NE.

Secret Key

You have several options for providing your Clerk secret key:

Option 1: Create a .env file (recommended for repeated use)

CLERK_SECRET_KEY=your-secret-key

Option 2: Pass via command line (useful for automation/AI agents)

bun migrate --clerk-secret-key sk_test_xxx

Option 3: Set environment variable

export CLERK_SECRET_KEY=sk_test_xxx
bun migrate

Option 4: Enter interactively

If no key is found, the interactive CLI will prompt you to enter one and optionally save it to a .env file.

You can find your secret key in the Clerk Dashboard under API Keys.

Run the tool

bun migrate

The tool will begin processing users and attempting to import them into Clerk. The tool respects rate limits for the Clerk Backend API. If the tool hits a rate limit, it will wait 10 seconds and retry (up to 5 times). Any errors will be logged to timestamped log files in the ./logs folder.

The tool can be run on the same data multiple times. Clerk automatically uses the email as a unique key so users won't be created again.

Error Handling & Resuming: If the migration stops for any reason (error, interruption, etc.), the tool will display the last processed user ID. You can resume the migration from that point by providing the user ID when prompted, or by using:

bun migrate --resume-after="user_xxx"

CLI Reference

The migration tool supports both interactive and non-interactive modes.

Usage

bun migrate [OPTIONS]

Options

OptionDescription
-t, --transformer <transformer>Source transformer (clerk, auth0, authjs, firebase, supabase)
-f, --file <path>Path to the user data file (JSON or CSV)
-r, --resume-after <userId>Resume migration after this user ID
--require-passwordOnly migrate users who have passwords (by default, users without passwords are migrated)
-y, --yesNon-interactive mode (skip all confirmations)
-h, --helpShow help message

Authentication Options

OptionDescription
--clerk-secret-key <key>Clerk secret key (alternative to .env file)

Firebase Options

Required when --transformer is firebase:

OptionDescription
--firebase-signer-key <key>Firebase hash signer key (base64)
--firebase-salt-separator <sep>Firebase salt separator (base64)
--firebase-rounds <num>Firebase hash rounds
--firebase-mem-cost <num>Firebase memory cost

Examples

# Interactive mode (default)
bun migrate
# Non-interactive mode with required options
bun migrate -y -t auth0 -f users.json
# Non-interactive with secret key (no .env needed)
bun migrate -y -t clerk -f users.json --clerk-secret-key sk_test_xxx
# Resume a failed migration
bun migrate -y -t clerk -f users.json -r user_abc123
# Firebase migration with hash config
bun migrate -y -t firebase -f users.csv \
--firebase-signer-key "abc123..." \
--firebase-salt-separator "Bw==" \
--firebase-rounds 8 \
--firebase-mem-cost 14

Non-Interactive Mode

For automation and AI agent usage, use the -y flag with required options:

bun migrate -y \
--transformer clerk \
--file users.json \
--clerk-secret-key sk_test_xxx

Required in non-interactive mode:

  • --transformer (or -t)
  • --file (or -f)
  • CLERK_SECRET_KEY (via --clerk-secret-key, environment variable, or .env file)

Exporting Users

Some platforms require exporting users directly from their database before migrating. See the Exporting Users guide for setup, CLI options, and troubleshooting.

bun export:supabase

Migrating OAuth Connections

OAuth connections can not be directly migrated. The creation of the connection requires the user to consent, which can't happen on a migration like this. Instead you can rely on Clerk's Account Linking to handle this.

Handle Existing User IDs and Foreign Key Constraints

When migrating from another authentication system, you likely have data in your database tied to your previous system's user IDs. To maintain data consistency as you move to Clerk, you'll need a strategy to handle these foreign key relationships. Below are several approaches.

Custom session claims

Our sessions allow for conditional expressions. This would allow you add a session claim that will return either the externalId (the previous id for your user) when it exists, or the userId from Clerk. This will result in your imported users returning their externalId while newer users will return the Clerk userId.

In your Dashboard, go to Sessions -> Edit. Add the following:

{
"userId": "{{user.externalId || user.id}}"
}

You can now access this value using the following:

const{ sessionClaims }=auth();console.log(sessionClaims.userId);

You can add the following for typescript:

// types/global.d.tsexport{};declareglobal{interfaceCustomJwtSessionClaims{userId?: string;}}

Other options

You could continue to generate unique ids for the database as done previously, and then store those in externalId. This way all users would have an externalId that would be used for DB interactions.

You could add a column in your user table inside of your database called ClerkId. Use that column to store the userId from Clerk directly into your database.

Configuration

The tool can be configured through the following environment variables:

VariableDescription
CLERK_SECRET_KEYYour Clerk secret key
RATE_LIMITRate limit in requests/second (auto-configured: 100 for prod, 10 for dev)
CONCURRENCY_LIMITNumber of concurrent requests (auto-configured: ~9 for prod, ~1 for dev)

The tool automatically detects production vs development instances from your CLERK_SECRET_KEY and sets appropriate rate limits and concurrency:

  • Production (sk_live_*):
    • Rate limit: 100 requests/second (Clerk's limit: 1000 requests per 10 seconds)
    • Concurrency: 9 concurrent requests (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~35 seconds
  • Development (sk_test_*):
    • Rate limit: 10 requests/second (Clerk's limit: 100 requests per 10 seconds)
    • Concurrency: 1 concurrent request (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~350 seconds

You can override these values by setting RATE_LIMIT or CONCURRENCY_LIMIT in your .env file.

Tuning Concurrency: If you want faster migrations, you can increase CONCURRENCY_LIMIT (e.g., CONCURRENCY_LIMIT=15 for ~150 req/s). Note that higher concurrency may trigger rate limit errors (429), which are automatically retried.

Commands

Run migration

bun migrate

Delete users

bun delete

This will delete all migrated users from the instance. It should not delete pre-existing users, but it is not recommended to use this with a production instance that has pre-existing users. Please use caution with this command.

Clean logs

bun clean-logs

All migrations and deletions will create logs in the ./logs folder. This command will delete those logs.

Convert logs from NDJSON to JSON

bun convert-logs

Convert Logs Utility

Converts NDJSON (Newline-Delimited JSON) log files to standard JSON array format for easier analysis in spreadsheets, databases, or other tools.

Usage

bun convert-logs

The utility will:

  1. List all .log files in the ./logs directory
  2. Let you select which files to convert
  3. Create corresponding .json files with the converted data

Example

Input (migration-2026-01-27T12:00:00.log):

{"userId":"user_1","status":"success","clerkUserId":"clerk_abc123"}
{"userId":"user_2","status":"error","error":"Email already exists"}
{"userId":"user_3","status":"fail","error":"invalid_type for required field.","path":["email"],"row":5}

Output (migration-2026-01-27T12:00:00.json):

[
{
"userId": "user_1",
"status": "success",
"clerkUserId": "clerk_abc123"
},
{
"userId": "user_2",
"status": "error",
"error": "Email already exists"
},
{
"userId": "user_3",
"status": "fail",
"error": "invalid_type for required field.",
"path": ["email"],
"row": 5
}
]

Why NDJSON for Logs?

The tool uses NDJSON for log files because:

  • Streaming: Can append entries as they happen without rewriting the file
  • Crash-safe: If the process crashes, all entries written so far are valid
  • Memory efficient: Can process line-by-line without loading entire log
  • Scalable: Works efficiently with thousands or millions of entries
  • Real-time: Can monitor with tail -f and see entries as they're written

When to Convert

Convert logs to JSON arrays when you need to:

  • Import into Excel, Google Sheets, or other spreadsheet tools
  • Load into a database for analysis
  • Process with tools that expect JSON arrays
  • Share logs with team members less familiar with NDJSON

Analyzing Logs

With NDJSON (original format)

# Count successful imports
grep '"status":"success"' logs/migration-2026-01-27T12:00:00.log | wc -l
# Find all errors
grep '"status":"error"' logs/migration-2026-01-27T12:00:00.log
# Get specific user
grep '"userId":"user_123"' logs/migration-2026-01-27T12:00:00.log

With JSON Arrays (converted format)

// Load in Node.js/JavaScriptconstlogs=require('./logs/migration-2026-01-27T12:00:00.json');// Filter successful importsconstsuccessful=logs.filter((entry)=>entry.status==='success');// Count errors by typeconsterrorCounts=logs.filter((entry)=>entry.status==='error').reduce((acc,entry)=>{acc[entry.error]=(acc[entry.error]||0)+1;returnacc;},{});
# Load in Pythonimportjsonwithopen('logs/migration-2026-01-27T12:00:00.json') asf:
logs=json.load(f)
# Count by statusfromcollectionsimportCounterstatus_counts=Counter(entry['status'] forentryinlogs)

About

No description, website, or topics provided.

Resources

Stars

61 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Clerk User Migration Tool

Description

This repository contains a tool that takes a JSON file as input, containing a list of users, and creates a user in Clerk using Clerk's backend API. The tool respects rate limits and handles errors.

Table of Contents

Documentation

Getting Started

Clone the repository and install the dependencies.

git clone git@github.com:clerk/migration-tool
cd migration-tool
bun install

Users file

The tool is designed to import from multiple sources, including moving users from one Clerk instance to another. You may need to edit the transformer for your source. Please see below for more information on that.

The tool will import from a CSV or JSON. It accounts for empty fields in a CSV and will remove them when converting from CSV to a javascript object.

The only required fields are userId and an identifier (one of email, phone or username).

Samples

The samples/ folder contains some samples you can test with. The samples include issues that will produce errors when running the import.

Some sample users have passwords. The password is Kk4aPMeiaRpAs2OeX1NE.

Secret Key

You have several options for providing your Clerk secret key:

Option 1: Create a .env file (recommended for repeated use)

CLERK_SECRET_KEY=your-secret-key

Option 2: Pass via command line (useful for automation/AI agents)

bun migrate --clerk-secret-key sk_test_xxx

Option 3: Set environment variable

export CLERK_SECRET_KEY=sk_test_xxx
bun migrate

Option 4: Enter interactively

If no key is found, the interactive CLI will prompt you to enter one and optionally save it to a .env file.

You can find your secret key in the Clerk Dashboard under API Keys.

Run the tool

bun migrate

The tool will begin processing users and attempting to import them into Clerk. The tool respects rate limits for the Clerk Backend API. If the tool hits a rate limit, it will wait 10 seconds and retry (up to 5 times). Any errors will be logged to timestamped log files in the ./logs folder.

The tool can be run on the same data multiple times. Clerk automatically uses the email as a unique key so users won't be created again.

Error Handling & Resuming: If the migration stops for any reason (error, interruption, etc.), the tool will display the last processed user ID. You can resume the migration from that point by providing the user ID when prompted, or by using:

bun migrate --resume-after="user_xxx"

CLI Reference

The migration tool supports both interactive and non-interactive modes.

Usage

bun migrate [OPTIONS]

Options

OptionDescription
-t, --transformer <transformer>Source transformer (clerk, auth0, authjs, firebase, supabase)
-f, --file <path>Path to the user data file (JSON or CSV)
-r, --resume-after <userId>Resume migration after this user ID
--require-passwordOnly migrate users who have passwords (by default, users without passwords are migrated)
-y, --yesNon-interactive mode (skip all confirmations)
-h, --helpShow help message

Authentication Options

OptionDescription
--clerk-secret-key <key>Clerk secret key (alternative to .env file)

Firebase Options

Required when --transformer is firebase:

OptionDescription
--firebase-signer-key <key>Firebase hash signer key (base64)
--firebase-salt-separator <sep>Firebase salt separator (base64)
--firebase-rounds <num>Firebase hash rounds
--firebase-mem-cost <num>Firebase memory cost

Examples

# Interactive mode (default)
bun migrate
# Non-interactive mode with required options
bun migrate -y -t auth0 -f users.json
# Non-interactive with secret key (no .env needed)
bun migrate -y -t clerk -f users.json --clerk-secret-key sk_test_xxx
# Resume a failed migration
bun migrate -y -t clerk -f users.json -r user_abc123
# Firebase migration with hash config
bun migrate -y -t firebase -f users.csv \
--firebase-signer-key "abc123..." \
--firebase-salt-separator "Bw==" \
--firebase-rounds 8 \
--firebase-mem-cost 14

Non-Interactive Mode

For automation and AI agent usage, use the -y flag with required options:

bun migrate -y \
--transformer clerk \
--file users.json \
--clerk-secret-key sk_test_xxx

Required in non-interactive mode:

  • --transformer (or -t)
  • --file (or -f)
  • CLERK_SECRET_KEY (via --clerk-secret-key, environment variable, or .env file)

Exporting Users

Some platforms require exporting users directly from their database before migrating. See the Exporting Users guide for setup, CLI options, and troubleshooting.

bun export:supabase

Migrating OAuth Connections

OAuth connections can not be directly migrated. The creation of the connection requires the user to consent, which can't happen on a migration like this. Instead you can rely on Clerk's Account Linking to handle this.

Handle Existing User IDs and Foreign Key Constraints

When migrating from another authentication system, you likely have data in your database tied to your previous system's user IDs. To maintain data consistency as you move to Clerk, you'll need a strategy to handle these foreign key relationships. Below are several approaches.

Custom session claims

Our sessions allow for conditional expressions. This would allow you add a session claim that will return either the externalId (the previous id for your user) when it exists, or the userId from Clerk. This will result in your imported users returning their externalId while newer users will return the Clerk userId.

In your Dashboard, go to Sessions -> Edit. Add the following:

{
"userId": "{{user.externalId || user.id}}"
}

You can now access this value using the following:

const{ sessionClaims }=auth();console.log(sessionClaims.userId);

You can add the following for typescript:

// types/global.d.tsexport{};declareglobal{interfaceCustomJwtSessionClaims{userId?: string;}}

Other options

You could continue to generate unique ids for the database as done previously, and then store those in externalId. This way all users would have an externalId that would be used for DB interactions.

You could add a column in your user table inside of your database called ClerkId. Use that column to store the userId from Clerk directly into your database.

Configuration

The tool can be configured through the following environment variables:

VariableDescription
CLERK_SECRET_KEYYour Clerk secret key
RATE_LIMITRate limit in requests/second (auto-configured: 100 for prod, 10 for dev)
CONCURRENCY_LIMITNumber of concurrent requests (auto-configured: ~9 for prod, ~1 for dev)

The tool automatically detects production vs development instances from your CLERK_SECRET_KEY and sets appropriate rate limits and concurrency:

  • Production (sk_live_*):
    • Rate limit: 100 requests/second (Clerk's limit: 1000 requests per 10 seconds)
    • Concurrency: 9 concurrent requests (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~35 seconds
  • Development (sk_test_*):
    • Rate limit: 10 requests/second (Clerk's limit: 100 requests per 10 seconds)
    • Concurrency: 1 concurrent request (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~350 seconds

You can override these values by setting RATE_LIMIT or CONCURRENCY_LIMIT in your .env file.

Tuning Concurrency: If you want faster migrations, you can increase CONCURRENCY_LIMIT (e.g., CONCURRENCY_LIMIT=15 for ~150 req/s). Note that higher concurrency may trigger rate limit errors (429), which are automatically retried.

Commands

Run migration

bun migrate

Delete users

bun delete

This will delete all migrated users from the instance. It should not delete pre-existing users, but it is not recommended to use this with a production instance that has pre-existing users. Please use caution with this command.

Clean logs

bun clean-logs

All migrations and deletions will create logs in the ./logs folder. This command will delete those logs.

Convert logs from NDJSON to JSON

bun convert-logs

Convert Logs Utility

Converts NDJSON (Newline-Delimited JSON) log files to standard JSON array format for easier analysis in spreadsheets, databases, or other tools.

Usage

bun convert-logs

The utility will:

  1. List all .log files in the ./logs directory
  2. Let you select which files to convert
  3. Create corresponding .json files with the converted data

Example

Input (migration-2026-01-27T12:00:00.log):

{"userId":"user_1","status":"success","clerkUserId":"clerk_abc123"}
{"userId":"user_2","status":"error","error":"Email already exists"}
{"userId":"user_3","status":"fail","error":"invalid_type for required field.","path":["email"],"row":5}

Output (migration-2026-01-27T12:00:00.json):

[
{
"userId": "user_1",
"status": "success",
"clerkUserId": "clerk_abc123"
},
{
"userId": "user_2",
"status": "error",
"error": "Email already exists"
},
{
"userId": "user_3",
"status": "fail",
"error": "invalid_type for required field.",
"path": ["email"],
"row": 5
}
]

Why NDJSON for Logs?

The tool uses NDJSON for log files because:

  • Streaming: Can append entries as they happen without rewriting the file
  • Crash-safe: If the process crashes, all entries written so far are valid
  • Memory efficient: Can process line-by-line without loading entire log
  • Scalable: Works efficiently with thousands or millions of entries
  • Real-time: Can monitor with tail -f and see entries as they're written

When to Convert

Convert logs to JSON arrays when you need to:

  • Import into Excel, Google Sheets, or other spreadsheet tools
  • Load into a database for analysis
  • Process with tools that expect JSON arrays
  • Share logs with team members less familiar with NDJSON

Analyzing Logs

With NDJSON (original format)

# Count successful imports
grep '"status":"success"' logs/migration-2026-01-27T12:00:00.log | wc -l
# Find all errors
grep '"status":"error"' logs/migration-2026-01-27T12:00:00.log
# Get specific user
grep '"userId":"user_123"' logs/migration-2026-01-27T12:00:00.log

With JSON Arrays (converted format)

// Load in Node.js/JavaScriptconstlogs=require('./logs/migration-2026-01-27T12:00:00.json');// Filter successful importsconstsuccessful=logs.filter((entry)=>entry.status==='success');// Count errors by typeconsterrorCounts=logs.filter((entry)=>entry.status==='error').reduce((acc,entry)=>{acc[entry.error]=(acc[entry.error]||0)+1;returnacc;},{});
# Load in Pythonimportjsonwithopen('logs/migration-2026-01-27T12:00:00.json') asf:
logs=json.load(f)
# Count by statusfromcollectionsimportCounterstatus_counts=Counter(entry['status'] forentryinlogs)

About

No description, website, or topics provided.

Resources

Stars

61 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Clerk User Migration Tool

Description

This repository contains a tool that takes a JSON file as input, containing a list of users, and creates a user in Clerk using Clerk's backend API. The tool respects rate limits and handles errors.

Table of Contents

Documentation

Getting Started

Clone the repository and install the dependencies.

git clone git@github.com:clerk/migration-tool
cd migration-tool
bun install

Users file

The tool is designed to import from multiple sources, including moving users from one Clerk instance to another. You may need to edit the transformer for your source. Please see below for more information on that.

The tool will import from a CSV or JSON. It accounts for empty fields in a CSV and will remove them when converting from CSV to a javascript object.

The only required fields are userId and an identifier (one of email, phone or username).

Samples

The samples/ folder contains some samples you can test with. The samples include issues that will produce errors when running the import.

Some sample users have passwords. The password is Kk4aPMeiaRpAs2OeX1NE.

Secret Key

You have several options for providing your Clerk secret key:

Option 1: Create a .env file (recommended for repeated use)

CLERK_SECRET_KEY=your-secret-key

Option 2: Pass via command line (useful for automation/AI agents)

bun migrate --clerk-secret-key sk_test_xxx

Option 3: Set environment variable

export CLERK_SECRET_KEY=sk_test_xxx
bun migrate

Option 4: Enter interactively

If no key is found, the interactive CLI will prompt you to enter one and optionally save it to a .env file.

You can find your secret key in the Clerk Dashboard under API Keys.

Run the tool

bun migrate

The tool will begin processing users and attempting to import them into Clerk. The tool respects rate limits for the Clerk Backend API. If the tool hits a rate limit, it will wait 10 seconds and retry (up to 5 times). Any errors will be logged to timestamped log files in the ./logs folder.

The tool can be run on the same data multiple times. Clerk automatically uses the email as a unique key so users won't be created again.

Error Handling & Resuming: If the migration stops for any reason (error, interruption, etc.), the tool will display the last processed user ID. You can resume the migration from that point by providing the user ID when prompted, or by using:

bun migrate --resume-after="user_xxx"

CLI Reference

The migration tool supports both interactive and non-interactive modes.

Usage

bun migrate [OPTIONS]

Options

OptionDescription
-t, --transformer <transformer>Source transformer (clerk, auth0, authjs, firebase, supabase)
-f, --file <path>Path to the user data file (JSON or CSV)
-r, --resume-after <userId>Resume migration after this user ID
--require-passwordOnly migrate users who have passwords (by default, users without passwords are migrated)
-y, --yesNon-interactive mode (skip all confirmations)
-h, --helpShow help message

Authentication Options

OptionDescription
--clerk-secret-key <key>Clerk secret key (alternative to .env file)

Firebase Options

Required when --transformer is firebase:

OptionDescription
--firebase-signer-key <key>Firebase hash signer key (base64)
--firebase-salt-separator <sep>Firebase salt separator (base64)
--firebase-rounds <num>Firebase hash rounds
--firebase-mem-cost <num>Firebase memory cost

Examples

# Interactive mode (default)
bun migrate
# Non-interactive mode with required options
bun migrate -y -t auth0 -f users.json
# Non-interactive with secret key (no .env needed)
bun migrate -y -t clerk -f users.json --clerk-secret-key sk_test_xxx
# Resume a failed migration
bun migrate -y -t clerk -f users.json -r user_abc123
# Firebase migration with hash config
bun migrate -y -t firebase -f users.csv \
--firebase-signer-key "abc123..." \
--firebase-salt-separator "Bw==" \
--firebase-rounds 8 \
--firebase-mem-cost 14

Non-Interactive Mode

For automation and AI agent usage, use the -y flag with required options:

bun migrate -y \
--transformer clerk \
--file users.json \
--clerk-secret-key sk_test_xxx

Required in non-interactive mode:

  • --transformer (or -t)
  • --file (or -f)
  • CLERK_SECRET_KEY (via --clerk-secret-key, environment variable, or .env file)

Exporting Users

Some platforms require exporting users directly from their database before migrating. See the Exporting Users guide for setup, CLI options, and troubleshooting.

bun export:supabase

Migrating OAuth Connections

OAuth connections can not be directly migrated. The creation of the connection requires the user to consent, which can't happen on a migration like this. Instead you can rely on Clerk's Account Linking to handle this.

Handle Existing User IDs and Foreign Key Constraints

When migrating from another authentication system, you likely have data in your database tied to your previous system's user IDs. To maintain data consistency as you move to Clerk, you'll need a strategy to handle these foreign key relationships. Below are several approaches.

Custom session claims

Our sessions allow for conditional expressions. This would allow you add a session claim that will return either the externalId (the previous id for your user) when it exists, or the userId from Clerk. This will result in your imported users returning their externalId while newer users will return the Clerk userId.

In your Dashboard, go to Sessions -> Edit. Add the following:

{
"userId": "{{user.externalId || user.id}}"
}

You can now access this value using the following:

const{ sessionClaims }=auth();console.log(sessionClaims.userId);

You can add the following for typescript:

// types/global.d.tsexport{};declareglobal{interfaceCustomJwtSessionClaims{userId?: string;}}

Other options

You could continue to generate unique ids for the database as done previously, and then store those in externalId. This way all users would have an externalId that would be used for DB interactions.

You could add a column in your user table inside of your database called ClerkId. Use that column to store the userId from Clerk directly into your database.

Configuration

The tool can be configured through the following environment variables:

VariableDescription
CLERK_SECRET_KEYYour Clerk secret key
RATE_LIMITRate limit in requests/second (auto-configured: 100 for prod, 10 for dev)
CONCURRENCY_LIMITNumber of concurrent requests (auto-configured: ~9 for prod, ~1 for dev)

The tool automatically detects production vs development instances from your CLERK_SECRET_KEY and sets appropriate rate limits and concurrency:

  • Production (sk_live_*):
    • Rate limit: 100 requests/second (Clerk's limit: 1000 requests per 10 seconds)
    • Concurrency: 9 concurrent requests (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~35 seconds
  • Development (sk_test_*):
    • Rate limit: 10 requests/second (Clerk's limit: 100 requests per 10 seconds)
    • Concurrency: 1 concurrent request (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~350 seconds

You can override these values by setting RATE_LIMIT or CONCURRENCY_LIMIT in your .env file.

Tuning Concurrency: If you want faster migrations, you can increase CONCURRENCY_LIMIT (e.g., CONCURRENCY_LIMIT=15 for ~150 req/s). Note that higher concurrency may trigger rate limit errors (429), which are automatically retried.

Commands

Run migration

bun migrate

Delete users

bun delete

This will delete all migrated users from the instance. It should not delete pre-existing users, but it is not recommended to use this with a production instance that has pre-existing users. Please use caution with this command.

Clean logs

bun clean-logs

All migrations and deletions will create logs in the ./logs folder. This command will delete those logs.

Convert logs from NDJSON to JSON

bun convert-logs

Convert Logs Utility

Converts NDJSON (Newline-Delimited JSON) log files to standard JSON array format for easier analysis in spreadsheets, databases, or other tools.

Usage

bun convert-logs

The utility will:

  1. List all .log files in the ./logs directory
  2. Let you select which files to convert
  3. Create corresponding .json files with the converted data

Example

Input (migration-2026-01-27T12:00:00.log):

{"userId":"user_1","status":"success","clerkUserId":"clerk_abc123"}
{"userId":"user_2","status":"error","error":"Email already exists"}
{"userId":"user_3","status":"fail","error":"invalid_type for required field.","path":["email"],"row":5}

Output (migration-2026-01-27T12:00:00.json):

[
{
"userId": "user_1",
"status": "success",
"clerkUserId": "clerk_abc123"
},
{
"userId": "user_2",
"status": "error",
"error": "Email already exists"
},
{
"userId": "user_3",
"status": "fail",
"error": "invalid_type for required field.",
"path": ["email"],
"row": 5
}
]

Why NDJSON for Logs?

The tool uses NDJSON for log files because:

  • Streaming: Can append entries as they happen without rewriting the file
  • Crash-safe: If the process crashes, all entries written so far are valid
  • Memory efficient: Can process line-by-line without loading entire log
  • Scalable: Works efficiently with thousands or millions of entries
  • Real-time: Can monitor with tail -f and see entries as they're written

When to Convert

Convert logs to JSON arrays when you need to:

  • Import into Excel, Google Sheets, or other spreadsheet tools
  • Load into a database for analysis
  • Process with tools that expect JSON arrays
  • Share logs with team members less familiar with NDJSON

Analyzing Logs

With NDJSON (original format)

# Count successful imports
grep '"status":"success"' logs/migration-2026-01-27T12:00:00.log | wc -l
# Find all errors
grep '"status":"error"' logs/migration-2026-01-27T12:00:00.log
# Get specific user
grep '"userId":"user_123"' logs/migration-2026-01-27T12:00:00.log

With JSON Arrays (converted format)

// Load in Node.js/JavaScriptconstlogs=require('./logs/migration-2026-01-27T12:00:00.json');// Filter successful importsconstsuccessful=logs.filter((entry)=>entry.status==='success');// Count errors by typeconsterrorCounts=logs.filter((entry)=>entry.status==='error').reduce((acc,entry)=>{acc[entry.error]=(acc[entry.error]||0)+1;returnacc;},{});
# Load in Pythonimportjsonwithopen('logs/migration-2026-01-27T12:00:00.json') asf:
logs=json.load(f)
# Count by statusfromcollectionsimportCounterstatus_counts=Counter(entry['status'] forentryinlogs)

About

No description, website, or topics provided.

Resources

Stars

61 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Clerk User Migration Tool

Description

This repository contains a tool that takes a JSON file as input, containing a list of users, and creates a user in Clerk using Clerk's backend API. The tool respects rate limits and handles errors.

Table of Contents

Documentation

Getting Started

Clone the repository and install the dependencies.

git clone git@github.com:clerk/migration-tool
cd migration-tool
bun install

Users file

The tool is designed to import from multiple sources, including moving users from one Clerk instance to another. You may need to edit the transformer for your source. Please see below for more information on that.

The tool will import from a CSV or JSON. It accounts for empty fields in a CSV and will remove them when converting from CSV to a javascript object.

The only required fields are userId and an identifier (one of email, phone or username).

Samples

The samples/ folder contains some samples you can test with. The samples include issues that will produce errors when running the import.

Some sample users have passwords. The password is Kk4aPMeiaRpAs2OeX1NE.

Secret Key

You have several options for providing your Clerk secret key:

Option 1: Create a .env file (recommended for repeated use)

CLERK_SECRET_KEY=your-secret-key

Option 2: Pass via command line (useful for automation/AI agents)

bun migrate --clerk-secret-key sk_test_xxx

Option 3: Set environment variable

export CLERK_SECRET_KEY=sk_test_xxx
bun migrate

Option 4: Enter interactively

If no key is found, the interactive CLI will prompt you to enter one and optionally save it to a .env file.

You can find your secret key in the Clerk Dashboard under API Keys.

Run the tool

bun migrate

The tool will begin processing users and attempting to import them into Clerk. The tool respects rate limits for the Clerk Backend API. If the tool hits a rate limit, it will wait 10 seconds and retry (up to 5 times). Any errors will be logged to timestamped log files in the ./logs folder.

The tool can be run on the same data multiple times. Clerk automatically uses the email as a unique key so users won't be created again.

Error Handling & Resuming: If the migration stops for any reason (error, interruption, etc.), the tool will display the last processed user ID. You can resume the migration from that point by providing the user ID when prompted, or by using:

bun migrate --resume-after="user_xxx"

CLI Reference

The migration tool supports both interactive and non-interactive modes.

Usage

bun migrate [OPTIONS]

Options

OptionDescription
-t, --transformer <transformer>Source transformer (clerk, auth0, authjs, firebase, supabase)
-f, --file <path>Path to the user data file (JSON or CSV)
-r, --resume-after <userId>Resume migration after this user ID
--require-passwordOnly migrate users who have passwords (by default, users without passwords are migrated)
-y, --yesNon-interactive mode (skip all confirmations)
-h, --helpShow help message

Authentication Options

OptionDescription
--clerk-secret-key <key>Clerk secret key (alternative to .env file)

Firebase Options

Required when --transformer is firebase:

OptionDescription
--firebase-signer-key <key>Firebase hash signer key (base64)
--firebase-salt-separator <sep>Firebase salt separator (base64)
--firebase-rounds <num>Firebase hash rounds
--firebase-mem-cost <num>Firebase memory cost

Examples

# Interactive mode (default)
bun migrate
# Non-interactive mode with required options
bun migrate -y -t auth0 -f users.json
# Non-interactive with secret key (no .env needed)
bun migrate -y -t clerk -f users.json --clerk-secret-key sk_test_xxx
# Resume a failed migration
bun migrate -y -t clerk -f users.json -r user_abc123
# Firebase migration with hash config
bun migrate -y -t firebase -f users.csv \
--firebase-signer-key "abc123..." \
--firebase-salt-separator "Bw==" \
--firebase-rounds 8 \
--firebase-mem-cost 14

Non-Interactive Mode

For automation and AI agent usage, use the -y flag with required options:

bun migrate -y \
--transformer clerk \
--file users.json \
--clerk-secret-key sk_test_xxx

Required in non-interactive mode:

  • --transformer (or -t)
  • --file (or -f)
  • CLERK_SECRET_KEY (via --clerk-secret-key, environment variable, or .env file)

Exporting Users

Some platforms require exporting users directly from their database before migrating. See the Exporting Users guide for setup, CLI options, and troubleshooting.

bun export:supabase

Migrating OAuth Connections

OAuth connections can not be directly migrated. The creation of the connection requires the user to consent, which can't happen on a migration like this. Instead you can rely on Clerk's Account Linking to handle this.

Handle Existing User IDs and Foreign Key Constraints

When migrating from another authentication system, you likely have data in your database tied to your previous system's user IDs. To maintain data consistency as you move to Clerk, you'll need a strategy to handle these foreign key relationships. Below are several approaches.

Custom session claims

Our sessions allow for conditional expressions. This would allow you add a session claim that will return either the externalId (the previous id for your user) when it exists, or the userId from Clerk. This will result in your imported users returning their externalId while newer users will return the Clerk userId.

In your Dashboard, go to Sessions -> Edit. Add the following:

{
"userId": "{{user.externalId || user.id}}"
}

You can now access this value using the following:

const{ sessionClaims }=auth();console.log(sessionClaims.userId);

You can add the following for typescript:

// types/global.d.tsexport{};declareglobal{interfaceCustomJwtSessionClaims{userId?: string;}}

Other options

You could continue to generate unique ids for the database as done previously, and then store those in externalId. This way all users would have an externalId that would be used for DB interactions.

You could add a column in your user table inside of your database called ClerkId. Use that column to store the userId from Clerk directly into your database.

Configuration

The tool can be configured through the following environment variables:

VariableDescription
CLERK_SECRET_KEYYour Clerk secret key
RATE_LIMITRate limit in requests/second (auto-configured: 100 for prod, 10 for dev)
CONCURRENCY_LIMITNumber of concurrent requests (auto-configured: ~9 for prod, ~1 for dev)

The tool automatically detects production vs development instances from your CLERK_SECRET_KEY and sets appropriate rate limits and concurrency:

  • Production (sk_live_*):
    • Rate limit: 100 requests/second (Clerk's limit: 1000 requests per 10 seconds)
    • Concurrency: 9 concurrent requests (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~35 seconds
  • Development (sk_test_*):
    • Rate limit: 10 requests/second (Clerk's limit: 100 requests per 10 seconds)
    • Concurrency: 1 concurrent request (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~350 seconds

You can override these values by setting RATE_LIMIT or CONCURRENCY_LIMIT in your .env file.

Tuning Concurrency: If you want faster migrations, you can increase CONCURRENCY_LIMIT (e.g., CONCURRENCY_LIMIT=15 for ~150 req/s). Note that higher concurrency may trigger rate limit errors (429), which are automatically retried.

Commands

Run migration

bun migrate

Delete users

bun delete

This will delete all migrated users from the instance. It should not delete pre-existing users, but it is not recommended to use this with a production instance that has pre-existing users. Please use caution with this command.

Clean logs

bun clean-logs

All migrations and deletions will create logs in the ./logs folder. This command will delete those logs.

Convert logs from NDJSON to JSON

bun convert-logs

Convert Logs Utility

Converts NDJSON (Newline-Delimited JSON) log files to standard JSON array format for easier analysis in spreadsheets, databases, or other tools.

Usage

bun convert-logs

The utility will:

  1. List all .log files in the ./logs directory
  2. Let you select which files to convert
  3. Create corresponding .json files with the converted data

Example

Input (migration-2026-01-27T12:00:00.log):

{"userId":"user_1","status":"success","clerkUserId":"clerk_abc123"}
{"userId":"user_2","status":"error","error":"Email already exists"}
{"userId":"user_3","status":"fail","error":"invalid_type for required field.","path":["email"],"row":5}

Output (migration-2026-01-27T12:00:00.json):

[
{
"userId": "user_1",
"status": "success",
"clerkUserId": "clerk_abc123"
},
{
"userId": "user_2",
"status": "error",
"error": "Email already exists"
},
{
"userId": "user_3",
"status": "fail",
"error": "invalid_type for required field.",
"path": ["email"],
"row": 5
}
]

Why NDJSON for Logs?

The tool uses NDJSON for log files because:

  • Streaming: Can append entries as they happen without rewriting the file
  • Crash-safe: If the process crashes, all entries written so far are valid
  • Memory efficient: Can process line-by-line without loading entire log
  • Scalable: Works efficiently with thousands or millions of entries
  • Real-time: Can monitor with tail -f and see entries as they're written

When to Convert

Convert logs to JSON arrays when you need to:

  • Import into Excel, Google Sheets, or other spreadsheet tools
  • Load into a database for analysis
  • Process with tools that expect JSON arrays
  • Share logs with team members less familiar with NDJSON

Analyzing Logs

With NDJSON (original format)

# Count successful imports
grep '"status":"success"' logs/migration-2026-01-27T12:00:00.log | wc -l
# Find all errors
grep '"status":"error"' logs/migration-2026-01-27T12:00:00.log
# Get specific user
grep '"userId":"user_123"' logs/migration-2026-01-27T12:00:00.log

With JSON Arrays (converted format)

// Load in Node.js/JavaScriptconstlogs=require('./logs/migration-2026-01-27T12:00:00.json');// Filter successful importsconstsuccessful=logs.filter((entry)=>entry.status==='success');// Count errors by typeconsterrorCounts=logs.filter((entry)=>entry.status==='error').reduce((acc,entry)=>{acc[entry.error]=(acc[entry.error]||0)+1;returnacc;},{});
# Load in Pythonimportjsonwithopen('logs/migration-2026-01-27T12:00:00.json') asf:
logs=json.load(f)
# Count by statusfromcollectionsimportCounterstatus_counts=Counter(entry['status'] forentryinlogs)

About

No description, website, or topics provided.

Resources

Stars

61 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Clerk User Migration Tool

Description

This repository contains a tool that takes a JSON file as input, containing a list of users, and creates a user in Clerk using Clerk's backend API. The tool respects rate limits and handles errors.

Table of Contents

Documentation

Getting Started

Clone the repository and install the dependencies.

git clone git@github.com:clerk/migration-tool
cd migration-tool
bun install

Users file

The tool is designed to import from multiple sources, including moving users from one Clerk instance to another. You may need to edit the transformer for your source. Please see below for more information on that.

The tool will import from a CSV or JSON. It accounts for empty fields in a CSV and will remove them when converting from CSV to a javascript object.

The only required fields are userId and an identifier (one of email, phone or username).

Samples

The samples/ folder contains some samples you can test with. The samples include issues that will produce errors when running the import.

Some sample users have passwords. The password is Kk4aPMeiaRpAs2OeX1NE.

Secret Key

You have several options for providing your Clerk secret key:

Option 1: Create a .env file (recommended for repeated use)

CLERK_SECRET_KEY=your-secret-key

Option 2: Pass via command line (useful for automation/AI agents)

bun migrate --clerk-secret-key sk_test_xxx

Option 3: Set environment variable

export CLERK_SECRET_KEY=sk_test_xxx
bun migrate

Option 4: Enter interactively

If no key is found, the interactive CLI will prompt you to enter one and optionally save it to a .env file.

You can find your secret key in the Clerk Dashboard under API Keys.

Run the tool

bun migrate

The tool will begin processing users and attempting to import them into Clerk. The tool respects rate limits for the Clerk Backend API. If the tool hits a rate limit, it will wait 10 seconds and retry (up to 5 times). Any errors will be logged to timestamped log files in the ./logs folder.

The tool can be run on the same data multiple times. Clerk automatically uses the email as a unique key so users won't be created again.

Error Handling & Resuming: If the migration stops for any reason (error, interruption, etc.), the tool will display the last processed user ID. You can resume the migration from that point by providing the user ID when prompted, or by using:

bun migrate --resume-after="user_xxx"

CLI Reference

The migration tool supports both interactive and non-interactive modes.

Usage

bun migrate [OPTIONS]

Options

OptionDescription
-t, --transformer <transformer>Source transformer (clerk, auth0, authjs, firebase, supabase)
-f, --file <path>Path to the user data file (JSON or CSV)
-r, --resume-after <userId>Resume migration after this user ID
--require-passwordOnly migrate users who have passwords (by default, users without passwords are migrated)
-y, --yesNon-interactive mode (skip all confirmations)
-h, --helpShow help message

Authentication Options

OptionDescription
--clerk-secret-key <key>Clerk secret key (alternative to .env file)

Firebase Options

Required when --transformer is firebase:

OptionDescription
--firebase-signer-key <key>Firebase hash signer key (base64)
--firebase-salt-separator <sep>Firebase salt separator (base64)
--firebase-rounds <num>Firebase hash rounds
--firebase-mem-cost <num>Firebase memory cost

Examples

# Interactive mode (default)
bun migrate
# Non-interactive mode with required options
bun migrate -y -t auth0 -f users.json
# Non-interactive with secret key (no .env needed)
bun migrate -y -t clerk -f users.json --clerk-secret-key sk_test_xxx
# Resume a failed migration
bun migrate -y -t clerk -f users.json -r user_abc123
# Firebase migration with hash config
bun migrate -y -t firebase -f users.csv \
--firebase-signer-key "abc123..." \
--firebase-salt-separator "Bw==" \
--firebase-rounds 8 \
--firebase-mem-cost 14

Non-Interactive Mode

For automation and AI agent usage, use the -y flag with required options:

bun migrate -y \
--transformer clerk \
--file users.json \
--clerk-secret-key sk_test_xxx

Required in non-interactive mode:

  • --transformer (or -t)
  • --file (or -f)
  • CLERK_SECRET_KEY (via --clerk-secret-key, environment variable, or .env file)

Exporting Users

Some platforms require exporting users directly from their database before migrating. See the Exporting Users guide for setup, CLI options, and troubleshooting.

bun export:supabase

Migrating OAuth Connections

OAuth connections can not be directly migrated. The creation of the connection requires the user to consent, which can't happen on a migration like this. Instead you can rely on Clerk's Account Linking to handle this.

Handle Existing User IDs and Foreign Key Constraints

When migrating from another authentication system, you likely have data in your database tied to your previous system's user IDs. To maintain data consistency as you move to Clerk, you'll need a strategy to handle these foreign key relationships. Below are several approaches.

Custom session claims

Our sessions allow for conditional expressions. This would allow you add a session claim that will return either the externalId (the previous id for your user) when it exists, or the userId from Clerk. This will result in your imported users returning their externalId while newer users will return the Clerk userId.

In your Dashboard, go to Sessions -> Edit. Add the following:

{
"userId": "{{user.externalId || user.id}}"
}

You can now access this value using the following:

const{ sessionClaims }=auth();console.log(sessionClaims.userId);

You can add the following for typescript:

// types/global.d.tsexport{};declareglobal{interfaceCustomJwtSessionClaims{userId?: string;}}

Other options

You could continue to generate unique ids for the database as done previously, and then store those in externalId. This way all users would have an externalId that would be used for DB interactions.

You could add a column in your user table inside of your database called ClerkId. Use that column to store the userId from Clerk directly into your database.

Configuration

The tool can be configured through the following environment variables:

VariableDescription
CLERK_SECRET_KEYYour Clerk secret key
RATE_LIMITRate limit in requests/second (auto-configured: 100 for prod, 10 for dev)
CONCURRENCY_LIMITNumber of concurrent requests (auto-configured: ~9 for prod, ~1 for dev)

The tool automatically detects production vs development instances from your CLERK_SECRET_KEY and sets appropriate rate limits and concurrency:

  • Production (sk_live_*):
    • Rate limit: 100 requests/second (Clerk's limit: 1000 requests per 10 seconds)
    • Concurrency: 9 concurrent requests (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~35 seconds
  • Development (sk_test_*):
    • Rate limit: 10 requests/second (Clerk's limit: 100 requests per 10 seconds)
    • Concurrency: 1 concurrent request (~95% of rate limit with 100ms API latency)
    • Typical migration speed: ~3,500 users in ~350 seconds

You can override these values by setting RATE_LIMIT or CONCURRENCY_LIMIT in your .env file.

Tuning Concurrency: If you want faster migrations, you can increase CONCURRENCY_LIMIT (e.g., CONCURRENCY_LIMIT=15 for ~150 req/s). Note that higher concurrency may trigger rate limit errors (429), which are automatically retried.

Commands

Run migration

bun migrate

Delete users

bun delete

This will delete all migrated users from the instance. It should not delete pre-existing users, but it is not recommended to use this with a production instance that has pre-existing users. Please use caution with this command.

Clean logs

bun clean-logs

All migrations and deletions will create logs in the ./logs folder. This command will delete those logs.

Convert logs from NDJSON to JSON

bun convert-logs

Convert Logs Utility

Converts NDJSON (Newline-Delimited JSON) log files to standard JSON array format for easier analysis in spreadsheets, databases, or other tools.

Usage

bun convert-logs

The utility will:

  1. List all .log files in the ./logs directory
  2. Let you select which files to convert
  3. Create corresponding .json files with the converted data

Example

Input (migration-2026-01-27T12:00:00.log):

{"userId":"user_1","status":"success","clerkUserId":"clerk_abc123"}
{"userId":"user_2","status":"error","error":"Email already exists"}
{"userId":"user_3","status":"fail","error":"invalid_type for required field.","path":["email"],"row":5}

Output (migration-2026-01-27T12:00:00.json):

[
{
"userId": "user_1",
"status": "success",
"clerkUserId": "clerk_abc123"
},
{
"userId": "user_2",
"status": "error",
"error": "Email already exists"
},
{
"userId": "user_3",
"status": "fail",
"error": "invalid_type for required field.",
"path": ["email"],
"row": 5
}
]

Why NDJSON for Logs?

The tool uses NDJSON for log files because:

  • Streaming: Can append entries as they happen without rewriting the file
  • Crash-safe: If the process crashes, all entries written so far are valid
  • Memory efficient: Can process line-by-line without loading entire log
  • Scalable: Works efficiently with thousands or millions of entries
  • Real-time: Can monitor with tail -f and see entries as they're written

When to Convert

Convert logs to JSON arrays when you need to:

  • Import into Excel, Google Sheets, or other spreadsheet tools
  • Load into a database for analysis
  • Process with tools that expect JSON arrays
  • Share logs with team members less familiar with NDJSON

Analyzing Logs

With NDJSON (original format)

# Count successful imports
grep '"status":"success"' logs/migration-2026-01-27T12:00:00.log | wc -l
# Find all errors
grep '"status":"error"' logs/migration-2026-01-27T12:00:00.log
# Get specific user
grep '"userId":"user_123"' logs/migration-2026-01-27T12:00:00.log

With JSON Arrays (converted format)

// Load in Node.js/JavaScriptconstlogs=require('./logs/migration-2026-01-27T12:00:00.json');// Filter successful importsconstsuccessful=logs.filter((entry)=>entry.status==='success');// Count errors by typeconsterrorCounts=logs.filter((entry)=>entry.status==='error').reduce((acc,entry)=>{acc[entry.error]=(acc[entry.error]||0)+1;returnacc;},{});
# Load in Pythonimportjsonwithopen('logs/migration-2026-01-27T12:00:00.json') asf:
logs=json.load(f)
# Count by statusfromcollectionsimportCounterstatus_counts=Counter(entry['status'] forentryinlogs)

About

No description, website, or topics provided.

Resources

Stars

61 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages