namespace-guard

Check slug uniqueness across multiple tables in one call.

/neym-speys gahrd/ noun A zero-dependency utility that prevents slug collisions across users, organisations, and reserved routes — with built-in Unicode spoofing protection.
Try it out View on GitHub Read the blog post

How it works

Format Validation

Validates identifiers against a configurable regex pattern. Default: 2–30 lowercase alphanumeric characters and hyphens.

"a" → invalid

Reserved Names

Blocks system routes, brand names, and other reserved identifiers. Supports categorised lists with custom error messages.

"admin" → reserved (system)

Collision Detection

Checks multiple database tables in parallel. Users, organisations, teams — one call covers them all, with alternative suggestions.

"sarah" → taken by user

Anti-Spoofing

Detects visually deceptive Unicode characters from 20+ scripts using a 613-pair confusable map generated from Unicode TR39.

"аdmin" → Cyrillic "а" looks like "a"

Try it out

Type a slug to check its availability against a simulated database of users and organisations.

Suggestion strategy:
Security options:
Try these:
Simulated database

Users

sarah bob charlie

Organisations

acme-corp github vercel

Reserved (system)

admin api settings dashboard login signup help support billing

Reserved (brand)

namespace-guard

Use it in your project

import { createNamespaceGuard } from "namespace-guard";
import { createPrismaAdapter } from "namespace-guard/adapters/prisma";
import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const guard = createNamespaceGuard({
  reserved: ["admin", "api", "settings"],
  sources: [
    { name: "user", column: "handle" },
    { name: "organization", column: "slug" },
  ],
  suggest: { strategy: ["sequential", "random-digits"], max: 3 },
}, createPrismaAdapter(prisma));

const result = await guard.check("acme-corp");

if (result.available) {
  // Safe to create
} else {
  console.log(result.message);      // "That name is already in use."
  console.log(result.suggestions); // ["acme-corp-1", "acme-corp-4821", "acme-corp1"]
}
import { createNamespaceGuard } from "namespace-guard";
import { createDrizzleAdapter } from "namespace-guard/adapters/drizzle";
import { eq } from "drizzle-orm";
import { db } from "./db";
import { users, organizations } from "./schema";

const adapter = createDrizzleAdapter(db, { users, organizations }, eq);

const guard = createNamespaceGuard({
  reserved: ["admin", "api", "settings"],
  sources: [
    { name: "users", column: "handle" },
    { name: "organizations", column: "slug" },
  ],
  suggest: { strategy: ["sequential", "random-digits"], max: 3 },
}, adapter);

const result = await guard.check("acme-corp");

if (result.available) {
  // Safe to create
} else {
  console.log(result.message);      // "That name is already in use."
  console.log(result.suggestions); // ["acme-corp-1", "acme-corp-4821", "acme-corp1"]
}
import { createNamespaceGuard } from "namespace-guard";
import { createKyselyAdapter } from "namespace-guard/adapters/kysely";
import { Kysely, PostgresDialect } from "kysely";

const db = new Kysely({ dialect: new PostgresDialect({ pool }) });

const guard = createNamespaceGuard({
  reserved: ["admin", "api", "settings"],
  sources: [
    { name: "users", column: "handle" },
    { name: "organizations", column: "slug" },
  ],
  suggest: { strategy: ["sequential", "random-digits"], max: 3 },
}, createKyselyAdapter(db));

const result = await guard.check("acme-corp");

if (result.available) {
  // Safe to create
} else {
  console.log(result.message);      // "That name is already in use."
  console.log(result.suggestions); // ["acme-corp-1", "acme-corp-4821", "acme-corp1"]
}
import { createNamespaceGuard } from "namespace-guard";
import { createKnexAdapter } from "namespace-guard/adapters/knex";
import Knex from "knex";

const knex = Knex({ client: "pg", connection: process.env.DATABASE_URL });

const guard = createNamespaceGuard({
  reserved: ["admin", "api", "settings"],
  sources: [
    { name: "users", column: "handle" },
    { name: "organizations", column: "slug" },
  ],
  suggest: { strategy: ["sequential", "random-digits"], max: 3 },
}, createKnexAdapter(knex));

const result = await guard.check("acme-corp");

if (result.available) {
  // Safe to create
} else {
  console.log(result.message);      // "That name is already in use."
  console.log(result.suggestions); // ["acme-corp-1", "acme-corp-4821", "acme-corp1"]
}
import { createNamespaceGuard } from "namespace-guard";
import { createTypeORMAdapter } from "namespace-guard/adapters/typeorm";
import { DataSource } from "typeorm";
import { User, Organization } from "./entities";

const dataSource = new DataSource({ /* ... */ });
const adapter = createTypeORMAdapter(dataSource, { user: User, organization: Organization });

const guard = createNamespaceGuard({
  reserved: ["admin", "api", "settings"],
  sources: [
    { name: "user", column: "handle" },
    { name: "organization", column: "slug" },
  ],
  suggest: { strategy: ["sequential", "random-digits"], max: 3 },
}, adapter);

const result = await guard.check("acme-corp");
import { createNamespaceGuard } from "namespace-guard";
import { createMikroORMAdapter } from "namespace-guard/adapters/mikro-orm";
import { MikroORM } from "@mikro-orm/core";
import { User, Organization } from "./entities";

const orm = await MikroORM.init(config);
const adapter = createMikroORMAdapter(orm.em, { user: User, organization: Organization });

const guard = createNamespaceGuard({
  reserved: ["admin", "api", "settings"],
  sources: [
    { name: "user", column: "handle" },
    { name: "organization", column: "slug" },
  ],
  suggest: { strategy: ["sequential", "random-digits"], max: 3 },
}, adapter);

const result = await guard.check("acme-corp");
import { createNamespaceGuard } from "namespace-guard";
import { createSequelizeAdapter } from "namespace-guard/adapters/sequelize";
import { User, Organization } from "./models";

const adapter = createSequelizeAdapter({ user: User, organization: Organization });

const guard = createNamespaceGuard({
  reserved: ["admin", "api", "settings"],
  sources: [
    { name: "user", column: "handle" },
    { name: "organization", column: "slug" },
  ],
  suggest: { strategy: ["sequential", "random-digits"], max: 3 },
}, adapter);

const result = await guard.check("acme-corp");
import { createNamespaceGuard } from "namespace-guard";
import { createMongooseAdapter } from "namespace-guard/adapters/mongoose";
import { User, Organization } from "./models";

const adapter = createMongooseAdapter({ user: User, organization: Organization });

const guard = createNamespaceGuard({
  reserved: ["admin", "api", "settings"],
  sources: [
    { name: "user", column: "handle", idColumn: "_id" },
    { name: "organization", column: "slug", idColumn: "_id" },
  ],
  suggest: { strategy: ["sequential", "random-digits"], max: 3 },
}, adapter);

const result = await guard.check("acme-corp");
import { createNamespaceGuard } from "namespace-guard";
import { createRawAdapter } from "namespace-guard/adapters/raw";
import { Pool } from "pg";

const pool = new Pool();
const adapter = createRawAdapter((sql, params) => pool.query(sql, params));

const guard = createNamespaceGuard({
  reserved: ["admin", "api", "settings"],
  sources: [
    { name: "users", column: "handle" },
    { name: "organizations", column: "slug" },
  ],
  suggest: { strategy: ["sequential", "random-digits"], max: 3 },
}, adapter);

const result = await guard.check("acme-corp");

if (result.available) {
  // Safe to create
} else {
  console.log(result.message);      // "That name is already in use."
  console.log(result.suggestions); // ["acme-corp-1", "acme-corp-4821", "acme-corp1"]
}

Anti-spoofing pipeline

Three stages, each aware of the others. No dead code, no wrong mappings.

Stage 1 NFKC normalize Collapses compatibility forms
Stage 2 Confusable map 613 TR39 character pairs
Stage 3 Mixed-script reject 19+ script families

NFKC-aware confusable map

Most libraries blindly ship Unicode TR39 mappings. We found 31 entries where TR39 and NFKC disagree — dead code that encodes the wrong mapping in any pipeline that normalizes first.

// TR39 says Long S (‹ſ›) → “f”
// NFKC says Long S → “s” ← correct
// TR39 says Math Bold I (‹𝐈›) → “l”
// NFKC says Math Bold I → “i” ← correct

Full Unicode script coverage

The confusable map covers 20+ scripts from the Unicode Consortium's authoritative source. Mixed-script detection goes further — blocking Latin mixed with Hebrew, Arabic, Devanagari, Thai, Georgian, Ethiopic, and more.

аdmin Cyrillic “a” + Latin
հello Armenian “h” + Latin
ɑdmin IPA “a” + Latin
Ꭺdmin Cherokee “A” + Latin

Reproducible from source

The map is generated from Unicode's official confusables.txt, not hand-curated. Run npx tsx scripts/generate-confusables.ts to regenerate for new Unicode versions. The script automatically excludes NFKC conflicts.

Defense in depth

The default slug pattern /^[a-z0-9][a-z0-9-]*$/ already blocks non-ASCII. The confusable map and mixed-script detection protect apps with permissive patterns that allow Unicode identifiers — or serve as a second line of defense if a format regex is misconfigured.