ObzenFlow IDKit
ObzenFlow IDKit provides phantom-typed ULIDs for ObzenFlow (native server + Leptos wasm). Extracted from the main ObzenFlow project to provide type-safe IDs across the ecosystem.
- Type-only by default (no RNG dependencies).
- App crates enable
gento generate IDs (server + wasm). - Optional
serdesupport (serializes as a ULID string). - Re-exports
ulid::Ulidfor interop.
Why this exists
In a full-stack Rust app (native server + Leptos in the browser), you want one ID type that round-trips cleanly between back end, shared crates, and the frontend.
ULIDs are a good fit: they're 128-bit IDs (timestamp + randomness) that serialize to a compact, human-friendly string (useful for URLs and JSON). The server can generate them from OS entropy, but browser wasm can't access an OS RNG directly; it needs to go through Web APIs like window.crypto.getRandomValues (and Node-based wasm tests need globalThis.crypto).
This crate keeps that complexity out of shared code: it's type-only by default, while app crates opt into generation via gen and configure the RNG backend per target. See "Targets & RNG backends" below for specifics.
ObzenFlow pattern
- Define marker types in domain crates (
struct User; type UserId = Id<User>;); this crate stays generic. - Shared crates should not enable
gen(oftenfeatures = ["serde"]only). - App crates enable
genwhen they need to generate IDs (server or Leptos). - Don't use
Id::default()unlessgenis enabled (apps only). - Versioning follows SemVer; prior to 1.0, minor releases may include breaking changes.
Install
Server (native)
[]
= { = "0.2", = ["gen", "serde"] }
Leptos app (wasm, browser)
[]
= { = "0.2", = ["gen", "serde"] }
# Route RNG to window.crypto.getRandomValues
= { = "0.2", = ["js"] }
Shared crates (e.g., topology)
[]
= { = "0.2", = ["serde"] } # no "gen" here
Node-based wasm tests: set
globalThis.crypto = require('node:crypto').webcrypto.
Quick start
use ;
;
pub type UserId = ;
// Parse / round-trip (shared crates can do this without `gen`)
let from_wire: UserId = "01ARZ3NDEKTSV4RRFFQ69G5FAV".parse.unwrap;
let ulid: Ulid = from_wire.as_ulid;
// Generate (apps with `features = ["gen"]`)
let generated = new;
Common helpers: from_ulid, as_ulid, to_bytes/from_bytes, timestamp_ms().
If you don't need phantom typing, you can generate raw ULIDs with new_ulid() / new_ulid_string() (requires gen).
Testing (no RNG)
Keep cargo test trivial in shared crates by synthesizing IDs:
Use next_id() in tests, or provide constructors that accept IDs explicitly.
Targets & RNG backends
- Native: OS entropy via
getrandom(apps enablegen). - Browser wasm: add
getrandom = { features = ["js"] }in the app;Id::new()usescrypto.getRandomValues. - Node (wasm tests): set
globalThis.cryptoas above.
CI guardrails (recommended)
-
Build both targets for shared crates:
-
Fail if any shared crate calls
Id::new/new_ulid/new_ulid_stringoutside tests:&&
Project policies
- Security:
SECURITY.md - Code of Conduct:
CODE_OF_CONDUCT.md - Trademarks:
TRADEMARKS.md
License
Dual-licensed under MIT OR Apache-2.0.