The Architecture Day (aka 0.4h on Hackatime but like 10h in reality)
So uh.
Hi.
New project.
Today was one of those days where I didn’t really code. Like, Hackatime says 0.4 hours. ZERO POINT FOUR. That’s nothing.
But here’s the thing: I spent basically the entire day thinking. Researching. Drawing arrows between boxes in my head. Googling “is candle-core wasm compat” and “best wasm compatible tokio alternative, smol or futures” and going down rabbit holes about OPFS and #[cfg] gates and whether petgraph is pure Rust (it is, thank god).
And I think I came out the other side with something actually good?
the architecture
so Penumbra is a spatial notes app. notes live on a canvas. related notes pull toward each other. unrelated notes drift apart. the whole thing is powered by embeddings and force-directed layout and it should feel like a living map of your thoughts.
and the ENTIRE thing needs to work on web AND desktop without platform #[cfg] spaghetti. that’s the rule. no #[cfg(target_arch = "wasm32")] in any cross-cutting interface. if I have to write #[cfg] I’ve already lost.
here’s what I landed on:
penumbra-core # domain types + traits. zero deps beyond serde/uuid/thiserror
penumbra-events # async-channel event bus
penumbra-graph # petgraph-based graph store
penumbra-layout # ForceAtlas2 + Barnes-Hut
penumbra-embed # Candle embedding provider
penumbra-index # USearch vector index
penumbra-search # hybrid search (vector + text + tags + temporal)
penumbra-storage # OPFS-backed persistence
penumbra-sync # cloud sync abstraction
nine crates. for a notes app. am I overengineering this? maybe. do I care? absolutely not. Saikuro taught me that clean crate boundaries make everything easier later. I’d rather have nine small crates with clear jobs than one megacrate that does everything and hates me. (learned never to entangle your code from HTMLPlayer v2 (yeah that still exists, but i kinda need to press the big Archive this Repo button, I’m making a better new one)
the dependency flow
this is the part I’m actually proud of:
everything depends on core. core depends on nothing interesting. the graph feeds into layout. embed feeds into index feeds into search. storage and sync are siblings. events is the bridge to the UI.
no cycles. no tangled imports. one-directional flow. I drew this like five times on paper before I was happy with it. (I should really learn to use Mermaid for everything but my paper and pen are faster for brainstorming, sue me)
the WASM thing
okay so this was the big research rabbit hole. the whole point of Penumbra is true cross-platform. not “it works on desktop and we have a janky web version.” ACTUAL cross-platform. same code. same behavior. same everything.
the trick is picking libraries that just… work on both targets without needing platform gates:
- serde/serde_json: universal. obviously.
-
uuid with the
jsfeature: usesjs-sysgetRandomValueson wasm32. no#[cfg]. -
chrono with
wasmbind: usesjs-sysDateon wasm32. no#[cfg]. - futures + async-trait: universal. no runtime dependency.
- petgraph: pure Rust. no system deps. just works.
-
opfs: this crate is kind of magic.
tokio::fson native, browser OPFS on wasm. one API. the whole reason I don’t need aStorageProvidertrait. - candle: wasm32 with SIMD backend. Snowflake-Arctic-Embed-XS quantized to GGUF.
- usearch: wasm32 compatible. HNSW index with add/search/remove.
- async-channel: universal. no runtime coupling.
I am unreasonably happy about this list.
Comments 0
No comments yet. Be the first!
Sign in to join the conversation.