# 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 `gen` to generate IDs (server + wasm).
* Optional `serde` support (serializes as a ULID string).
* Re-exports `ulid::Ulid` for 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` (often `features = ["serde"]` only).
* App crates enable `gen` when they need to generate IDs (server or Leptos).
* Don't use `Id::default()` unless `gen` is enabled (apps only).
* Versioning follows SemVer; prior to 1.0, minor releases may include breaking changes.
## Install
**Server (native)**
```toml
[dependencies]
obzenflow-idkit = { version = "0.2", features = ["gen", "serde"] }
```
**Leptos app (wasm, browser)**
```toml
[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)**
```toml
[dependencies]
obzenflow-idkit = { version = "0.2", features = ["serde"] } # no "gen" here
```
> Node-based wasm tests: set `globalThis.crypto = require('node:crypto').webcrypto`.
## Quick start
```rust
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`).
## Testing (no RNG)
Keep `cargo test` trivial in shared crates by synthesizing IDs:
```rust
#[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.
## Targets & RNG backends
* **Native:** OS entropy via `getrandom` (apps enable `gen`).
* **Browser wasm:** add `getrandom = { features = ["js"] }` in the **app**; `Id::new()` uses `crypto.getRandomValues`.
* **Node (wasm tests):** set `globalThis.crypto` as above.
## CI guardrails (recommended)
* Build both targets for shared crates:
```bash
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:
```bash
rg -n "(Id::new|new_ulid|new_ulid_string)\\s*\\(" crates/your_shared_crate -g '!**/*test*' && exit 1
```
## Project policies
* Security: `SECURITY.md`
* Code of Conduct: `CODE_OF_CONDUCT.md`
* Trademarks: `TRADEMARKS.md`
## License
Dual-licensed under MIT OR Apache-2.0.