Slug Registry

Atomic slug, handle, and username registry for Convex with scoped uniqueness, rename redirects, case folding, and typed rejection reasons.

Installation

npm install @vllnt/convex-slugs

About Slug Registry

Slug Registry is a unique-slug and handle reservation system for Convex: `reserve` a string against an opaque `resourceRef`, enforce uniqueness per `(scope, slug)`, `rename` with automatic redirect tracking, and `resolve` or `release` — all atomic inside Convex mutation transactions, no double-reserve. Redirect chains collapse on each `rename` (A→B→C collapses to `redirectFor(A) === C`), case folding and Unicode NFC normalization eliminate homoglyph collisions, and configurable `pattern`, `minLength`, `maxLength`, and `reservedWords` return typed rejection reasons. Slug data lives in sandboxed component tables (zero blast radius into your schema), the host gates every mutation and owns authentication, and it runs identically on Convex Cloud and self-hosted convex-backend.

Benefits

Use cases

how to enforce unique usernames in Convex without race conditions

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.

how to handle username renames with redirect preservation in Convex

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.

how to namespace slugs per tenant or content type in Convex

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.

how to look up which username a user currently holds in Convex

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.

Frequently asked questions

Does @vllnt/convex-slugs prevent two users from claiming the same slug simultaneously?

@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' }.

How does case folding work in @vllnt/convex-slugs?

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.

What is a resourceRef in @vllnt/convex-slugs?

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.

How do rename redirects work and are they safe to chain?

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.

Does @vllnt/convex-slugs provide React hooks for checking slug availability?

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.

Links