REST API Overview
pgStack exposes a PostgREST-compatible REST API at /rest/v1/. Any table or view in your PostgreSQL database (that the authenticated role has access to) is queryable via HTTP.
Base URL
http://127.0.0.1:8080/rest/v1/
Authentication
Requests authenticate via an Authorization header:
Authorization: Bearer <token>
Where <token> is:
- Your anon key (
ANON_KEY) for unauthenticated/public requests - A JWT access token (from login) for authenticated user requests
- Your service role key (
SERVICE_ROLE_KEY) for admin requests that bypass RLS
Alternatively, send the key in an apikey: <key> header (Supabase-compatible) — apikey: <anon-key> alone authenticates as anon, apikey: <service-role-key> as service_role. When JWT_SECRET is unset, requests without credentials pass through as anon.
SDK usage
The TypeScript SDK wraps the REST API with a fluent query builder:
import { createClient } from '@pgstack/sdk/pgstack';
const pgstack = createClient('http://127.0.0.1:8080', 'your-anon-key');
// After login, the SDK automatically uses the user's access token
await pgstack.auth.signInWithPassword({ email: '...', password: '...' });
// SELECT
const { data, error } = await pgstack.from('todos')
.select('*')
.eq('done', false)
.order('created_at', { ascending: false })
.limit(20);
// INSERT
const { data, error } = await pgstack.from('todos')
.insert({ title: 'Buy groceries', done: false });
// UPDATE
const { data, error } = await pgstack.from('todos')
.update({ done: true })
.eq('id', 42);
// DELETE
const { data, error } = await pgstack.from('todos')
.delete()
.eq('id', 42);
CRUD operations
SELECT
GET /rest/v1/todos
GET /rest/v1/todos?select=id,title,done
GET /rest/v1/todos?done=eq.false&order=created_at.desc&limit=20
INSERT
POST /rest/v1/todos
Content-Type: application/json
{"title": "Buy groceries", "done": false}
Insert multiple rows by sending an array:
POST /rest/v1/todos
Content-Type: application/json
[
{"title": "Buy groceries"},
{"title": "Walk the dog"},
{"title": "Pay bills"}
]
UPDATE
PATCH /rest/v1/todos?id=eq.42
Content-Type: application/json
{"done": true}
UPSERT
:::caution Not yet implemented
UPSERT is not yet implemented by the proxy. on_conflict and Prefer: resolution=merge-duplicates are ignored — POST always executes a plain INSERT, and a conflicting key returns HTTP 409 {"error": "duplicate value violates unique constraint"}. The SDK upsert() method sends these parameters but they have no effect server-side.
:::
DELETE
DELETE /rest/v1/todos?id=eq.42
Response format
Successful responses return a JSON array for SELECT, or the modified rows for INSERT/UPDATE/DELETE:
[
{"id": 1, "title": "Buy groceries", "done": false, "created_at": "2025-01-01T00:00:00Z"},
{"id": 2, "title": "Walk the dog", "done": true, "created_at": "2025-01-01T01:00:00Z"}
]
Error responses contain a single sanitized error field — raw PostgreSQL messages are never returned, and the status is carried by the HTTP status code, not the body. Example — unknown table returns HTTP 404:
{"error": "relation not found"}
Vertical filtering (select columns)
Return only specific columns:
GET /rest/v1/todos?select=id,title
With embedding (FK joins):
GET /rest/v1/orders?select=id,status,customers(name,email)
TypeScript types
Generate TypeScript types from your database schema with the CLI:
npx pgstack generate types --output src/types/database.ts
Then use them with the query builder:
import type { Database } from './types/database';
type Todo = Database['public']['Tables']['todos']['Row'];
const { data } = await pgstack.from<Todo>('todos').select('*');
// data is Todo[] | null — fully typed
Next steps
- Filters — all filter operators with examples
- Embedding — fetch related rows via foreign keys
- RPC — call PostgreSQL functions
- Pagination — cursor and offset pagination