How it works
Format Validation
Validates identifiers against a configurable regex pattern. Default: 2–30 lowercase alphanumeric characters and hyphens.
Reserved Names
Blocks system routes, brand names, and other reserved identifiers. Supports categorised lists with custom error messages.
Collision Detection
Checks multiple database tables in parallel. Users, organisations, teams — one call covers them all, with alternative suggestions.
Anti-Spoofing
Detects visually deceptive Unicode characters from 20+ scripts using a 613-pair confusable map generated from Unicode TR39.
Try it out
Type a slug to check its availability against a simulated database of users and organisations.
Simulated database
Users
Organisations
Reserved (system)
Reserved (brand)
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.
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.
// 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.
հ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.