The REST API
Auto-generated CRUD endpoints with filtering, sorting, expansion, and paging.
Updated
Every collection you define gets a complete REST API automatically — no controllers to write. The endpoints are protected by the access rules on the collection.
Endpoints
GET /api/collections/{name}/records list
POST /api/collections/{name}/records create
POST /api/collections/{name}/records/batch batch write
GET /api/collections/{name}/records/{id} view
PATCH /api/collections/{name}/records/{id} update
DELETE /api/collections/{name}/records/{id} delete
POST /api/collections/{name}/records/{id}/restore restore (soft-delete)
Querying a list
The list endpoint takes query parameters:
| Param | Example | Purpose |
|---|---|---|
page / perPage |
?page=2&perPage=50 |
Offset pagination |
filter |
?filter=status='published' && views>100 |
Expression filter |
sort |
?sort=-created,title |
Multi-key sort (- = descending) |
expand |
?expand=author |
Inline related records |
q |
?q=railbase |
Full-text search over FTS() fields |
cursor |
?cursor=… |
Cursor pagination (nextCursor in the response) |
count |
?count=exact |
Fill totalItems/totalPages: exact · estimate · cap[:N] |
Filters on indexed fields (
.Index()/.Unique()in the schema, plus the built-inid,created,updatedandtenant_id) are served from the field's secondary index — only the matching candidates are read and decoded instead of the whole collection. Equality conditions benefit most;>/>=/<ranges are accelerated too. Results are identical either way — the index only narrows what gets scanned.
curl "http://localhost:8095/api/collections/posts/records?filter=status='published'&sort=-created&expand=author&count=exact"
The list response is enveloped:
{
"page": 1,
"perPage": 30,
"totalItems": 142,
"totalPages": 5,
"items": [ { "id": "…", "title": "…", "expand": { "author": { } } } ]
}
Note
Counting is opt-in: without count=…, totalItems and totalPages are
null — a deliberate default, since an exact count is a full scan on large
collections.
Batch writes
POST /api/collections/{name}/records/batch takes up to 200 operations and
returns a per-op result list; atomic: true makes it all-or-nothing:
{
"atomic": true,
"ops": [
{ "action": "create", "data": { "name": "Tech" } },
{ "action": "update", "id": "abc123", "data": { "name": "Life" } },
{ "action": "delete", "id": "def456" }
]
}
Files
File fields (File() / Files() in the schema) are populated after
the record exists, not inline in the create JSON:
POST /api/collections/{name}/records/{id}/files/{field} multipart, part name "file"
DELETE /api/collections/{name}/records/{id}/files/{field}/{filename}
GET /api/files/{collection}/{record_id}/{field}/{filename}?token=…&expires=…
The upload response (and the record's field value) carries a signed download
URL — the token is the auth, so the GET needs no Authorization header and
expires after a few minutes; re-read the record for a fresh one.
Authentication
Send a bearer token from one of the auth endpoints:
curl http://localhost:8095/api/collections/posts/records \
-H "Authorization: Bearer <token>"
The current identity is available in rules as @request.auth.* and via
GET /api/auth/me.
URL compatibility
Railbase is wire-compatible with PocketBase-style clients: everything is served
under /api/… in PB-compatible shapes, and that's the default v1 target
(mode strict). A compat.mode knob exists for compatibility experiments —
set it at runtime (railbase config set compat.mode '"both"') or via
RAILBASE_COMPAT_MODE, and read the active mode from GET /api/_compat-mode:
strict— PocketBase shapes only, under/api/…(default)native— Railbase-native shapes under/v1/…(reserved)both— both prefixes; shape resolved per request (migration aid)
Note
The native /v1/… route surface is not mounted yet — today the modes only
influence shape details such as response and realtime payload framing. Build
against /api/…. RAILBASE_PB_COMPAT / RAILBASE_PBCOMPAT remain legacy
fallbacks, but new deployments should use RAILBASE_COMPAT_MODE.
Tip
You rarely call these by hand — generate a typed client with railbase generate sdk and let it do CRUD, auth, and realtime for you. See the
TypeScript SDK.