Try it now (client-side only, nothing is sent anywhere): Capacity Calculator (parameters in, exact capacity and operational lifetime out) and Code Designer (required capacity in, shortest valid configuration out).
baseH converts a long number into a short string of characters that a person can read, type and say out loud, then converts it back to exactly the same number. Under the default expandable tier:
| Identifier | Code |
|---|---|
| 1 | PE84 |
| 12,345 | KYV3 |
| 83,753 | GFPVU |
| 1,000,000 | FMA-R1Z |
| 10,000,000,000 | YGRG4-4UKF |
Codes stay short while the namespace is small and gain one character at a time as it fills, so a ten-billion-id namespace still issues four-character codes on day one. Every code carries a checksum, so a mistyped one is rejected rather than silently resolving to the wrong record.
Communicating identifiers with humans has been a challenge: you either end up with a long string of numbers or a mix of letters and numbers like an airline reservation. You're forced to choose between the benefits of hard to confuse, easy to say over the phone but easily to mis-transcribe and too long against the opposite.
baseH is designed to give you the best of both worlds: short, clear codes that are easily to copy and to say. It's base36 reworked for humans: a reversible, checksummed encoding of non-negative integers into short references that people can read, type and dictate over the phone. The alphabet drops the symbols that cause transcription errors and a checksum catches the rest. It's intended to be used for order numbers, support tickets, returns, bookings and similar records.
This implementation gives you total control over length, capacity, checksums and profanity, with error hardening for audio, visual or both. It was originally developed in support of new AI based customer service systems where a user might start in one channel and follow up in another, e.g. start on chat, follow up by phone, and so needed an easy to use reference number that worked both over the phone and the keyboard.
- Reversible: an internal integer ID converts to a code and back, exactly.
- Expandable by default: codes start at four characters and grow one symbol at a time as the id sequence fills each length. No migrations, no re-issue; shorter codes decode forever. Fixed-width profiles remain for constant-width needs.
- Checksummed: routine transcription typos are detected on decode.
- Human-safe alphabet: no
O/0,I/1/Lconfusion in canonical output. - Aliases: typed
O,I,Lare accepted as0,1,1. - Permutation always on: every frozen tier shuffles sequence with a
published frozen key (per code length in expandable mode), so codes never
read as adjacent. For a private mapping the keyed
-ptiers take your own key. Either way it is presentation only; it is not encryption and not access control. - Correction with abstention: checksum-guided substitution suggestions
that return
AMBIGUOUS_INPUTinstead of guessing.
| Language | Command | Examples |
|---|---|---|
| JavaScript / TypeScript | npm install @cloudyventures/baseh |
js/examples/examples.ts |
| Python | pip install baseh |
python/examples/examples.py |
| Go | go get github.com/cloudyventures/baseh/go/v2 |
go/examples/main.go |
| Rust | cargo add baseh |
rust/examples/examples.rs |
| Ruby | gem install baseh |
ruby/examples/examples.rb |
import { Baseh, basehExpandableV1 } from "@cloudyventures/baseh";
const h = new Baseh(basehExpandableV1());
const code = h.encode(12345n); // 4 characters at this namespace size; grows as ids climb
const { id } = h.decode(code);
console.log(id === 12345n); // trueCodes from the expandable tier are short while the namespace is small and gain a symbol automatically as it fills. If you need a constant width instead, the fixed tiers behave exactly as before:
const h = new Baseh(basehMediumV1());
const code = h.encode(123456789n); // "C8XP-8J49"Every frozen tier hides sequence with the public frozen key (per code length
in expandable mode). For a private mapping, use a keyed -p tier and supply
your own key:
const h = new Baseh(basehExpandablePV1({ keyBytes: myKey, keyId: "prod-01" }));Every implementation shares one behaviour contract: the vectors in
vectors/ are the cross-language conformance suite, and a release
fails if any implementation disagrees with them.
Profiles carry a mode: "expandable" (the default for new profiles) or
"fixed". Every existing option (visual and spoken safety levels, custom
alphabets, profanity modes, blocklists, separators) composes with either
mode unchanged.
The baseh-expandable-v1 tier is the recommended starting point:
- Starts short, grows on its own: minimum four characters (the profile
field
minLength). When the id sequence outgrows a length, codes become one symbol longer, transparently, with no re-issue and no migration. Shorter codes decode forever. - Medium safety plus the zero ban: the default body alphabet is the 27
symbols left after the medium visual strips (
I,L,B,SandO) and the medium spoken strips (T,N,W), with0also removed by the expandable zero ban, so an issued code never emits a visual or spoken confusable. A custom alphabet has0/Oremoved during profile preparation (tooling always displays the derived alphabet). No code can start with a zero glyph, so there is nothing to mis-drop and no left-padding anywhere. - Checksum keeps the zero: the checksum alphabet is the body alphabet
plus
0, and theO -> 0input alias still applies, so a misreadOin a checksum position resolves correctly. - Short checksum on by default: the shortest, most-typed codes carry one
checksum symbol instead of two, one through five characters, two beyond,
so generation 4 holds 19,683 ids instead of 729 (spec section 22;
configurable via
shortChecksumLength/shortChecksumUntil,shortChecksumUntil: 0turns it off). - Permuted per length: the Feistel permutation runs within each code length's range with the length mixed into the key derivation, so codes look random at every size even though issuance is sequential.
- Separator on a threshold: the shipped tier introduces the hyphen at
six characters (the profile field
separatorMinLength); shorter codes print bare. The split is a balanced function of the code length (XXX-XXXat six,XXXX-XXXat seven,XXXX-XXXXat eight), so there is no grouping to configure in expandable mode. - Repetition filter on: like every frozen tier, the expandable tier
ships
maxRepetition: 4, so codes with a run of four or more identical symbols are never issued (configurable to any floor of 3 or more, or 0 to turn it off).
A keyed variant baseh-expandable-p-v1 takes your own key for a private
mapping, like the other -p tiers.
Four frozen tiers ship with the library, all mode: "fixed", all running
the default profanity blocklist, all blocking runs of four or more identical
symbols (the repetition filter, maxRepetition: 4, configurable to any
floor of 3 or more, or 0 to turn it off), all six characters of body, all
hyphen-delimited and all permuting with the published frozen key:
| Tier | Symbols | Checksum | Shape | Capacity | Use for |
|---|---|---|---|---|---|
baseh-minimum-v1 |
36 | none | XXX-XXX |
2,176,782,336 | Typed contexts, maximum capacity |
baseh-light-v1 |
31 | 2 | XXXX-XXXX |
887,503,681 | Typed workflows with light safety |
baseh-medium-v1 |
28 | 2 | XXXX-XXXX |
481,890,304 | General use; the default fixed tier |
baseh-heavy-v1 |
26 | 2 | XXXX-XXXX |
308,915,776 | Spoken-first workflows |
The frozen key is public by design: it hides sequence, not records. Each
tier also has a keyed -p variant (baseh-minimum-p-v1 through
baseh-heavy-p-v1) for a private mapping; those keys are supplied by your
application and never shipped in profiles or exports.
Profile helpers return a fresh profile object on every call, so you can load a default and modify it (longer body, custom separator, blocklist off) without touching the frozen definition:
const profile = basehMediumV1();
profile.bodyLength = 7;With an expandable profile, it fills up gracefully on its own: the code simply gains a symbol and issuance continues. Nothing is re-issued, nothing is migrated, and every shorter code keeps decoding. The guidance below matters only for fixed-mode profiles, where capacity is a one-time design decision.
Plan so it does not, and design so it does not matter if it does.
- Size with headroom. The designer defaults to a maximum utilization of 50% and the calculator shows how many years a configuration lasts at your traffic. One extra body symbol multiplies capacity by the whole alphabet (about 28x at Medium), so an extra character of headroom usually costs less than a migration.
- Old codes keep working when you do outgrow it. Stretching the body
creates a new versioned profile (
orders-v1toorders-v2); codes issued under the old profile still decode against it forever. Keep both profiles registered in your lookup layer and route by length: codes are fixed width, so 8 characters means the old profile and 9 means the new one. The checksum mixes in the profile ID, so a code presented to the wrong profile fails validation loudly instead of silently resolving to the wrong record. - Customers never mark their codes. Length alone distinguishes old from new, and the same id sequence simply continues under the longer profile.
spec/ normative design documents
vectors/ frozen cross-language conformance vectors
js/ TypeScript reference implementation (npm: @cloudyventures/baseh)
python/ Python implementation (PyPI: baseh)
go/ Go implementation (module github.com/cloudyventures/baseh/go/v2)
rust/ Rust implementation (crates.io: baseh)
ruby/ Ruby implementation (RubyGems: baseh)
web/ calculator and designer source
docs/ examples in all five languages, application cookbook
An baseH is a reference alias, never an authorization token. Enforce
authorization after decode, rate-limit public lookups and do not treat the
permutation as secrecy. See spec/README.md and
docs/cookbook.md.
Runnable zero-config, preset and customized examples in all five languages
live in docs/examples.md and the examples/ directory
of each package.
The codec, the five frozen tiers and the vector suite are version 2.
Expandable mode is implemented in all five languages; only the
published-package timing is pending, since v2.0.0 is not yet tagged, so the
baseh-expandable-v1 helpers do not exist in published packages yet. Releases are cut with a git tag (vX.Y.Z); CI verifies all five implementations
against the frozen vectors, then publishes to npm, PyPI, crates.io and
RubyGems and tags go/vX.Y.Z for the Go module. Publishing uses OIDC
trusted publishing with no stored tokens; setup is in
docs/releasing.md.
Apache-2.0 (see LICENSE).