Skip to content

Latest commit

 

History

History
158 lines (120 loc) · 4.64 KB

File metadata and controls

158 lines (120 loc) · 4.64 KB

FlowDB Supabase Pattern — Embedded Auth & App Data

« Back to Tutorials


Objective

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.

When to Use This Pattern

  • 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.

Architecture

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

Step-by-Step

1. Define the Schema

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();

2. Sign Up (Atomic User + Session)

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})
}

3. Validate Session

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)
}

4. Row-Level Security (RLS)

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.

5. RLS on Writes

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
}

6. Unique Email Lookup

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()
}

Full Working Example

See examples/supabase_example.rs.

Run it:

cargo run --example supabase_example

Going Further

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