| Crates.io | obzenflow-idkit |
| lib.rs | obzenflow-idkit |
| version | 0.2.0 |
| created_at | 2026-01-17 16:47:28.376389+00 |
| updated_at | 2026-01-17 16:47:28.376389+00 |
| description | Phantom-typed ULID identifiers for ObzenFlow - type-safe IDs for native and WASM |
| homepage | https://github.com/ObzenFlow/obzenflow-idkit |
| repository | https://github.com/ObzenFlow/obzenflow-idkit |
| max_upload_size | |
| id | 2050763 |
| size | 52,098 |
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.
gen to generate IDs (server + wasm).serde support (serializes as a ULID string).ulid::Ulid for interop.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.
struct User; type UserId = Id<User>;); this crate stays generic.gen (often features = ["serde"] only).gen when they need to generate IDs (server or Leptos).Id::default() unless gen is enabled (apps only).Server (native)
[dependencies]
obzenflow-idkit = { version = "0.2", features = ["gen", "serde"] }
Leptos app (wasm, browser)
[dependencies]
obzenflow-idkit = { version = "0.2", features = ["gen", "serde"] }
# Route RNG to window.crypto.getRandomValues
getrandom = { version = "0.2", features = ["js"] }
Shared crates (e.g., topology)
[dependencies]
obzenflow-idkit = { version = "0.2", features = ["serde"] } # no "gen" here
Node-based wasm tests: set
globalThis.crypto = require('node:crypto').webcrypto.
use obzenflow_idkit::{Id, Ulid};
pub struct User;
pub type UserId = Id<User>;
// 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"]`)
#[cfg(feature = "gen")]
let generated = UserId::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).
Keep cargo test trivial in shared crates by synthesizing IDs:
#[cfg(test)]
mod test_ids {
use std::sync::atomic::{AtomicU128, Ordering};
use obzenflow_idkit::Id;
pub struct TestKind;
static CTR: AtomicU128 = AtomicU128::new(0);
pub fn next_id() -> Id<TestKind> {
Id::from_bytes(CTR.fetch_add(1, Ordering::Relaxed).to_be_bytes())
}
}
Use next_id() in tests, or provide constructors that accept IDs explicitly.
getrandom (apps enable gen).getrandom = { features = ["js"] } in the app; Id::new() uses crypto.getRandomValues.globalThis.crypto as above.Build both targets for shared crates:
cargo build -p your_shared_crate
cargo build -p your_shared_crate --target wasm32-unknown-unknown
Fail if any shared crate calls Id::new/new_ulid/new_ulid_string outside tests:
rg -n "(Id::new|new_ulid|new_ulid_string)\\s*\\(" crates/your_shared_crate -g '!**/*test*' && exit 1
SECURITY.mdCODE_OF_CONDUCT.mdTRADEMARKS.mdDual-licensed under MIT OR Apache-2.0.