Skip to content

Find a component

Loading the directory…

↑ ↓ to select · Enter to open · Esc to closeBrowse all →
Contributor login

Slugs

Unique slug and handle registry with rename redirects, as a Bijection component

Bijection source
git clone https://github.com/bijectionhq/slugs.git
cd slugs
git checkout 724a8414f44e9f661a7d7410471df85ba39eb507

Bijection 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.

Available to evaluate

Review the source and its dependencies before adopting this package. Source availability does not establish npm publication or runtime qualification.

Getting started

  1. Get the source at the revision above and read its package documentation.
  2. Check the required Bijection SDK version, dependencies, and external service credentials.
  3. Follow the package setup instructions and the guide to using components.
  4. 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.

agent-prompt.md
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; opaque resourceRef.

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

MethodKindResult
reserve(ctx, slug, resourceRef, scope?)mutation{ ok: true } or { ok: false, reason } (SLUG_INVALID | SLUG_RESERVED | SLUG_TAKEN)
release(ctx, slug, scope?)mutationnull (idempotent)
rename(ctx, fromSlug, toSlug, scope?)mutation{ ok } (SLUG_INVALID | SLUG_NOT_FOUND | SLUG_TAKEN)
resolve(ctx, slug, scope?)queryresourceRef | null
redirectFor(ctx, slug, scope?)querytoSlug | null
slugForResource(ctx, resourceRef, scope?)queryslug | 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 });
HookReturns
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 — resourceRef and scope are 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.

Keywords