seeded_nanoid

Deterministic, URL-friendly identifiers for Dart and Flutter, inspired by Nano ID.

Instead of random nano ids, this package provides an explicit seeded API to generate identical seed bytes always produce identical identifiers.

Goal

Provide a NanoId like package to generate IDs for your data, like invoices, orders, requests and other similar cases.

Be aware

The IDs generated by this package are not advised for secure passwords since they CAN BE BROKEN if the listener is able to calculate the seed.

As said earlier, use this for simple identifiers, that only require a "collision free" implementation.

Usage

import 'package:seeded_nanoid/seeded_nanoid.dart';

final date = DateTime.utc(2026, 5, 26).toIso8601String();
final id = nanoid(seed: 'daily-offer:$date');

nanoid() uses Nano ID's default URL-safe alphabet and 21-character length.

Specify a length or create a reusable custom-alphabet generator when needed:

final shortId = nanoid(seed: 'invoice:9417', size: 12);

final hexIds = customAlphabet('0123456789abcdef', size: 16);
final hexId = hexIds.generate('invoice:9417');

For binary seed material, use SeededNanoId().generateBytes(bytes).

Collision Contract

nanoid(seed: ...) is stable and deterministic, but it cannot guarantee that two distinct seeds never collide: it intentionally compresses arbitrary input into a fixed-length output.

When the input is already unique and a collision-free URL-safe representation is required, use reversible encoding:

final id = encodeSeed('customer:9417');
final seed = decodeSeed(id);

encodeSeed() and encodeSeedBytes() are unpadded Base64 URL encodings.

Distinct input byte sequences always have distinct output, at the cost of variable-length IDs. Namespace identifiers when domains may overlap, such as customer:42 and invoice:42.

Stability And Security

Compact seeded IDs use a deterministic non-cryptographic algorithm. Version 1 defines u32(value) as the low unsigned 32 bits of value and imul32(a, b) as the low unsigned 32 bits of a * b. Its seed mixer starts with:

input = UTF8("seeded_nanoid/v1\0") || seedBytes
h1 = 1779033703
h2 = 3144134277
h3 = 1013904242
h4 = 2773480762

for byte in input:
  h1 = u32(h2 XOR imul32(h1 XOR byte, 597399067))
  h2 = u32(h3 XOR imul32(h2 XOR byte, 2869860233))
  h3 = u32(h4 XOR imul32(h3 XOR byte, 951274213))
  h4 = u32(h1 XOR imul32(h4 XOR byte, 2716044179))

h1 = imul32(h3 XOR (h1 >>> 18), 597399067)
h2 = imul32(h4 XOR (h2 >>> 22), 2869860233)
h3 = imul32(h1 XOR (h3 >>> 17), 951274213)
h4 = imul32(h2 XOR (h4 >>> 19), 2716044179)
h1 = u32(h1 XOR h2 XOR h3 XOR h4)
h2 = u32(h2 XOR h1)
h3 = u32(h3 XOR h1)
h4 = u32(h4 XOR h1)

Assignments above are evaluated top to bottom. The four final values seed this sfc32 stream, with a = h1, b = h2, c = h3, and d = h4:

nextWord():
  word = u32(a + b + d)
  d = u32(d + 1)
  a = u32(b XOR (b >>> 9))
  b = u32(c + (c << 3))
  c = u32((c << 21) OR (c >>> 11))
  c = u32(c + word)
  return word

The first 12 generated words are discarded, then each word is emitted least-significant byte first.

Generated bytes are mapped onto Unicode scalar values in the supplied alphabet; values outside the largest evenly divisible byte range are rejected to avoid modulo bias. Changing this output contract would require a breaking package release.

AND REMEMBER

Seeded IDs are not secret tokens!

A predictable seed such as a date or database key permits an observer to compute the same identifier!

If you plan on using this package to generate secure IDs, be sure to provide a secure identifier or an authenticated secret-based construction for session tokens, password reset links, API keys, and similar security-sensitive values.

Credits

The default alphabet and API model are based on the MIT-licensed ai/nanoid project by Andrey Sitnik.

Libraries

seeded_nanoid
Deterministic Nano ID-style identifiers based on caller-provided seeds.