A Supabase-compatible backend in a single Rust binary.
Drop-in replacement for Supabase's REST, Auth, and Storage APIs β no Docker, no containers, one binary.
| Feature | Details | |
|---|---|---|
| π | Single binary | One suparust executable β no containers, no sidecars |
| π | Embedded PostgreSQL | Auto-managed via pg-embed, data in ./data/postgres |
| π | supabase-js compatible | Drop-in for createClient('http://localhost:3000', ANON_KEY) |
| π | PostgREST REST API | Filter, select, order, limit, offset, upsert via URL params |
| π | Auth | Signup, login, JWT sessions, Argon2 password hashing |
| ποΈ | Storage | Multipart upload/download, bucket management, RLS-gated access |
| π‘οΈ | Row-Level Security | Enforced via SET LOCAL ROLE + JWT claims per request |
| ποΈ | SeaQuery SQL builder | Injection-safe AST-based query construction for the REST layer |
| π | Structured logging | JSON logs with request ID correlation β plug into any log pipeline |
| π | Multi-instance safe | Per-env PID files β production and test can run concurrently |
cargo build --release
# Binary: ./target/release/suparust# Foreground (logs to stdout, Ctrl+C to stop)
suparust start
# Background daemon (logs to app.log)
suparust start --daemonFirst run β do not interrupt. SupaRust downloads the embedded PostgreSQL binary (~50 MB) and initializes the database cluster. This takes 2β5 minutes depending on network speed. You will see:
Setting up embedded PostgreSQL (first run downloads binary ~50MB)...Once ready, the log shows
Listening on 0.0.0.0:3000. Subsequent runs start in seconds with no download.
On first run, SupaRust also auto-generates a JWT secret and writes it to .env:
SUPARUST_JWT_SECRET=...
SUPARUST_ANON_KEY=eyJ...
SUPARUST_SERVICE_KEY=eyJ...
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'http://localhost:3000',
process.env.SUPARUST_ANON_KEY
)suparust start # Start server in foreground
suparust start --daemon # Start as background daemon
suparust stop # Stop server
suparust restart # Stop + restart daemon
suparust status # Show status, endpoints, and API keys
suparust logs # Tail app.log (daemon mode)
suparust logs --lines 100 # Tail last N linesIsolate environments with --profile:
suparust start # .env β identity: local
suparust start --profile test # .env.test only β identity: profile.test
suparust start --env-file dev.env # dev.env only β identity: env.dev_envThe default identity local reflects the universal convention that .env = local development (Supabase, Next.js, Vite, docker-compose all use this). Every instance β including default β gets a deterministic PID file: .suparust.<identity>.<port>.pid.
All subcommands (stop, status, restart) accept --profile too.
β See docs/migrating-from-supabase.md for full profile reference.
Status: RUNNING (PID 12345, uptime 2h 14m 33s)
API URL: http://localhost:3000/rest/v1
Auth URL: http://localhost:3000/auth/v1
Storage URL: http://localhost:3000/storage/v1
Anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Service key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
All config via .env or environment variables:
| Variable | Default | Description |
|---|---|---|
SUPARUST_PORT |
3000 |
HTTP listen port |
SUPARUST_ENV |
local |
Environment name β used in PID filename |
SUPARUST_JWT_SECRET |
auto-generated | HS256 signing secret |
SUPARUST_ANON_KEY |
auto-generated | JWT for anon role |
SUPARUST_SERVICE_KEY |
auto-generated | JWT for service_role |
SUPARUST_DB_DATA_DIR |
./data/postgres |
Embedded PG data directory |
SUPARUST_STORAGE_ROOT |
./data/storage |
File storage root |
SUPARUST_DB_URL |
(unset) | External PG URL (disables embedded PG) |
SUPARUST_LOG_LEVEL |
info |
trace | debug | info | warn | error |
SUPARUST_LOG_FORMAT |
pretty |
pretty (dev) | json (production) |
SUPARUST_PID_FILE |
(derived) | Override PID file path (Docker / systemd) |
SupaRust derives a unique PID filename per instance from the profile identity + port β so multiple environments coexist without collision:
| Command | PID file |
|---|---|
suparust start |
.suparust.local.3000.pid |
suparust start --profile test |
.suparust.profile.test.53001.pid |
suparust start --profile staging |
.suparust.profile.staging.8080.pid |
suparust start --env-file prod.env |
.suparust.env.prod_env.3000.pid |
SUPARUST_PID_FILE overrides the derived path entirely (useful for Docker, systemd socket activation, etc.).
| Method | Endpoint | Description |
|---|---|---|
POST |
/auth/v1/signup |
Register with email + password |
POST |
/auth/v1/token?grant_type=password |
Login, returns JWT session |
GET |
/auth/v1/user |
Get current user (requires Bearer token) |
GET |
/auth/v1/health |
Health check β confirms DB + migrations ready |
Follows PostgREST conventions:
GET /rest/v1/users?select=id,email&role=eq.admin # Filter + select
POST /rest/v1/users # Insert
PATCH /rest/v1/users?id=eq.1 # Update
DELETE /rest/v1/users?id=eq.1 # DeleteSupported operators: eq, neq, lt, lte, gt, gte, like, ilike, is, in, not.in, cs, cd, fts, and(), or()
GET /storage/v1/bucket # List buckets
POST /storage/v1/bucket # Create bucket
POST /storage/v1/object/{bucket}/* # Upload file (multipart)
GET /storage/v1/object/{bucket}/* # Download file
DELETE /storage/v1/object/{bucket} # Delete files (JSON body: {prefixes:[...]})SupaRust emits structured JSON logs with request ID correlation β no embedded log shipper required:
{"timestamp":"...","level":"INFO","target":"suparust::api::rest","req_id":"a1b2c3","method":"GET","path":"/rest/v1/users","message":"..."}Every HTTP request gets a unique req_id that propagates through all log lines for that request β making concurrent request logs trivially correlatable.
# Development β human-readable, file + line numbers
SUPARUST_LOG_LEVEL=debug SUPARUST_LOG_FORMAT=pretty suparust start
# Production β JSON for log aggregators
SUPARUST_LOG_LEVEL=info SUPARUST_LOG_FORMAT=json suparust start --daemon
# Fine-grained override
RUST_LOG=suparust=debug,sqlx=debug suparust startβ See docs/observability.md for integration guides: Vector, Grafana Loki + Promtail, Datadog, and systemd journald.
21 Vitest tests covering Auth, REST API, Storage, and RLS β server starts and stops automatically.
- Rust toolchain (
cargo) - Node.js 18+
Generate isolated test environment (separate port + database):
node scripts/gen-env-test.mjsThis creates:
.env.testβ server config (port 53001, isolated pg-embed atdata/pg-test/)test-client/.env.testβ client config with matching JWT keys.env.supabase.testβ compat server config (port 53002, Supabase alias vars)test-client/.env.supabase.testβ compat client config
cd test-client && npm test # SUPARUST_* style (port 53001)
cd test-client && npm run test:compat # Supabase alias style (port 53002)Server starts automatically, all 21 tests run, server stops on completion. Both suites can run concurrently β each uses a separate PostgreSQL port.
First run: pg-embed downloads its binary (~50MB). This takes 2β5 minutes. Subsequent runs start in seconds.
Tests 21 passed (21)
# Regenerate JWT secret + wipe test database + wipe pg-embed system cache
node scripts/gen-env-test.mjs --regenUse --regen when:
- Auth tests fail unexpectedly (JWT secret drift)
- Test database is in a bad state
- Switching branches with schema changes
- pg-embed fails with
Failed to unpack PostgreSQL binaries(corrupt cache)
| Layer | Library |
|---|---|
| HTTP server | axum 0.8 |
| Async runtime | tokio 1 |
| Database driver | sqlx 0.8 β async, compile-time checked queries |
| SQL builder | sea-query 0.32 + sea-query-binder 0.7 |
| Filter parser | nom 7 β PostgREST filter/select/order syntax |
| Embedded PG | pg-embed 1.0 |
| Auth | jsonwebtoken 9, argon2 0.5 |
| Logging | tracing 0.1, tracing-subscriber 0.3, tower-http 0.6 |
| CLI | clap 4 |
src/
main.rs β CLI dispatch
config.rs β Config::from_env(), env + pid_file derivation
tracing.rs β init_tracing(), TracingWriter enum
cli/
start.rs β foreground + daemon start, HTTP router setup
stop.rs β stop via PID file or port scan fallback
status.rs β status with endpoints + key display
logs.rs β tail app.log
api/
rest.rs β PostgREST-compatible CRUD handlers
auth.rs β signup / login / getUser / health
storage.rs β bucket + object management
parser/
filter.rs β nom parser for col.op.val filter syntax
select.rs β nom parser for select= column list
order.rs β order= parser
sql/
ast.rs β QueryAst, Operation, CountMethod
builder.rs β SeaQuery AST builders
rls.rs β RlsContext β SET LOCAL statements
db/
embed.rs β EmbeddedPostgres via pg-embed
pool.rs β sqlx PgPool creation
execute.rs β execute_query() with RLS context injection
migrations/ β 6 SQL migration files (roles, auth, storage, RLS, grants)
scripts/
gen-env-test.mjs β generate 4 env files (2 pairs: SUPARUST_* + Supabase compat)
start-test-server.sh β manual test server bootstrap helper
docs/
migrating-from-supabase.md β Supabase β SupaRust env var mapping + profile guide
observability.md β Log forwarding guide (Vector, Loki, Datadog, journald)
plans/ β Implementation design docs
test-client/ β Vitest integration test suite (21 tests, 2 modes)
- REST API (PostgREST-compatible)
- Auth (JWT + Argon2)
- Storage (multipart, RLS-gated)
- Row-Level Security
- Structured logging with request ID correlation
- Multi-instance PID isolation (env + port based)
- Integration test suite (21 Vitest tests, auto start/stop)
- Realtime WebSockets (logical replication β
axum::ws) - Edge Functions (
wasmtimeor V8 isolate) - Local Studio UI dashboard
MIT