Baseplate
A Postgres backend you run yourself: an HTTP API over your tables with per-user row access, auth, file storage, encrypted backups, and a local studio. Open source, MIT.
The problem
One developer with a handful of small apps needs a database, an API, login, files and backups before anything else. A managed platform gives you that in an afternoon, along with a bill and a data owner that are not you. Rolling it yourself takes days.
- Small app
- Needs a backend
- Platform bill, or days of setup
What I built
- 01Postgres and PostgREST behind Caddy, with row-level security on every table before it takes its first row.
- 02Auth, storage and backup services in TypeScript on Docker Compose. Two commands from an empty folder to a running API.
- 03A local studio, a CLI and an MCP server, all calling the same ten use cases.
- 04Terraform that creates a Hetzner server, firewall, DNS record and TLS certificate from the operator's own account tokens. Those tokens never leave the operator's machine.
- 05A typed client that answers { data, error } and never throws, so a failure is a value the app renders.
Features
Row access
Every table gets an owner column, a policy against the caller's token, and a trigger that stamps the owner on insert. Reads are private, shared or public per table. Writes are always the owner's.
Auth and storage
Signup, login, refresh, password reset and email verification, rate limited. Buckets and objects guarded by the same rule as rows, with signed URLs for img tags.
Encrypted backups
Scheduled pg_dump, sealed with AES-256-GCM, shipped to any S3-compatible bucket. A drill restores the newest one into a scratch database and counts what came back.
Studio, CLI and MCP
Tables, rows, row security, storage, backups, users, logs and settings on 127.0.0.1. The same operations from the terminal, and from an agent over stdio.
Bring your own Hetzner
Paste your Cloud and DNS tokens, pick a region and SSH key from your account, and Terraform builds the server. No cloud account on Baseplate's side.
1 of 2
The hard part
The database decides
There is no anon key and no filter in the app. A table nobody declared is locked rather than left open, and every policy is re-applied on every start, so a restore or a restart can never leave a table exposed. The proof is two callers against one endpoint: the second one gets an empty array because Postgres said so, not because the app remembered to filter. CI packs the package, installs it into an empty project, and runs that proof against the installed copy, not the checkout.
Stack
Database
- Postgres
- PostgREST
Services
- TypeScript
- Node.js
- Docker
- Caddy
- SeaweedFS
- Mailpit
Studio
- React
- Vite
- Tailwind
Cloud
- Terraform
- Hetzner
Tests
- Vitest
- GitHub Actions

