Atomic slug, handle, and username registry for Convex with scoped uniqueness, rename redirects, case folding, and typed rejection reasons.
npm install @vllnt/convex-slugs@vllnt/convex-slugs maps a readable name to an opaque resource reference. Your app can reuse the same registry pattern for profiles, workspaces, content, or any resource that needs a unique human-readable identifier.
reserve enforces uniqueness within a scope. resolve finds the resource, and slugForResource performs a reverse lookup. rename records redirect mappings and collapses redirect chains. Configure length, character patterns, reserved words, and case sensitivity; Unicode NFC normalization is supported.
Your backend authorizes name changes and your router applies redirects. The component does not create web routes or handle impersonation policy. Normalization does not eliminate every visually similar Unicode name; choose validation rules appropriate to your product.
Maintained by [bntvllnt](https://bntvllnt.com) at [VLLNT](https://vllnt.com). Find the author on [GitHub](https://github.com/bntvllnt) and [X](https://x.com/bntvllnt).
The Slug Registry component (@vllnt/convex-slugs) provides a reserve() mutation that runs inside the Convex transaction, making double-reserve impossible by construction. It returns { ok: true } on success or { ok: false, reason } with a typed reason such as SLUG_TAKEN, so you can surface the exact failure to the user without a try/catch.
The rename() method in @vllnt/convex-slugs updates the slug and records an old-to-new redirect in a single mutation. Redirect chains are collapsed automatically, so if a user renames from A to B and then to C, redirectFor(A) returns C directly rather than storing an intermediate hop.
Every method in @vllnt/convex-slugs accepts an optional scope parameter. By passing a tenant ID, locale, or content type string as the scope, you can enforce uniqueness independently per namespace without running separate tables or components.
The slugForResource() query in @vllnt/convex-slugs performs a reverse lookup from an opaque resourceRef back to the slug currently assigned to it within a given scope. This lets you display or validate the current handle for a user without storing it redundantly in your own tables.
@vllnt/convex-slugs runs the reserve() call inside a Convex mutation transaction, so uniqueness is enforced at the database level. Two concurrent reservation attempts for the same (scope, slug) pair cannot both succeed; the second will receive { ok: false, reason: 'SLUG_TAKEN' }.
By default, @vllnt/convex-slugs lowercases slugs and applies Unicode NFC normalization before storing or comparing them. This prevents homoglyph collisions where visually similar characters would otherwise produce separate entries. Case folding can be disabled by passing foldCase: false to the Slugs constructor.
A resourceRef is an arbitrary opaque string that the host application associates with a slug at reservation time, such as a Convex document ID or a user ID. The component stores and returns it without interpreting it, so authentication and authorization remain entirely in the host application.
When rename() is called in @vllnt/convex-slugs, a redirect record is written mapping the old slug to the new slug. If the same source slug is renamed again later, the chain is collapsed so that redirectFor() always returns the current canonical slug in one lookup, with no intermediate hops stored.
Yes. The optional @vllnt/convex-slugs/react entrypoint exports useSlugAvailable() and useResolve(), which are thin wrappers over Convex useQuery. useSlugAvailable() returns { available, resourceRef } where available is undefined while loading, true when the slug is free, and false when it is held. React 18 or later is required as an optional peer dependency.