Architecture
SwirlDB is a composition framework where you assemble your database from independent, swappable adapters.
Core Philosophy: Everything is an Adapter
No Monolith, Just Composition
Traditional databases give you a single implementation with feature flags and configuration. SwirlDB gives you traits and adapters:
- Storage: Choose your backend (memory, localStorage, IndexedDB, redb, S3, custom)
- Sync: Choose your protocol (WebSocket, HTTP, WebRTC, custom)
- Auth: Choose your policy engine (JWT, OAuth, ABAC, custom)
- Encryption: Choose your crypto (AES-GCM, field-level, custom)
Key insight: There are no "default" implementations or "primary" platforms. Every component is a peer. Every environment is equal.
The 3-Crate System
SwirlDB uses complete separation of concerns:
๐ฆ swirldb-core
Platform-agnostic Rust library
Contains:
- CRDT engine (Automerge-based)
- Trait definitions (DocumentStorage, ChangeLog)
- Policy engine (path-based authorization)
- Auth provider interface
- In-memory reference implementations
Does NOT contain:
- โ WASM bindings
- โ Browser APIs
- โ Server networking
- โ Platform-specific code
Pure Rust. Platform-agnostic. Thread-safe.
๐ swirldb-browser
WASM bindings for browsers
Contains:
- JavaScript bindings via wasm-bindgen
- localStorage adapter (5-10MB)
- IndexedDB adapter (50MB-1GB)
- JavaScript interop
- Web API implementations
Build output:
- ~479KB WASM binary (gzipped)
- ~10KB JavaScript glue (gzipped)
- TypeScript definitions
Target: wasm32-unknown-unknown. Single-threaded (?Send).
๐ฅ๏ธ swirldb-server
Pure Rust binary for servers
Contains:
- HTTP/WebSocket server (axum + tokio)
- redb adapter (persistent, ACID)
- Memory adapter (volatile, fast)
- Binary sync protocol
- Namespace management
Does NOT contain:
- โ Node.js dependencies
- โ npm packages
- โ JavaScript runtime
Pure native binary. Multi-threaded (Send + Sync).
Browser and Server are Equivalent Nodes
This is a fundamental design principle: browser and server are peers, not client and server.
Same Traits, Different Implementations
Both environments implement identical storage traits:
// Defined in swirldb-core/src/storage/mod.rs
trait DocumentStorage {
async fn save(&self, key: &str, data: &[u8]) -> Result<()>;
async fn load(&self, key: &str) -> Result<Option<Vec<u8>>>;
async fn delete(&self, key: &str) -> Result<()>;
async fn list_keys(&self) -> Result<Vec<String>>;
}
trait ChangeLog {
async fn init(&mut self) -> Result<()>;
async fn append_change(&self, namespace_id: &str, change: Change) -> Result<()>;
async fn append_changes(&self, namespace_id: &str, changes: Vec<Change>) -> Result<()>;
async fn get_changes(&self, namespace_id: &str) -> Result<Vec<Change>>;
async fn get_changes_since(&self, namespace_id: &str, since: i64) -> Result<Vec<Change>>;
async fn change_count(&self, namespace_id: &str) -> Result<usize>;
async fn delete_namespace(&self, namespace_id: &str) -> Result<()>;
} Browser Implementations
// swirldb-browser/src/storage.rs
// localStorage adapter
pub struct LocalStorageAdapter { /* ... */ }
#[async_trait(?Send)] // Single-threaded WASM
impl DocumentStorage for LocalStorageAdapter {
async fn save(&self, key: &str, data: &[u8]) -> Result<()> {
// Uses web_sys::Storage API
}
// ...
}
// IndexedDB adapter
pub struct IndexedDBAdapter { /* ... */ }
#[async_trait(?Send)]
impl DocumentStorage for IndexedDBAdapter {
async fn save(&self, key: &str, data: &[u8]) -> Result<()> {
// Uses web_sys::IdbDatabase API
}
// ...
} Server Implementations
// swirldb-server/src/storage/redb_adapter.rs
// redb adapter
pub struct RedbAdapter { /* ... */ }
#[async_trait] // Multi-threaded, Send + Sync
impl DocumentStorage for RedbAdapter {
async fn save(&self, key: &str, data: &[u8]) -> Result<()> {
// Uses redb embedded database
}
// ...
}
#[async_trait]
impl ChangeLog for RedbAdapter {
async fn append_change(&self, namespace_id: &str, change: Change) -> Result<()> {
// Persistent change log
}
// ...
} Key insight: The only difference is the #[async_trait(?Send)] marker. Browser is single-threaded, server is multi-threaded. Otherwise, identical APIs.
Shared Implementations
The pure Rust core is shared by both browser and server. There is no duplication, no divergence, no platform-specific CRDT logic.
What's Shared
- CRDT Engine: Automerge-based document merging (swirldb-core/src/core.rs)
- Storage Traits: DocumentStorage + ChangeLog definitions (swirldb-core/src/storage/mod.rs)
- Policy Engine: Path-based authorization logic (swirldb-core/src/policy.rs)
- Auth Providers: JWT, OAuth, ABAC interfaces (swirldb-core/src/auth.rs)
- In-Memory Storage: Reference implementations used for testing
What's Different
- Browser: Web APIs (localStorage, IndexedDB), WASM FFI, JavaScript interop
- Server: Native I/O (redb, filesystem), HTTP/WebSocket, multi-threading
This separation means:
- โ Bug fixes in core benefit both platforms simultaneously
- โ CRDT behavior is identical across all environments
- โ New features land everywhere at once
- โ Testing core logic once validates all platforms
Runtime Composition
Adapters are swapped at runtime, not compile time. No recompilation needed to change behavior.
Example: Storage Adapter Switching
// Browser: Start with memory, switch to localStorage later
let db = await SwirlDB.create(); // In-memory
// Later, migrate to persistent storage
const state = db.saveState();
db = await SwirlDB.withLocalStorage('my-app');
db.loadState(state);
// Or switch to IndexedDB for larger storage
db = await SwirlDB.withIndexedDB('my-app');
db.loadState(state); Example: Server Adapter Composition
// Server: Start with memory for development
let storage = Arc::new(MemoryAdapter::new());
let db = SwirlDB::with_storage(storage, "db").await;
// Deploy with redb for production
let storage = Arc::new(RedbAdapter::new("./data")?);
let db = SwirlDB::with_storage(storage, "db").await;
// Same code. Same APIs. Just different adapter. Access Control with Policy Engine
Control who can read and write specific paths using the policy engine. Policies are defined as JSON documents.
Policy-Based Access Control
// Define access rules in JSON
const policy = {
rules: [
{
path: "/user/:userId/**",
read: ["owner", "admin"],
write: ["owner"]
},
{
path: "/public/**",
read: ["*"],
write: ["authenticated"]
}
]
};
// Apply policy to database instance
db.withPolicy(policy); Policies enable fine-grained control: different paths, different permissions.
Building Custom Adapters
The trait system is fully extensible. Roll your own adapters for any backend.
Example: Custom Redis Adapter
use redis::{Client, AsyncCommands};
pub struct RedisAdapter {
client: Client,
}
impl RedisAdapter {
pub fn new(url: &str) -> Result<Self> {
Ok(RedisAdapter {
client: Client::open(url)?,
})
}
}
impl DocumentStorageMarker for RedisAdapter {}
#[async_trait]
impl DocumentStorage for RedisAdapter {
async fn save(&self, key: &str, data: &[u8]) -> Result<()> {
let mut conn = self.client.get_async_connection().await?;
conn.set(key, data).await?;
Ok(())
}
async fn load(&self, key: &str) -> Result<Option<Vec<u8>>> {
let mut conn = self.client.get_async_connection().await?;
let result: Option<Vec<u8>> = conn.get(key).await?;
Ok(result)
}
async fn delete(&self, key: &str) -> Result<()> {
let mut conn = self.client.get_async_connection().await?;
conn.del(key).await?;
Ok(())
}
async fn list_keys(&self) -> Result<Vec<String>> {
let mut conn = self.client.get_async_connection().await?;
let keys: Vec<String> = conn.keys("*").await?;
Ok(keys)
}
}
// Now use it just like any other adapter!
let storage = Arc::new(RedisAdapter::new("redis://localhost")?);
let db = SwirlDB::with_storage(storage, "db").await; That's it. Implement the trait, pass it to SwirlDB::with_storage(), and you're done.
Why This Architecture?
๐ True Modularity
No monolithic codebase. Each adapter is independent, testable, replaceable.
โ๏ธ No Primary Platform
Browser and server are peers. No "privileged" environment. Equal functionality.
๐ Shared Logic
CRDT engine is written once, used everywhere. Bug fixes land in all platforms.
๐ Runtime Flexibility
Swap adapters without recompilation. Deploy same code, different configuration.
๐งช Testability
Test core traits with in-memory adapters. Integration tests use real adapters.
๐ Ecosystem
Community can build adapters for any backend. Unlimited extensibility.