Client libraries
Baselyra ships one official client, for JavaScript and TypeScript. Everything else is plain HTTP with JSON and three headers, which is why the Dart, PHP and Python pages here are complete hand-rolled clients rather than a promise of an SDK that does not exist.
What exists
| Language | Status |
|---|---|
| JavaScript / TypeScript | Official — @baselyra/client, zero dependencies. Browsers, Node 22, Deno, Bun, React Native. |
| Dart / Flutter | A documented, complete plain-HTTP client to copy into your project |
| PHP | A documented, complete plain-HTTP client |
| Python | A documented, complete plain-HTTP client |
| Anything else | Three headers and JSON. See below. |
Which key goes where
| Key | Role | Where it may appear |
|---|---|---|
| anon key | anon | Anywhere. A frontend bundle, a mobile binary, a public repository. It is meant to be public — row level security is what protects the data. |
| service key | service_role, BYPASSRLS | Server-side only. Never in a bundle, never in a NEXT_PUBLIC_* / VITE_* / EXPO_PUBLIC_* variable, never in a mobile app. |
The HTTP contract
Three headers cover every language.
apikey: <anon key> # which Postgres role the request runs as
authorization: Bearer <user access token> # who the user is; wins over apikey
content-type: application/json
| You want | Do |
|---|---|
| Sign in | POST /auth/v1/token?grant_type=password with {email, password} |
| Renew a session | POST /auth/v1/token?grant_type=refresh_token with {refresh_token} |
| Read rows | GET /rest/v1/<table>?<filters> |
| Write rows | POST / PATCH / DELETE /rest/v1/<table> |
| Call a function | POST /rest/v1/rpc/<name> with the named arguments |
| Upload a file | POST /storage/v1/object/<bucket>/<key> |
| Watch changes | WebSocket /realtime/v1?apikey=<token> |
The error envelope
Every failure, from every prefix, has the same shape. Write one decoder and reuse it.
{"error":{"code":"unique_violation",
"message":"duplicate key value violates unique constraint \"articles_slug_key\"",
"details":{"detail":"Key (slug)=(hello) already exists.","hint":null,"sqlstate":"23505"}}}
| Field | Meaning |
|---|---|
code | A stable string — bad_request, unauthorized, unique_violation, statement_timeout. Branch on this, not on the message. |
message | Human-readable. Safe to show a developer; think before showing a user. |
details | null, or an object. For a Postgres error it carries detail, hint and sqlstate. |
Things every client should do
- Store the rotated refresh token. Every refresh consumes its token and returns a new one. Replaying a spent token revokes the entire chain, and the user is signed out. This is the single most common bug in a hand-rolled client.
- Refresh before the access token expires, not after a
401. An hour is the default lifetime, and a failure-driven refresh means every call site has to know how to replay itself. - Send a filter on every update and delete. The server refuses an unfiltered one, and that refusal is a feature — but a client that constructs the URL by hand should assert it too.
- Read
content-rangefor the total when you asked forPrefer: count=exact. - Percent-encode filter values. A literal
+in a query string decodes to a space.
The JavaScript client
npm install @baselyra/client
import { createClient } from '@baselyra/client';
const bl = createClient('https://api.example.com', ANON_KEY);
const { data, error } = await bl.from('posts').select('id,title').eq('published', true);
Zero dependencies: fetch and WebSocket are the only platform features it
uses, so the same build runs in browsers, Node 22, Deno, Bun and React Native with
no polyfill and no bundler shim. The whole reference is on
the JavaScript client page.
Framework guides
Anything else
Sign in, read, write, subscribe. That is the whole protocol.
# 1. sign in
curl -s -X POST "$URL/auth/v1/token?grant_type=password" \
-H 'content-type: application/json' \
-d '{"email":"ada@example.com","password":"…"}'
# 2. read as that user — RLS decides what comes back
curl -s "$URL/rest/v1/notes?select=id,title&order=created_at.desc" \
-H "apikey: $ANON_KEY" -H "authorization: Bearer $ACCESS_TOKEN"
# 3. write
curl -s -X POST "$URL/rest/v1/notes" \
-H "apikey: $ANON_KEY" -H "authorization: Bearer $ACCESS_TOKEN" \
-H 'content-type: application/json' -H 'Prefer: return=representation' \
-d '{"title":"From anywhere"}'
# 4. watch
websocat "wss://api.example.com/realtime/v1?apikey=$ACCESS_TOKEN"
{"type":"subscribe","channel":"public:notes"}
The full query language is on Filtering and paging, sessions on Auth, files on Storage and the socket protocol on Realtime.