The old URL is part of your data contract

A project already has an ID, a task referring to it, and a bookmarked URL. You want new projects to use CUID2. The least disruptive change in this example is to keep the existing ID and broaden readers before replacing the writer. The CUID v1 guide explains the legacy fields; here we work through the behavior that needs to survive a rollout.

The original CUID project is deprecated and points users to CUID2. That recommendation concerns future generation; it does not give your application a mapping that can replace every existing key and external link. Source: the legacy CUID repository.

Our example assumes legacy IDs are 25 characters and new CUID2 IDs use the default 24-character length. Inventory actual stored values before adopting that assumption. If your system used custom lengths, truncated IDs, or several generators, its compatibility rule will need to be different.

First, teach readers to accept both formats

A route rule for this application's two formats
javascript
const acceptsRouteId = (value) => typeof value === "string" &&
  (/^c[a-z0-9]{24}$/.test(value) || /^[a-z][a-z0-9]{23}$/.test(value));

function findProject(id) {
  if (!acceptsRouteId(id)) return undefined;
  return db.prepare("SELECT id, name FROM project WHERE id = ?").get(id);
}

This is an input-shape gate, not a detector that proves which generator created a string. The next step is an exact, parameterized database lookup. The download demonstrates that a constructed 24-character string can pass the gate while returning no record. In a real application, follow the lookup with the normal authorization checks.

Deploy this reader change everywhere that consumes IDs: route handlers, API validators, background jobs, webhook consumers, and client-side forms. Check storage constraints too. A database that accepts only 25 characters would reject new writes even if every application validator had been updated.

Then change new writes, keeping old values intact

Replace the generator at the creation boundary
javascript
import { createId } from "@paralleldrive/cuid2";

const newId = createId();
db.prepare("INSERT INTO project (id, name) VALUES (?, ?)")
  .run(newId, "New project");

// Existing project IDs and task.project_id values are left intact.

During a rolling deployment, old and new writers may coexist. That is acceptable for this example because all readers now accept both shapes and the database still enforces uniqueness. Do not remove legacy support just because the writer rollout finished: old records and links may remain valid for years.

A rollback also has two parts. Reverting a writer does not erase the new CUID2 records created since deployment. Keep the broadened readers in any rollback build. If a migration must replace the old public identifiers themselves, you need a separate mapping and redirect design, along with a plan for external consumers; swapping the generator alone cannot do that.

Prove that the old relationship still resolves

This example was tested with Node.js 24.5.0, its built-in SQLite module, and CUID2 3.3.0. Save cuid-migration.mjs beside your package installation and run it with Node.js 24:

Run the in-memory migration example
bash
npm install --save-exact @paralleldrive/cuid2@3.3.0
node cuid-migration.mjs

The script inserts a legacy project and a task referencing it, switches new project creation to CUID2, and checks both records. It parses the old URL and resolves its ID through the same lookup function. A join verifies that the task still reaches its original parent, and a duplicate insert must fail. The database is in memory and closes when the script finishes.

A successful run ends with “Migration checks passed: old URL, old relationship, new ID, and duplicate rejection.” Node may also print an experimental warning for its SQLite module. The assertions establish these local data behaviors; they do not test your deployed router, cache, or third-party integrations.

Carry the example into a rollout check

Before enabling new writes, run an equivalent check against a staging copy of your application with representative legacy records. Exercise an old bookmarked URL through the actual HTTP route, create a new record, and read both through each supported client. Watch format rejections, failed lookups, and database constraint errors during rollout.

If an old endpoint derives creation time from CUID v1, give new records an explicit timestamp and update that endpoint before sending it CUID2 values. A timestamp field also supports the ordering strategy in the PostgreSQL walkthrough. Once compatibility is verified, existing IDs can remain ordinary opaque lookup keys.