Slugs
Unique slug and handle registry with rename redirects, as a Bijection component
git clone https://github.com/bijectionhq/slugs.git
cd slugs
git checkout 724a8414f44e9f661a7d7410471df85ba39eb507Bijection repository access is required. Request access.
Description
Unique slug and handle registry with rename redirects, as a Bijection component
Explore the package README for API concepts, configuration, and examples. Check the Bijection source for adaptation-specific setup.
Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.
Getting started
- Get the source at the revision above and read its package documentation.
- Check the required Bijection SDK version, dependencies, and external service credentials.
- Follow the package setup instructions and the guide to using components.
- Exercise the component in your deployment according to your project’s verification rules.
Open source
Maintained source: bijectionhq/slugs. Based on vllnt/convex-slugs. License: MIT. Consult the repository for license terms, notices, and contributor attribution.
From the upstream publisher
Unique slug and handle registry with rename redirects, as a Convex component
Explore the demo ↗Upstream overview
From the upstream package documentation.
@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 at VLLNT. Find the author on GitHub and X.
Use cases
From the upstream package documentation.
- Social and mobile apps: reserve a unique profile handle.
- SaaS products: give each workspace a readable URL segment.
- Publishing tools: map article or collection slugs to content records.
- Games and communities: reserve team names or public creator handles.
How it works
From the upstream package documentation.
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.
Upstream package FAQ
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.
Use with agents and CLI
Give your coding agent the component’s source identity and setup guidance.
Help me integrate Slugs with Bijection.
Read: https://bijection.com/components/slugs/SKILL.md
Check SDK compatibility and dependency availability.
Follow this project's execution and verification rules.Upstream README.md
View repository ↗Documentation from vllnt/convex-slugs, recorded October 23, 2026. APIs and setup may differ in the Bijection adaptation.
@vllnt/convex-slugs
Unique slug and handle registry with rename redirects, as a Convex component.
const slugs = new Slugs(components.slugs);
await slugs.reserve(ctx, "ada", userId); // { ok: true } | { ok: false, reason }
const ref = await slugs.resolve(ctx, "ada"); // resourceRef | null
Reserve a unique string (slug, @handle, username) against an opaque
resourceRef, enforce uniqueness inside a scope, resolve it back, release it, and
redirect on rename. Domain-neutral: article slugs, profile handles, workspace
slugs — any public-URL key.
Features
- Atomic uniqueness per
(scope, slug)— rides the Convex mutation transaction, no double-reserve. - Scopes — global by default, or namespace per tenant / locale / type.
- Rename + redirects — renaming records an old → new redirect for link preservation.
- Chain-safe redirects — chains collapse (A→B→C ⇒
redirectFor(A) === C), one row per source. - Reverse lookup — find the slug currently held by a
resourceRef. - Case folding + NFC — case-insensitive (opt-out) and Unicode NFC-normalized to kill homoglyph collisions.
- Configurable input rules — length bounds, charset
pattern,reservedWords; typed rejection reasons. - Degrades, never throws — reads use
.first(); release cleans up inbound redirects; opaqueresourceRef.
Installation
pnpm add @vllnt/convex-slugs
Peer dependency: convex@^1.41.0 (optionally react@>=18 for the ./react hooks).
Usage
// convex/convex.config.ts
import { defineApp } from "convex/server";
import slugs from "@vllnt/convex-slugs/convex.config";
const app = defineApp();
app.use(slugs);
export default app;
// convex/handles.ts — host owns auth; pass an opaque resourceRef in.
import { components } from "./_generated/api";
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";
import { Slugs } from "@vllnt/convex-slugs";
const slugs = new Slugs(components.slugs);
export const claim = mutation({
args: { handle: v.string(), userId: v.string() },
handler: (ctx, { handle, userId }) => slugs.reserve(ctx, handle, userId),
});
export const lookup = query({
args: { handle: v.string() },
handler: (ctx, { handle }) => slugs.resolve(ctx, handle),
});
Client options: new Slugs(component, { defaultScope = "global", foldCase = true, minLength = 1, maxLength = 256, pattern?, reservedWords = [] }).
API Reference
| Method | Kind | Result |
|---|---|---|
reserve(ctx, slug, resourceRef, scope?) | mutation | { ok: true } or { ok: false, reason } (SLUG_INVALID | SLUG_RESERVED | SLUG_TAKEN) |
release(ctx, slug, scope?) | mutation | null (idempotent) |
rename(ctx, fromSlug, toSlug, scope?) | mutation | { ok } (SLUG_INVALID | SLUG_NOT_FOUND | SLUG_TAKEN) |
resolve(ctx, slug, scope?) | query | resourceRef | null |
redirectFor(ctx, slug, scope?) | query | toSlug | null |
slugForResource(ctx, resourceRef, scope?) | query | slug | null |
Full reference: docs/API.md.
React
Optional, tree-shakeable hooks from @vllnt/convex-slugs/react — thin wrappers over
useQuery; react is an optional peer dep. Each takes the host's re-exported
resolve query reference.
import { useSlugAvailable } from "@vllnt/convex-slugs/react";
import { api } from "../convex/_generated/api";
// `available` is undefined while loading, true when free, false when held.
const { available, resourceRef } = useSlugAvailable(api.handles.resolveHandle, { slug });
| Hook | Returns |
|---|---|
useSlugAvailable(resolveRef, { slug, scope? }) | { available, resourceRef } — available undefined while loading, true free, false held |
useResolve(resolveRef, { slug, scope? }) | resourceRef | null | undefined (undefined while loading) |
Security
- Auth-agnostic — the host authenticates the caller, decides who may claim a handle, and passes an opaque
resourceRef. - Opaque refs only —
resourceRefandscopeare arbitrary strings; tables are sandboxed (reached only via the client). - Boundary re-guard — the component re-guards length at the trust boundary even if a caller bypasses the client.
See docs/API.md.
Testing
pnpm test # single run
pnpm test:coverage # enforced 100% on covered files
Tests run against the real component runtime via convex-test (@edge-runtime/vm), not mocks.
Contributing
See CONTRIBUTING.md.
Author
Built by bntvllnt · bntvllnt.com · X @bntvllnt
Part of the @vllnt Convex component fleet — vllnt.com
If this is useful, sponsor the work.
License
MIT — see LICENSE.