npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

deterministic-trilabel

v2.1.0

Published

Deterministic, human-readable URL generator for multi-tenant applications. Produces memorable adjective-verb-noun labels from any client/route pair — same input always yields the same output. Zero dependencies.

Readme

Deterministic Trilabel

A deterministic URL generation system that creates human-readable labels using combinations of adjectives, verbs, and nouns. Designed for multi-tenant applications that need consistent, memorable subdomains or URL identifiers — the same input always produces the same output, on any machine, with no shared state.

Features

  • Deterministic — same client + route input always yields the same label
  • Human-readable — produces memorable labels like vivid-gather-tiger
  • 753M+ unique combinations — 910 adjectives × 910 verbs × 910 nouns
  • Bias-free distribution — uses rejection sampling for perfectly uniform word selection
  • Trilabel and bilabel modesadjective-verb-noun or adjective-noun
  • Exception keys — bypass generation for specific clients
  • Composite URL mode — optional client-route format
  • Collision detection — built-in utility to verify uniqueness across your input set
  • TypeScript-first — full type definitions included
  • Zero dependencies

Installation

npm install deterministic-trilabel
# or
yarn add deterministic-trilabel

Usage

Basic Usage (Trilabel)

import { createLabels } from 'deterministic-trilabel';

const generateUrl = createLabels({});

generateUrl('acme-corp', 'app'); // -> 'vivid-gather-tiger'
generateUrl('acme-corp', 'api'); // -> 'tired-direct-lance'
generateUrl('acme-corp', 'app'); // -> 'vivid-gather-tiger' (always the same)

Bilabel Mode

// Set at generator level
const bilabel = createLabels({ generatesTrilabel: false });
bilabel('acme-corp', 'app'); // -> 'vivid-tiger'

// Or override per-call on any generator
const trilabel = createLabels({});
trilabel('acme-corp', 'app', false); // -> 'vivid-tiger' (bilabel override)

Exception Keys

Bypass label generation for a specific client — useful when one tenant should use the route name directly.

const generateUrl = createLabels({ exceptionKey: 'sportbuddy' });

generateUrl('acme-corp', 'app');  // -> 'vivid-gather-tiger'
generateUrl('sportbuddy', 'app'); // -> 'app' (bypassed)
generateUrl('sportbuddy', 'api'); // -> 'api' (bypassed)

Composite URL Mode

Return client-route format instead of word combinations.

const generateUrl = createLabels({
  exceptionKey: 'sportbuddy',
  useCompositeUrls: true,
});

generateUrl('manchester-city', 'app'); // -> 'manchester-city-app'
generateUrl('sportbuddy', 'app');      // -> 'app' (exception still applies)

GUID Mode

Use a UUID/GUID as the input instead of client + route names. The GUID's raw bytes are used directly for word selection — no hashing step needed since GUIDs already contain sufficient entropy.

When useGuids is enabled, only appendNumbers may be used alongside it — all other options must be omitted.

const generateUrl = createLabels({ useGuids: true });

generateUrl('550e8400-e29b-41d4-a716-446655440000'); // -> 'yummy-divide-ticket'
generateUrl('550e8400e29b41d4a716446655440000');      // -> 'yummy-divide-ticket' (same without hyphens)
generateUrl('6ba7b810-9dad-11d1-80b4-00c04fd430c8'); // -> 'hostile-urge-jazz'

Append Numbers

Appends a decimal numeric suffix derived from unused hash/GUID bytes, effectively eliminating collisions. Works in both GUID mode and standard mode.

// GUID mode with numbers — uses remaining 10 GUID bytes (80 bits) as suffix
const guidUrl = createLabels({ useGuids: true, appendNumbers: true });
guidUrl('550e8400-e29b-41d4-a716-446655440000'); // -> 'yummy-divide-ticket-310876571016013493305344'
guidUrl('6ba7b810-9dad-11d1-80b4-00c04fd430c8'); // -> 'hostile-urge-jazz-84144873758547900641480'

// Standard mode with numbers — uses 12 bytes (96 bits) of SHA-256 hash as suffix
const standardUrl = createLabels({ appendNumbers: true });
standardUrl('acme-corp', 'app'); // -> 'vivid-gather-tiger-27163738176725723472341469084'
standardUrl('acme-corp', 'api'); // -> 'tired-direct-lance-69232944429967161165327811215'

The word prefix remains the same whether appendNumbers is enabled or not — the suffix is purely additive.

Use shortSuffix: true alongside appendNumbers for a compact 10-digit suffix (4 bytes / 32 bits) instead of the full-length suffix:

// Short suffix — GUID mode (10 digits max)
const guidShort = createLabels({ useGuids: true, appendNumbers: true, shortSuffix: true });
guidShort('550e8400-e29b-41d4-a716-446655440000'); // -> 'yummy-divide-ticket-1430519808'

// Short suffix — standard mode (10 digits max)
const standardShort = createLabels({ appendNumbers: true, shortSuffix: true });
standardShort('acme-corp', 'app'); // -> 'vivid-gather-tiger-1472549197'

shortSuffix requires appendNumbers to be enabled.

Collision Detection

Verify that your set of inputs produces unique labels.

import { checkCollisions } from 'deterministic-trilabel';

const pairs = [
  { clientName: 'client-a', routeName: 'app' },
  { clientName: 'client-b', routeName: 'app' },
  { clientName: 'client-c', routeName: 'api' },
];

const collisions = checkCollisions(pairs);
// Returns [] if all labels are unique
// Returns [{ label: 'word-word-word', inputs: [...] }] if any collide

API

createLabels(options)

Creates a URL generator function.

Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | exceptionKey | string | — | Client name that bypasses generation (returns route name as-is) | | useCompositeUrls | boolean | false | Use client-route format instead of word combinations | | generatesTrilabel | boolean | true | true for adjective-verb-noun, false for adjective-noun | | useGuids | boolean | false | Use UUID/GUID as input. When true, only appendNumbers may be used alongside it | | appendNumbers | boolean | false | Append a decimal numeric suffix from unused hash/GUID bytes for near-100% collision elimination | | shortSuffix | boolean | false | Use a compact 10-digit suffix (4 bytes) instead of the full-length suffix. Requires appendNumbers |

Returns: (clientName: string, routeName?: string, trilabel?: boolean) => string

  • In standard mode, pass clientName and routeName. The optional third parameter (trilabel) overrides the generator's default mode for that call.
  • In GUID mode (useGuids: true), pass the UUID as the first argument. Accepts with or without hyphens (32 hex characters). The second and third parameters are ignored.

checkCollisions(pairs, options?)

Checks an array of { clientName, routeName } pairs for label collisions.

Returns: CollisionResult[] — array of { label: string, inputs: Array<{ clientName, routeName }> } for any labels that appear more than once. Empty array means no collisions.

calculatePotentialVariations()

Returns system capacity statistics.

interface SystemCapacity {
  nouns: number;           // Total noun count (910)
  verbs: number;           // Total verb count (910)
  adjectives: number;      // Total adjective count (910)
  totalCombinations: number; // 753,571,000
  bitsOfEntropy: number;   // ~29.5
  maxUrlLength: number;    // Longest possible label
}

generateUrl(clientName, routeName)

Legacy standalone function that always generates a trilabel. Prefer createLabels() for new code.

Exported Validation Rules

import { CLIENT_NAME_RULES, ROUTE_NAME_RULES } from 'deterministic-trilabel';

Client names must:

  • Be 3–63 characters (RFC 1035 DNS label limit)
  • Contain only letters, numbers, and hyphens
  • Start and end with a letter or number
  • Not contain consecutive hyphens

Route names must:

  • Be 2–16 characters
  • Contain only letters, numbers, and hyphens

All inputs are lowercased and trimmed before processing.

Capacity & Collision Properties

The system maps arbitrary string inputs to a fixed output space using SHA-256 hashing with rejection sampling for uniform distribution.

Output space

| Mode | Combinations | Entropy | |------|-------------|---------| | Trilabel (adj-verb-noun) | 753,571,000 | ~29.5 bits | | Bilabel (adj-noun) | 828,100 | ~19.7 bits |

Why collisions can occur

The library generates labels from a fixed vocabulary — 910 adjectives, 910 verbs, and 910 nouns — producing 753 million possible trilabel combinations. The valid input space (all possible client + route pairs, or all possible GUIDs) is far larger than 753 million. By the pigeonhole principle, multiple different inputs must eventually map to the same label.

In practice, collisions don't require anywhere near 753 million inputs. The birthday paradox means the probability of any two inputs sharing a label grows quadratically with the number of inputs:

| Input pairs | Collision probability | |------------|---------------------| | 100 | 0.0007% | | 500 | 0.017% | | 1,000 | 0.066% | | 5,000 | 1.6% | | 10,000 | 6.4% | | ~32,000 | 50% (birthday bound) |

These probabilities apply to standard mode, GUID mode, and bilabel mode (without appendNumbers). The bottleneck is the output space (753M combinations), not the input entropy.

Using appendNumbers effectively eliminates collisions. The numeric suffix adds additional entropy from unused hash/GUID bytes. Use shortSuffix for a compact 10-digit number that's still more than sufficient for any practical deployment:

| Mode | Suffix bytes | Max digits | Combined bits | P(collision) at 500K | |------|-------------|-----------|---------------|---------------------| | Words only | — | — | ~29.5 | 1 in 6 | | appendNumbers | 10–12 | 25–29 | ~109.5 | ~1 in 10^22 | | appendNumbers + shortSuffix | 4 | 10 | ~62.5 | ~1 in 20 million |

When to use checkCollisions()

The checkCollisions() utility lets you verify that your specific set of inputs produces unique labels — without shared state or a database. Run it:

  • At onboarding/provisioning — when adding a new tenant or route, check it against your existing set
  • In CI/CD — validate your tenant registry hasn't grown into collision territory
  • As a pre-deployment check — catch collisions before they reach production
import { checkCollisions } from 'deterministic-trilabel';

const allPairs = getAllTenantRoutePairs(); // your data source
const collisions = checkCollisions(allPairs);

if (collisions.length > 0) {
  console.error('Label collisions detected:', collisions);
  // Handle: reassign, use composite mode for conflicts, etc.
}

For most deployments with fewer than 1,000 active label pairs, collision risk is negligible (<0.07%).

When to consider a database solution

This library is stateless by design — it generates labels deterministically but cannot track what has been assigned. If your deployment exceeds the comfort threshold for purely probabilistic uniqueness, consider adding a database layer on your end:

  • > 5,000 active label pairs — collision probability exceeds 1.6%, making collisions a realistic operational concern
  • Uniqueness is a hard requirement — any collision would break your system (e.g., DNS routing, tenant isolation)
  • Labels must survive reassignment — you need to revoke, reserve, or redirect specific labels

A common pattern is to use the library to generate a candidate label, then check-and-insert into a uniqueness-constrained table:

const generateUrl = createLabels({});
const candidate = generateUrl(clientName, routeName);

// INSERT INTO labels (label, client, route)
// VALUES ($candidate, $client, $route)
// ON CONFLICT (label) → handle collision (retry with salt, use composite, alert)

The library handles the deterministic generation; your database handles the uniqueness guarantee.

Scaling the vocabulary

If you need lower collision probabilities without a database, the lever is vocabulary size. The table below shows what the word lists would need to look like:

| Words/letter | Per collection | Trilabel combos | <1% collision threshold | |---|---|---|---| | 35 (current) | 910 | 753M | ~3,900 inputs | | 100 | 2,600 | 17.6B | ~18,700 inputs | | 250 | 6,500 | 274B | ~74,100 inputs | | 500 | 13,000 | 2.2T | ~209,000 inputs | | 891 | 23,174 | 12.4T | ~500,000 inputs |

Word Collections

Each collection contains 910 words organized as 35 words per letter (A–Z):

  • 910 adjectives
  • 910 verbs
  • 910 nouns

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.