# Slugs Unique slug and handle registry with rename redirects, as a Bijection component - Package: @bijectionhq/slugs - Category: Database & Backend - Status: Preview - License: MIT - Bijection source (repository access required): https://github.com/bijectionhq/slugs/tree/724a8414f44e9f661a7d7410471df85ba39eb507 - Upstream: https://github.com/vllnt/convex-slugs/tree/2051641ebf8c7a3a0ceebc347ed0af5a5c3047d7 - Documentation: https://docs.bijection.com/components/using - Directory: https://bijection.com/components/slugs ## Get the source Bijection repository access is required. Request access at https://bijection.com/contact. ```sh git clone https://github.com/bijectionhq/slugs.git cd slugs git checkout 724a8414f44e9f661a7d7410471df85ba39eb507 ``` ## Before using this component Read the package README at the recorded revision for setup, dependencies, and API examples. Upstream documentation describes the original package and may differ from the Bijection adaptation. This is a source candidate for evaluation. Catalog inclusion does not establish npm availability or runtime qualification. Check compatibility with your SDK and deployment before adoption. External providers run their own compute. ## Upstream package documentation Recorded 2026-10-23. This describes @vllnt/convex-slugs; check the Bijection adaptation separately. @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). ### Use cases - 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 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 README # @vllnt/convex-slugs Unique slug and handle registry with rename redirects, as a Convex component. ```ts 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 ```bash pnpm add @vllnt/convex-slugs ``` Peer dependency: `convex@^1.41.0` (optionally `react@>=18` for the `./react` hooks). ## Usage ```ts // 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; ``` ```ts // 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](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. ```tsx 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** — `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](docs/API.md). ## Testing ```bash 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](CONTRIBUTING.md). ## Author Built by [bntvllnt](https://github.com/bntvllnt) · [bntvllnt.com](https://bntvllnt.com) · [X @bntvllnt](https://x.com/bntvllnt) Part of the [@vllnt](https://github.com/vllnt) Convex component fleet — [vllnt.com](https://vllnt.com) If this is useful, [sponsor the work](https://github.com/sponsors/bntvllnt). ## License MIT — see [LICENSE](LICENSE).