svid (Stateless Verifiable IDs)
64-bit domain-typed, stateless, verifiable IDs for native and WASM.
svid generates i64 IDs that are chronologically sortable, carry a 7-bit entity tag, and need zero coordination between processes.
Bit Layout
63 62 32 31 30 24 23 0
┌──┬──────────────────┬──┬──────────┬────────────────────────────────┐
│S0│ TIMESTAMP │W │ ID TYPE │ RANDOM │
│1b│ 31 bits │1b│ 7 bits │ 24 bits │
└──┴──────────────────┴──┴──────────┴────────────────────────────────┘
- Sign (63): always
0. - Timestamp (32–62): seconds since
2026-01-01 UTC(SVID_EPOCH). - Source (31):
0= server,1= client/WASM. - ID Type (24–30): 7-bit entity tag (0–127).
- Random (0–23): 24 bits of CSPRNG output.
Install
[]
= "0.1"
# optional: features = ["serde", "diesel", "ts"]
Quick Start
use FromStr;
define_id!;
define_id!;
define_id_registry!;
let reg = new;
let u: UserId = reg.user_id.generate_id;
let g: GroupId = reg.group_id.generate_id;
// Tag is checked on parse.
assert_eq!;
assert!;
The SvidTag enum must be in scope at every define_id! site, must be #[repr(u8)], and variant names must match newtype names exactly. Tag values are embedded in persisted IDs — never reuse them.
Encoding
let u: UserId = reg.user_id.generate_id;
let b: String = u.to_base58; // variable-length base58
let h: String = u.to_str; // fixed 11-char base58
let n: i64 = u.to_i64; // raw — no tag check
from_base58?; // tag-checked
from_str_id?; // tag-checked
let _: UserId = from; // unchecked
// Display / FromStr auto-dispatch by length (11 → str_id, else base58).
let _: UserId = u.to_string.parse?;
Domain Enums
Group related IDs into one type:
define_domain_enum!
let f: FolderId = reg.folder_id.generate_id;
let any: FolderEnum = f.into;
let _: FolderEnum = from_i64?;
let _: FolderId = f.try_into?; // recover inner newtype
from_i64 rejects IDs whose tag isn't in the domain.
Bridging to a Wider Enum
define_enum_bridge!;
let any: AnyId = Folder.into;
Inspecting Raw IDs
use SvidExt;
let id: i64 = u.to_i64;
id.tag; // u8
id.unix_timestamp; // i64
id.is_client; // bool
id.random_bits; // u32
let dec = from_i64;
Features
| Feature | Adds |
|---|---|
serde |
string-based Serialize / Deserialize |
diesel |
ToSql / FromSql for BigInt on Postgres |
ts |
#[derive(TS)] for ts-rs TypeScript export |
The macros emit #[cfg(feature = "…")] impls that resolve against your crate's features — mirror them in your Cargo.toml:
[]
= { = "0.1", = ["diesel"] }
= { = "2", = ["postgres"] }
[]
= ["svid/diesel"]
JavaScript / TypeScript
The crate ships JS/TS bindings built with wasm-bindgen. IDs cross the FFI as native bigint (no precision loss).
Build
Usage
import {
generateSvid,
decodeSvid,
encodeHumanReadable,
decodeHumanReadableExpecting,
encodeBase58,
decodeBase58,
extractTag,
svidEpoch,
} from "svid";
const USER_ID_TAG = 1;
const id: bigint = generateSvid(USER_ID_TAG);
const s: string = encodeHumanReadable(id); // fixed 11-char base58
const b: string = encodeBase58(id); // variable-length base58
const back: bigint = decodeHumanReadableExpecting(s, USER_ID_TAG); // throws on tag mismatch
console.assert(back === id);
const d = decodeSvid(id);
// { timestamp: number, isClient: true, idType: 1, random: number, unixTimestamp: bigint }
extractTag(id); // 1
svidEpoch(); // 1767225600n
decodeBase58(b) === id; // true
Notes
- Time uses
js_sys::Date::now(); randomness usesgetrandom'sjsbackend. generateSvidalways setsisClient = true(the WASM build runs in the client). To mint server-source IDs from JS, use the low-levelencodeSvidpacker.IdRegistryand the strongly-typed newtype macros are Rust-only — JS works with rawbigints plus theextract*helpers for tag-based dispatch.
Limitations
- Epoch: 31-bit seconds + 2026 epoch ⇒ wraps in January 2094.
- Tags: 7 bits ⇒ max 128 entity types.
- Collisions: 24-bit random ⇒ 50% birthday-bound at ~5,100 IDs/sec/tag. Plan retries above that.
- Source bit: 1 bit only (server vs client).
- No reserved bits — format changes are breaking.