Auto UUID
Strapi 5 custom field

UUIDs that stay put

Add a UUID field in the Content-Type Builder and every entry gets a UUID v4 or UUID v7 value on save, from the admin, REST, GraphQL or the Document Service. Duplicates are rejected, and values stay locked unless you allow editing.

Strapi 5.24+ Node.js 22.12+ UUID v4 and v7 5 admin languages No telemetry MIT
UUID management page in the Strapi admin with counts for content types, UUID fields, entries, duplicates, empty and malformed values, and repair buttons

Features

One custom field, enforced on the server for every write path, plus an admin page to check and repair what is already stored.

Generated on the server

Random v4 or time-sortable v7, with an optional prefix such as usr_. REST, GraphQL, the Document Service and the admin all get a value.

Unique and locked

Checked on every save across documents, locales, drafts and published rows, case insensitive. Values cannot change unless the field allows editing.

Check and repair

See duplicates, empty and malformed values per field. Repairs show a preview, ask for confirmation and run in one transaction.

Export and import

Back up every UUID as JSON and restore it into empty fields. Large exports are split into files that fit one import each.

Several instances

Railway replicas and pm2 cluster workers share one database. Locks and rate limits live there, so you do not need Redis or sticky sessions.

Nothing leaves your server

No outbound requests, no telemetry and no license check. The admin UI uses the Strapi Design System in light and dark mode, in five languages.

Install

Requires Strapi 5.24 or newer (acceptance host: Strapi 5.61.0) and Node.js 22.12 or newer. A fresh Strapi app installs it with npm, Yarn 1 or pnpm without a single peer dependency warning from this package.

  1. Add the package

    Terminalnpm, yarn or pnpm
    npm install strapi-auto-uuid-v5
  2. Enable it

    config/plugins.ts
    export default () => ({
      'field-uuid': {
        enabled: true,
      },
    });

    JavaScript projects use config/plugins.js with module.exports = { 'field-uuid': { enabled: true } };

  3. Add the custom field

    Run npm run build && npm run develop. In the Content-Type Builder choose Add another field, Custom, UUID, pick v4 or v7, and save an entry.

Super Admins see Settings > Auto UUID > UUID management right away. Other roles need the permissions listed under Security.

Examples

A content type ticket with a v7 field ticketId and the prefix tkt_. All values are synthetic.

schema.json (written by the Content-Type Builder)
"ticketId": {
  "type": "customField",
  "customField": "plugin::field-uuid.uuid",
  "unique": true,
  "options": { "uuid-version": "v7", "uuid-prefix": "tkt_" }
}
RESTPOST /api/tickets
// request
{ "data": { "title": "Printer offline" } }

// response
{
  "data": {
    "documentId": "k2v8q4n6x0c1m3b5z7l9p2r4",
    "title": "Printer offline",
    "ticketId": "tkt_019a5f3c-8b2e-7d41-9c3a-5e2f1b7a4d10"
  },
  "meta": {}
}
Server codeDocument Service
const ticket = await strapi.documents('api::ticket.ticket').create({
  data: { title: 'Printer offline' },
});
strapi.log.info(ticket.ticketId); // tkt_019a5f3c-...
A rejected write400
{
  "data": null,
  "error": {
    "status": 400,
    "name": "ValidationError",
    "message": "UUID already exists for field 'ticketId'. Please use a unique value.",
    "details": { "field": "ticketId", "code": "DUPLICATE_UUID" }
  }
}

Other codes: INVALID_UUID, IMMUTABLE_UUID, BULK_UUID_WRITE, INVALID_UUID_FIELD_CONFIG and UUID_TARGET_UNKNOWN.

How values are protected

The plugin's lifecycle hooks apply these rules to every write, whatever the source.

Situation Result
Create with an empty field A value is generated, unless auto-generation is off for the field.
Create with a value, field not editable The value is replaced with a generated one.
Create with a value, field editable Kept if it matches the version and prefix and no other document uses it, otherwise 400.
Publish, discard draft, new locale The document keeps its value.
Clone The copy gets a new value. The admin clone page clears the copied value first.
Same UUID in other letter case A duplicate (400). New values are stored in lowercase.
Update with a different value Only for editable fields, or once while the field is empty.
updateMany that sets one UUID on several documents Rejected with 400.

A localized UUID field has its own value per locale; a non-localized one is shared by all locales of a document. UUID fields inside components are filled by the admin only; the server does not generate, validate or repair them.

Configuration

Plugin defaults go under the field-uuid key in config/plugins.js or .ts. All keys are optional; unknown keys and wrong types fail at boot.

Option Type Default Effect
defaultVersion 'v4' | 'v7' 'v4' Version for fields that do not set one.
autoGenerate boolean true Generate on create and fill empty fields on save, unless off for the field.
validateUniqueness boolean true Reject a value that another document already uses.
maxRetryAttempts integer 1 to 10 3 Retries when a generated value is already taken.
autoMigrate boolean false Repair empty, malformed and duplicate values at startup. Never replaces values in an older version or prefix.
logLevel debug, info, warn, error 'info' Minimum level of the plugin's log lines.
rateLimitStore auto, database, memory 'auto' Where admin rate-limit counters live. auto uses the database on PostgreSQL and MySQL and memory on SQLite.

Field options

Option in the Content-Type Builder Default Effect
UUID version v4 v4 is random, v7 is time-sortable.
Prefix none 1 to 16 letters, numbers, _ or -.
Unique values on Strapi's publish check. Not a database constraint; the plugin rejects duplicates on every save either way.
Private field off The field is not returned by the API.
Disable auto-generation off Editors enter the value; without manual editing it is then locked.
Allow manual editing off Editors and API clients can set and change the value.

Security, privacy and permissions

The plugin has no Content API routes. Its management routes need an admin JWT and a plugin permission, checked on the server.

Action UID Allows
plugin::field-uuid.read The settings page, counts and the check.
plugin::field-uuid.migrate Replace duplicates and malformed values, fill empty fields.
plugin::field-uuid.export Export every documentId and UUID of every content type, regardless of Content Manager permissions. Grant it like a backup permission.
plugin::field-uuid.import Fill empty UUID fields from an export.
  • Request bodies are validated with strict zod schemas; unknown keys are rejected.
  • Errors never contain stack traces, database errors or stored values. Logs record the admin id for repairs, exports and imports, never values or IP addresses.
  • Rate limits per admin and minute; with the database store they hold across all instances. Keys are SHA-256 hashes.
  • No outbound network requests, no telemetry, no analytics and no license check.

Report vulnerabilities privately through GitHub security advisories.

Admin API

Admin routes under /field-uuid. Write routes default to a preview; send "dryRun": false to write.

Method and path Permission Limit / min Purpose
GET /health admin login none Plugin status.
GET /stats read 20 Counts and UUID fields per content type.
GET /models read none UUID fields per content type.
GET /diagnose read 5 Counts per field, no values.
GET /migration/status read 10 Counts per field, no values.
POST /autofix migrate 5 Replace duplicates.
POST /generate-missing migrate 5 Fill empty fields.
POST /migration/run migrate 3 Selected fixes: fixEmpty, fixInvalid, fixDuplicates, fixLegacy.
GET /migration/export export 3 Download all mappings as JSON.
POST /migration/import import 3 Fill empty fields from an export. At most 10,000 entries; the body must fit Strapi's JSON limit (1 MB by default).

Status codes: 400 invalid body, 401 no admin login, 403 missing permission, 409 REPAIR_IN_PROGRESS while another repair or import runs on any instance, 429 rate limit. Only the export contains documentIds and UUID values.

Repair previewPOST /field-uuid/migration/run
// request
{ "fixEmpty": true, "fixDuplicates": true, "fixInvalid": true }

// response (dryRun defaults to true, nothing is written)
{
  "dryRun": true,
  "fixed": { "empty": 3, "invalid": 0, "legacy": 0, "duplicates": 1 },
  "skipped": 0,
  "errors": [],
  "totalFixed": 4
}

Performance

Checks and exports read rows in pages of 5,000 and select only the columns they need. Imports look up 500 entries per query batch. On SQLite with about 5,000 synthetic entries, the overview scan took 40 ms and a 5,000-value import 315 ms.

Each save looks up the UUID column a few times. For large tables, add an index with a Strapi database migration:

database/migrations/2026.11.07T00.00.00.uuid-indexes.js
'use strict';

const INDEXES = [['tickets', 'ticket_id']];

async function up(knex) {
  for (const [table, column] of INDEXES) {
    if (!(await knex.schema.hasTable(table)) || !(await knex.schema.hasColumn(table, column))) {
      continue;
    }
    await knex.schema.alterTable(table, (t) => t.index([column], `${table}_${column}_uuid_idx`));
  }
}

module.exports = { up };

Running several instances (Railway replicas, pm2 cluster)

Tested with 8 Node processes on one PostgreSQL 16 database (for example 2 Railway replicas with 4 pm2 workers each). The database is the only shared state.

Same client UUID on two processes

The save locks the value before its uniqueness check. One wins, the other gets 400 DUPLICATE_UUID.

Repair started twice

One repair or import runs at a time across all processes. The other answers 409 REPAIR_IN_PROGRESS.

Shared rate limits

Counters live in the table field_uuid_rate_limits and are increased with one atomic upsert, so 8 processes do not allow 8 times the limit.

Locks that clean up

PostgreSQL advisory locks or MySQL row locks, scoped to the transaction. A crashed worker never leaves one behind.

  • PostgreSQL is recommended. MySQL 8 and MariaDB (InnoDB) are supported but not yet tested live. SQLite means one process.
  • The database user must be allowed to create the plugin tables at startup, as for Strapi's own schema sync.
  • Size the pool for all processes: processes x pool.max must stay below max_connections.
  • After a rolling deploy that adds a UUID field, run Check and Repair all once when every replica is up.

Compatibility

Plugin Strapi Node.js Database
2.x 5.24 or newer, acceptance host 5.61.0 22.12 to 26 SQLite, PostgreSQL, MySQL 8 / MariaDB
1.x 5.x as Strapi requires any Strapi database
Strapi 4 Cringe-Studio/strapi-auto-uuid

Required peers are the ones in the Strapi app template (@strapi/strapi, react, react-dom, react-router-dom, styled-components). @strapi/design-system, @strapi/icons and react-intl are optional peers because Strapi installs them itself.

Upgrading

2.1 needs no changes except, with several processes, create-table rights for the database user. The 2.0 migration from 1.x is stricter:

Client UUIDs

Fields with manual editing off replace values sent on create. Turn on Allow manual editing for fields that API clients fill.

Duplicates

A create with a UUID another document uses, in any letter case, returns 400 DUPLICATE_UUID.

Admin API

POST /check-duplicate was removed. Imports take at most 10,000 entries and never accept overwrite: true.

Runtime

Node.js 22.12 or newer and Strapi 5.24 or newer. Unknown config keys fail at boot.

From Strapi 4: rename the plugin key from strapi-auto-uuid to field-uuid, run the check, export a backup, then repair if needed.

FAQ

Is there a database unique constraint?

No. Draft, published and locale rows of one document share a value, so a plain unique index is impossible. The plugin checks every save and serializes concurrent saves with database locks.

Why does Duplicate in the list view fail?

Strapi refuses its quick clone for unique attributes. Use the clone page of the entry, or turn off Unique values for the field.

The import says the file is too large

Export again: the export is split into files that each fit one import.

What about fields inside components?

The admin fills an empty one when the component is shown. The server does not generate, validate or repair them.

Do I need Redis?

No. Locks and rate-limit counters live in the database.

Does it phone home?

No. The plugin makes no network requests and has no license check.

Releases and contributing

Conventional Commits and semantic-release on pushes to main. Every release runs lint, format check, tests (including lifecycle and repair tests on a real SQLite database), build and package verification, then publishes to npm with provenance. Pull requests also run the PostgreSQL and 8-process tests on Node.js 22 and 24.

Terminallocal checks
npm ci --legacy-peer-deps
npm run lint
npm run format:check
npm test
npm run test:concurrency
npm run build
npm run verify