Model a Supabase-like backend — user auth, session management, Row-Level Security (RLS), and application data — entirely in-process using FlowDB + JsonDB. No PostgreSQL required.
- Edge Functions / Serverless — embed FlowDB directly in your Wasm or VM function for low-latency local state.
- Local Dev Tooling — replace a remote Supabase instance with a local embedded database during development.
- Offline-First Apps — use the same pattern on-device, sync to remote Supabase when online.
- Testing — spin up a fully isolated backend in tests without Docker or network.
User Store ──── Session Store ──── Todo Store
│ │
└── unique index on email ├── index on (user_id, status)
(get_user_by_email) ├── index on (user_id, priority)
└── RLS: queries scoped by user_id
use flowdb::jsondb::{JsonDB, StoreSchema};
use flowdb::Config;
let db = JsonDB::open(Config { data_dir: "./supa".into(), ..Default::default() }).unwrap();
// Auth + App data — all schema in one place
db.apply_schemas(&[
StoreSchema::new("users", "id")
.with_index("by_email", &["email"], true),
StoreSchema::new("sessions", "token")
.with_index("by_user", &["user_id"], false),
StoreSchema::new("todos", "id")
.with_index("by_user_status", &["user_id", "status"], false)
.with_index("by_user_priority", &["user_id", "priority"], false),
]).unwrap();Use an explicit transaction to atomically create a user and a session:
use flowdb::jsondb::TransactionMode;
use serde_json::json;
fn sign_up(db: &JsonDB, email: &str, password: &str) -> serde_json::Value {
let user_id = uuid_v4();
let token = uuid_v4();
let mut tx = db.transaction(&["users", "sessions"], TransactionMode::ReadWrite).unwrap();
tx.put("users", json!({
"id": user_id,
"email": email,
"password": password, // real app: hash this!
"created_at": now_iso(),
})).unwrap();
tx.put("sessions", json!({
"token": token,
"user_id": user_id,
"expires_at": "2026-12-31T23:59:59Z",
})).unwrap();
tx.commit().unwrap();
json!({"user_id": user_id, "token": token})
}fn validate_session(db: &JsonDB, token: &str) -> Option<String> {
let session = db.get("sessions", &json!(token)).unwrap()?;
session.get("user_id").and_then(|v| v.as_str()).map(String::from)
}Every query filters by user_id — this is your RLS:
fn my_open_todos(db: &JsonDB, user_id: &str) -> Vec<serde_json::Value> {
db.query("todos")
.where_eq("user_id", json!(user_id))
.where_eq("status", json!("open"))
.order_by("priority", flowdb::jsondb::SortDir::Desc)
.collect()
.unwrap()
}The compound index by_user_status makes this query efficient — it seeks directly to (user_id, "open") within the index.
Check ownership before mutating:
fn complete_todo(db: &JsonDB, user_id: &str, todo_id: &str) -> bool {
let doc = match db.get("todos", &json!(todo_id)).unwrap() {
Some(d) => d,
None => return false,
};
if doc["user_id"] != user_id {
return false; // RLS: not owner
}
let mut doc = doc;
doc["status"] = json!("done");
db.put("todos", doc).unwrap();
true
}fn get_user_by_email(db: &JsonDB, email: &str) -> Option<serde_json::Value> {
db.get_by_index("users", "by_email", &json!(email))
.unwrap()
.into_iter()
.next()
}See examples/supabase_example.rs.
Run it:
cargo run --example supabase_example| Concept | How to Implement |
|---|---|
| Password hashing | Hash with argon2 or bcrypt before storing |
| Session expiry | Check expires_at on each validate_session call |
| Pagination | Use QueryBuilder::limit(n).offset(m) |
| Soft delete | Add "deleted": true field + index filter |
| Audit log | Append-only logs store with auto-increment (put_auto) |
| Multi-tenant | Prefix all queries with tenant_id + compound index |