You have a PostgreSQL database with 15 tables. Your frontend needs a REST API. You have two choices: write 45 CRUD endpoints by hand, or let PostgREST generate them automatically from your schema.
PostgREST is one of the best-kept secrets in backend development. It reads your PostgreSQL schema and exposes every table as a RESTful endpoint -- with filtering, pagination, bulk inserts, and row-level security. No code. No ORM. No boilerplate.
The problem: deploying PostgREST on a VPS traditionally requires Docker Compose files, reverse proxy configuration, SSL certificates, and careful networking between PostgREST and PostgreSQL. It takes an hour if you know what you are doing, and an afternoon if you do not.
sh0 reduces this to three clicks. Here is how.
What You Get
When you enable PostgREST on a PostgreSQL database server in sh0, you get:
- Instant REST API from your existing tables
- Automatic SSL via Let's Encrypt (Caddy reverse proxy)
- Custom domain support (e.g.,
api.yourapp.com) - Row-level security via PostgreSQL roles
- Configurable schemas (expose
public,api, or any custom schema) - Zero maintenance -- PostgREST runs as a sidecar container, managed by sh0
No Docker Compose file. No nginx configuration. No manual certificate renewal.
Step-by-Step: From Zero to REST API
Prerequisites
- A Linux server (Ubuntu, Debian, CentOS, or any distribution with Docker)
- A domain name pointed to your server (for SSL)
Step 1: Install sh0 (2 minutes)
bashcurl -fsSL https://get.sh0.dev | bash
sh0 serveOpen http://your-server:9000 and create your admin account.
Step 2: Create a PostgreSQL database server (2 minutes)
In the sh0 dashboard, navigate to Database Servers and click New Database Server.
- Engine: PostgreSQL
- Name:
my-database - Version: 17 (latest)
Click Create. sh0 pulls the PostgreSQL 17 Docker image, creates the container, sets up the data volume, and starts the server. Your database is running.
Step 3: Create your tables (3 minutes)
Connect to your database using the credentials shown in the dashboard. You can use any PostgreSQL client -- psql, pgAdmin, DBeaver, or the sh0 terminal.
sqlCREATE TABLE products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price INTEGER NOT NULL,
category TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
product_id INTEGER REFERENCES products(id),
quantity INTEGER NOT NULL DEFAULT 1,
customer_email TEXT NOT NULL,
ordered_at TIMESTAMPTZ DEFAULT now()
);
INSERT INTO products (name, price, category) VALUES
('Widget A', 1999, 'hardware'),
('Widget B', 2999, 'hardware'),
('Service Plan', 4999, 'subscription');Step 4: Enable PostgREST (1 minute)
In the dashboard, go to your database server's detail page and click the REST API tab. Toggle Enable PostgREST.
Configure two settings:
- Anonymous role:
web_anon(PostgREST uses this PostgreSQL role for unauthenticated requests) - Schemas:
public(which schemas to expose)
sh0 automatically:
- Creates the
web_anonrole in PostgreSQL withSELECTprivileges - Deploys a PostgREST sidecar container connected to your database
- Configures the reverse proxy for HTTPS access
Step 5: Add a domain (2 minutes)
In the Domains section, add api.yourapp.com. Caddy provisions a Let's Encrypt certificate automatically.
Your API is now live at https://api.yourapp.com.
Using Your API
PostgREST exposes a powerful query language through URL parameters.
List all products
bashcurl https://api.yourapp.com/productsjson[
{"id": 1, "name": "Widget A", "price": 1999, "category": "hardware", "created_at": "2026-04-23T10:00:00Z"},
{"id": 2, "name": "Widget B", "price": 2999, "category": "hardware", "created_at": "2026-04-23T10:00:00Z"},
{"id": 3, "name": "Service Plan", "price": 4999, "category": "subscription", "created_at": "2026-04-23T10:00:00Z"}
]Filter by category
bashcurl "https://api.yourapp.com/products?category=eq.hardware"Select specific columns
bashcurl "https://api.yourapp.com/products?select=name,price"Pagination
bashcurl "https://api.yourapp.com/products?limit=10&offset=20"Insert a row
bashcurl -X POST https://api.yourapp.com/orders \
-H "Content-Type: application/json" \
-d '{"product_id": 1, "quantity": 2, "customer_email": "[email protected]"}'Join tables
bashcurl "https://api.yourapp.com/orders?select=id,quantity,products(name,price)"PostgREST resolves foreign key relationships automatically. No join configuration needed.
Security: Row-Level Security
PostgREST does not mean "expose everything to everyone." PostgreSQL's Row-Level Security (RLS) lets you control access at the row level.
sql-- Enable RLS on orders
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
-- Anonymous users can only see products, not orders
REVOKE ALL ON orders FROM web_anon;
-- Create an authenticated role
CREATE ROLE app_user NOLOGIN;
GRANT SELECT, INSERT ON orders TO app_user;
GRANT USAGE, SELECT ON SEQUENCE orders_id_seq TO app_user;
-- Users can only see their own orders
CREATE POLICY orders_own ON orders
FOR SELECT TO app_user
USING (customer_email = current_setting('request.jwt.claims')::json->>'email');PostgREST reads JWT tokens from the Authorization header and sets PostgreSQL session variables. Your security logic lives in the database, not in application code.
How It Works Under the Hood
sh0's PostgREST implementation is not a template or a Docker Compose file. It is a first-class sidecar managed by the platform.
When you toggle PostgREST on:
- sh0-docker creates a
postgrest/postgrestcontainer on the same Docker network as your PostgreSQL container - sh0-db stores the PostgREST configuration (anonymous role, schemas, JWT secret) in SQLite alongside your database server record
- sh0-proxy registers a Caddy route for the PostgREST container, with automatic TLS
- PostgREST connects to PostgreSQL via the internal Docker network -- no port exposed to the host
When you toggle it off, sh0 removes the container, the Caddy route, and the domain binding. Clean.
The sidecar approach means PostgREST has no access to your server's filesystem, no access to other databases, and no way to reach other containers. It can only talk to the PostgreSQL instance it is paired with.
Alternatives Comparison
| Approach | Time to deploy | SSL | Maintenance | Cost |
|---|---|---|---|---|
| Manual PostgREST + nginx + certbot | 1-2 hours | Manual renewal | You manage everything | VPS cost only |
| Docker Compose + Traefik | 30-60 min | Auto (Traefik) | You write/maintain compose file | VPS cost only |
| Supabase Cloud | 5 min | Included | Zero | $25/month (Pro) |
| sh0 + PostgREST sidecar | 10 min | Auto (Caddy) | Zero | VPS cost only |
sh0 gives you the speed of a managed service with the economics of self-hosting.
Beyond PostgREST: The Full BaaS Stack
PostgREST is one of six backend services built into sh0:
| Service | What it does |
|---|---|
| PostgREST | Auto-generated REST API from PostgreSQL |
| Logto | OIDC-compliant auth with social login, MFA, admin console |
| Centrifugo | WebSocket + SSE with channels, presence, pub/sub |
| Deno Functions | TypeScript/JavaScript serverless functions |
| MinIO | S3-compatible object storage |
| Stalwart | Email hosting with DKIM/SPF/DMARC |
All six are one-click, auto-SSL, and managed by sh0. Together, they replace Supabase, Firebase, or a dozen SaaS subscriptions -- on your own server.
Get Started
bashcurl -fsSL https://get.sh0.dev | bashFrom install to a live REST API with SSL: 10 minutes. No Docker Compose. No reverse proxy configuration. No certificate management.
Your database. Your API. Your server.
sh0 is built by ZeroSuite, Inc. PostgREST is an open-source project by Joe Nelson and contributors. sh0 integrates PostgREST as a managed sidecar -- no fork, no patches, just orchestration.