Docs · Reference
Project API
Each project has its own address, and supabase-js connects to it unchanged. Every route below is a list held to the server’s router by a test.
Address and keys
A project lives at https://<ref>.<zone>. Send a key with every request in the apikey header (or Authorization: Bearer). The publishable key is for browsers and follows the access rules (RLS). The secret key bypasses every rule, so use it only on a server. A signed-in user’s token in Authorization: Bearer runs the request as that user. The keys are on the API keys screen.
curl "https://abcdefghijklmnopqrst.end.blackall.ai/rest/v1/notes?select=id,title" \
-H "apikey: <publishable key>"
supabase-js compatibility
Paths, query strings, headers and responses are Supabase’s, so @supabase/supabase-js works as it is: change only the address and the key. The supabase-js column of each table is the call that reaches that route.
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://abcdefghijklmnopqrst.end.blackall.ai', '<publishable key>')
Data (/rest/v1)
The PostgREST query language. The table name is the path and column names are query parameters. Every request runs with the caller’s role and the access rules (RLS).
| Method | Path | What it does | supabase-js |
|---|---|---|---|
| GET | /rest/v1/{table} | Read rows | from('t').select() |
| HEAD | /rest/v1/{table} | Count only | select('*', { head: true, count: 'exact' }) |
| POST | /rest/v1/{table} | Insert rows (upsert with Prefer: resolution=…) | insert() / upsert() |
| PUT | /rest/v1/{table} | Upsert one row | |
| PATCH | /rest/v1/{table} | Update the rows that match the filters | update() |
| DELETE | /rest/v1/{table} | Delete the rows that match the filters | delete() |
| GET | /rest/v1/rpc/{function} | Call a function (read) | rpc('f', args, { get: true }) |
| POST | /rest/v1/rpc/{function} | Call a function | rpc('f', args) |
| POST | /graphql/v1 | GraphQL (pg_graphql, once turned on under Database → Extensions; same keys and access rules as /rest/v1) | fetch / Apollo / urql |
Query parameters
| Name | Description |
|---|---|
select | Columns to return. Aliases (label:title), casts (id::text), JSON paths (data->>name), rows joined through a foreign key (*,author(name), !inner) |
order | Ordering, created_at.desc.nullslast; several with commas |
limit | How many rows, at most 1000 |
offset | How many rows to skip |
on_conflict | The columns an upsert matches on |
columns | The columns an insert writes |
<column> | A column=operator.value filter |
or · and | or=(a.eq.1,b.gt.2), and=(…), nested, and not.or=(…) |
Filter operators
Only these operators are accepted. An unknown one is refused, not silently ignored. Prefix any of them with not. to negate it.
| Operator | SQL |
|---|---|
eq | = |
neq | <> |
gt | > |
gte | >= |
lt | < |
lte | <= |
like | like |
ilike | ilike |
match | ~ |
imatch | ~* |
cs | @> |
cd | <@ |
ov | && |
sl | << |
sr | >> |
nxr | &< |
nxl | &> |
adj | -|- |
isdistinct | is distinct from |
is | is null | not null | true | false | unknown |
in | in (…) |
fts | @@ to_tsquery() |
plfts | @@ plainto_tsquery() |
phfts | @@ phraseto_tsquery() |
wfts | @@ websearch_to_tsquery() |
Headers
| Name | Description |
|---|---|
Prefer | return=minimal|representation|headers-only, count=exact|planned|estimated, resolution=merge-duplicates|ignore-duplicates, missing=default |
Accept | Accept: application/vnd.pgrst.object+json returns one row as an object (.single()); zero or several rows is a 406 |
Accept-Profile | Accept-Profile and Content-Profile accept only public |
Content-Range | The total (count=) comes back in the Content-Range header |
Calling functions (rpc)
/rest/v1/rpc/<function> runs a SQL function in the project schema with the caller’s role. With POST the body is the arguments; with GET, query parameters named like an argument are arguments and the rest filter the result.
const { data, error } = await supabase
.from('notes')
.select('id, title, author(name)')
.eq('done', false)
.order('created_at', { ascending: false })
.range(0, 19)
await supabase.from('notes').insert({ title: 'Hello' }).select().single()
await supabase.rpc('search_notes', { query: 'hello' })
curl "https://abcdefghijklmnopqrst.end.blackall.ai/rest/v1/notes?select=id,title&done=eq.false&order=created_at.desc&limit=20" \
-H "apikey: <publishable key>" \
-H "Prefer: count=exact"
Not supported
- Aggregate (
count(),sum()and so on) and spread (...rel(*)) selects - Schemas other than
public - Reading more than 1000 rows at once (
limitis capped at 1000)
Auth (/auth/v1)
GoTrue’s paths and responses. Email and password, magic links, anonymous sign-in, social sign-in (with PKCE), TOTP two-factor authentication and user management with the secret key.
| Method | Path | What it does | supabase-js |
|---|---|---|---|
| POST | /auth/v1/signup | Sign up; with no email and password, an anonymous sign-in | auth.signUp() / auth.signInAnonymously() |
| POST | /auth/v1/token?grant_type=password | Sign in with email and password | auth.signInWithPassword() |
| POST | /auth/v1/token?grant_type=refresh_token | Refresh a session | auth.refreshSession() |
| POST | /auth/v1/token?grant_type=pkce | Exchange a PKCE code for a session | auth.exchangeCodeForSession() |
| POST | /auth/v1/token?grant_type=web3 | Sign in with a Sign-In with Ethereum message the wallet signed | auth.signInWithWeb3() |
| POST | /auth/v1/token?grant_type=id_token | Sign in with a Google or Apple ID token | auth.signInWithIdToken() |
| POST | /auth/v1/otp | Send a magic link or a text-message code | auth.signInWithOtp({ email } | { phone }) |
| POST | /auth/v1/verify | Verify a mailed token or a text-message code (signup, magiclink, recovery, email_change, sms, phone_change) | auth.verifyOtp() |
| GET | /auth/v1/verify | A mailed link opened in a browser; redirects to a registered address with the session | |
| POST | /auth/v1/resend | Resend the confirmation mail | auth.resend() |
| POST | /auth/v1/recover | Send a password reset mail | auth.resetPasswordForEmail() |
| POST | /auth/v1/reset | Set a new password with a reset token (used by the page shown when there is nowhere to redirect) | |
| GET | /auth/v1/user | The signed-in user and their factors | auth.getUser() |
| PUT | /auth/v1/user | Change email, password or metadata | auth.updateUser() |
| GET | /auth/v1/reauthenticate | Mail a code to confirm a password change | auth.reauthenticate() |
| POST | /auth/v1/logout | Sign out (scope=local|others|global) | auth.signOut({ scope }) |
| GET | /auth/v1/settings | Which sign-in methods are on | |
| GET | /auth/v1/authorize | Start a social sign-in | auth.signInWithOAuth() |
| GET | /auth/v1/callback | Where the provider returns (POST for Apple) | |
| DELETE | /auth/v1/user/identities/{id} | Unlink a social identity | auth.unlinkIdentity() |
| POST | /auth/v1/factors | Enroll a TOTP factor | auth.mfa.enroll({ factorType: 'totp' }) |
| POST | /auth/v1/factors/{id}/challenge | Start a challenge | auth.mfa.challenge() |
| POST | /auth/v1/factors/{id}/verify | Verify a code (an aal2 session) | auth.mfa.verify() |
| DELETE | /auth/v1/factors/{id} | Remove a factor | auth.mfa.unenroll() |
| GET | /auth/v1/admin/users | List users (secret key, per_page at most 1000) | auth.admin.listUsers({ page, perPage }) |
| POST | /auth/v1/admin/users | Create a user (secret key) | auth.admin.createUser() |
| POST | /auth/v1/sso | Start a SAML SSO sign-in (returns the IdP address; PKCE works) | auth.signInWithSSO({ domain } | { providerId }) |
| GET | /auth/v1/sso/saml/metadata | This project's SP metadata (register it with the IdP) | |
| POST | /auth/v1/sso/saml/acs | Where the IdP posts its SAML response (signature, audience and replay checked) | |
| GET | /auth/v1/admin/sso/providers | List SSO providers (secret key) | auth.admin.sso.listProviders() |
| POST | /auth/v1/admin/sso/providers | Add an SSO provider (metadata_url or metadata_xml, domains) | auth.admin.sso.createProvider() |
| GET | /auth/v1/admin/sso/providers/{id} | One SSO provider | auth.admin.sso.getProvider() |
| PUT | /auth/v1/admin/sso/providers/{id} | Change an SSO provider (re-read metadata after a certificate rotation) | auth.admin.sso.updateProvider() |
| DELETE | /auth/v1/admin/sso/providers/{id} | Delete an SSO provider | auth.admin.sso.deleteProvider() |
| GET | /auth/v1/admin/users/{id} | One user (secret key) | auth.admin.getUserById() |
| PUT | /auth/v1/admin/users/{id} | Update a user (secret key) | auth.admin.updateUserById() |
| DELETE | /auth/v1/admin/users/{id} | Delete a user (secret key) | auth.admin.deleteUser() |
| POST | /auth/v1/invite | Send an invitation email, through the project's SMTP and template (secret key) | auth.admin.inviteUserByEmail() |
| POST | /auth/v1/admin/generate_link | Make a mail link: signup, invite, magiclink, recovery (secret key) | auth.admin.generateLink() |
| GET | /auth/v1/.well-known/jwks.json | Public keys to verify user tokens (ES256) | |
| GET | /auth/v1/.well-known/openid-configuration | OpenID configuration (issuer, jwks_uri) | |
| GET | /auth/v1/.well-known/oauth-authorization-server | OAuth authorization server metadata (RFC 8414, for MCP clients) | |
| GET | /.well-known/oauth-authorization-server/auth/v1 | The same metadata, at the address RFC 8414 defines |
Token grants (grant_type): password, refresh_token, pkce, id_token (an ID token from Google or Apple).
Social sign-in providers: google, kakao, naver, apple, github, gitlab, microsoft, facebook, line, discord, x, bitbucket, figma, linkedin_oidc, notion, twitch, slack_oidc, spotify, zoom, azure, keycloak, workos, twitter, slack. Configure them in the console or with the management API’s set_auth_provider.
await supabase.auth.signUp({ email: '[email protected]', password: 'a-long-passphrase' })
await supabase.auth.signInWithPassword({ email: '[email protected]', password: 'a-long-passphrase' })
await supabase.auth.signInWithOAuth({ provider: 'google', options: { redirectTo: 'https://app.example.com' } })
const { data: { user } } = await supabase.auth.getUser()
Not supported
- Second factors other than TOTP (WebAuthn, phone)
- Reauthentication (
reauthenticate) and linking an identity (linkIdentity); only unlinking is supported auth.admin.inviteUserByEmail; usegenerateLink({ type: 'invite' })instead
Storage (/storage/v1)
Supabase Storage’s paths. Files are recorded in a table of the project, so the access rules on that table decide who may read and write each file.
A file is accepted up to 50MB by default (a bucket can set a lower limit). A signed URL lasts at most 7 days, and a signed upload URL two hours. Bucket names use lowercase letters, digits, ., _ and -.
| Method | Path | What it does | supabase-js |
|---|---|---|---|
| GET | /storage/v1/bucket | List buckets | storage.listBuckets() |
| POST | /storage/v1/bucket | Create a bucket (public, file_size_limit, allowed_mime_types) | storage.createBucket() |
| GET | /storage/v1/bucket/{bucket} | One bucket | storage.getBucket() |
| PUT | /storage/v1/bucket/{bucket} | Change a bucket’s settings | storage.updateBucket() |
| DELETE | /storage/v1/bucket/{bucket} | Delete an empty bucket | storage.deleteBucket() |
| POST | /storage/v1/bucket/{bucket}/empty | Empty a bucket | storage.emptyBucket() |
| POST | /storage/v1/object/{bucket}/{path} | Upload (x-upsert) | from('b').upload() |
| PUT | /storage/v1/object/{bucket}/{path} | Replace | from('b').update() |
| GET | /storage/v1/object/{bucket}/{path} | Download as the caller (HEAD too) | from('b').download() |
| DELETE | /storage/v1/object/{bucket}/{path} | Delete one file | |
| DELETE | /storage/v1/object/{bucket} | Delete several files (prefixes, at most 1000) | from('b').remove([paths]) |
| GET | /storage/v1/object/public/{bucket}/{path} | A file in a public bucket | from('b').getPublicUrl() |
| GET | /storage/v1/object/authenticated/{bucket}/{path} | Download as the signed-in user | |
| POST | /storage/v1/object/list/{bucket} | List one folder level (limit at most 1000) | from('b').list(prefix, { limit, offset, sortBy, search }) |
| GET | /storage/v1/object/info/{bucket}/{path} | File details | from('b').info() |
| POST | /storage/v1/object/move | Move | from('b').move() |
| POST | /storage/v1/object/copy | Copy | from('b').copy() |
| POST | /storage/v1/object/sign/{bucket}/{path} | Make a signed URL | from('b').createSignedUrl(path, expiresIn, { transform }) |
| POST | /storage/v1/object/sign/{bucket} | Several signed URLs (at most 100) | from('b').createSignedUrls(paths, expiresIn) |
| GET | /storage/v1/object/sign/{bucket}/{path} | Download with a signed URL (?token=) | |
| POST | /storage/v1/object/upload/sign/{bucket}/{path} | Make a signed upload URL | from('b').createSignedUploadUrl() |
| PUT | /storage/v1/object/upload/sign/{bucket}/{path} | Upload with a signed URL (?token=) | from('b').uploadToSignedUrl() |
| GET | /storage/v1/render/image/public/{bucket}/{path} | Transform a public image | from('b').getPublicUrl(path, { transform }) |
| GET | /storage/v1/render/image/authenticated/{bucket}/{path} | Transform an image as the signed-in user | from('b').download(path, { transform }) |
| GET | /storage/v1/render/image/sign/{bucket}/{path} | Transform an image with a signed URL | |
| POST | /storage/v1/upload/resumable | Start a resumable upload (TUS 1.0, Upload-Length, bucketName and objectName in Upload-Metadata) | tus-js-client / Uppy |
| PATCH | /storage/v1/upload/resumable/{id} | Send the next part (Upload-Offset, application/offset+octet-stream) | |
| HEAD | /storage/v1/upload/resumable/{id} | How much has been received so far | |
| DELETE | /storage/v1/upload/resumable/{id} | Cancel a resumable upload | |
| GET | /storage/v1/s3/{bucket}/{key} | S3-compatible API (SigV4 with an S3 key made in the console, path-style, default region ap-northeast-2) | AWS SDK / rclone |
Upload the file as the body (with its Content-Type) or as multipart/form-data. POST refuses an existing name unless x-upsert: true; PUT replaces.
Image transformation
Pass the options as query parameters to /render/image/{public|authenticated|sign}/<bucket>/<path>. A server without an image transformer serves the original and says so in the x-blackwall-transform header.
| Name | Description |
|---|---|
width | 1 to 2500 pixels |
height | 1 to 2500 pixels |
resize | cover (default), contain, fill |
quality | 20 to 100 (default 80) |
format | origin (default), webp, avif, png, jpeg |
const bucket = supabase.storage.from('avatars')
await bucket.upload('me.png', file, { upsert: true })
const { data } = bucket.getPublicUrl('me.png', { transform: { width: 200, height: 200 } })
const { data: signed } = await bucket.createSignedUrl('me.png', 3600)
Not supported
- A per-file cache setting (the
cacheControloption) is not stored
Realtime (/realtime/v1)
wss://<ref>.<zone>/realtime/v1/websocket?apikey=<key>&vsn=1.0.0 speaks Phoenix channels, which is what supabase-js uses. vsn=2.0.0 uses array messages and binary broadcasts. /realtime/v1 (without websocket) is a plain JSON protocol for the same features.
A channel’s topic is realtime:<name>. Clients that join the same name share broadcasts and presence.
Events a client sends
| Event | Description |
|---|---|
phx_join | Join a channel: config.broadcast.self/ack, config.presence.key, config.postgres_changes, access_token |
phx_leave | Leave a channel |
heartbeat | Sent on the phoenix topic; the server answers at once with phx_reply. Used to check the connection is alive |
access_token | Switch to a signed-in user’s new token |
broadcast | Send to the others in the channel (never touches the database) |
presence | track to publish your state, untrack to remove it |
Events the server sends
| Event | Description |
|---|---|
phx_reply | The answer to a message (ok or error) |
presence_state | Who is already there when you join |
presence_diff | Who joined and who left |
broadcast | Send to the others in the channel (never touches the database) |
postgres_changes | A row changed in a table (INSERT, UPDATE, DELETE) |
phx_close | The channel closed |
system | Notices such as a refused token switch |
postgres_changes arrive only from tables with realtime turned on in the console or with set_realtime. Each row is checked against the receiver’s access rules before it is sent. Filters are column=eq|neq|lt|lte|gt|gte|in.value.
supabase
.channel('room-1')
.on('broadcast', { event: 'cursor' }, ({ payload }) => console.log(payload))
.on('presence', { event: 'sync' }, () => console.log('online'))
.on('postgres_changes', { event: 'INSERT', schema: 'public', table: 'notes', filter: 'done=eq.false' }, (change) => console.log(change.new))
.subscribe()
wss://abcdefghijklmnopqrst.end.blackall.ai/realtime/v1/websocket?apikey=<publishable key>&vsn=1.0.0
{"topic":"realtime:room-1","event":"phx_join","payload":{"config":{"broadcast":{"self":false},"presence":{"key":"me"},"postgres_changes":[{"event":"*","schema":"public","table":"notes"}]}},"ref":"1","join_ref":"1"}
{"topic":"realtime:room-1","event":"broadcast","payload":{"type":"broadcast","event":"cursor","payload":{"x":1}},"ref":"2","join_ref":"1"}
{"topic":"phoenix","event":"heartbeat","payload":{},"ref":"3"}
Not supported
- Private channels (
config.private: true) - The previous values of an UPDATE (
oldis empty)
Functions (/functions/v1)
/functions/v1/<name> calls a deployed function. Every HTTP method is passed through, and the body may be up to 2MB. A private function (the default) needs a key of this project: the publishable key (anon), a signed-in user's token (authenticated) or the secret key (service_role). A function deployed as public can be called with no key. The function receives the verified claims, never the token. Names start with a lowercase letter and use lowercase letters, digits and -.
| Method | Path | What it does | supabase-js |
|---|---|---|---|
| * | /functions/v1/{name} | Call a function (any method) | functions.invoke('name', { body }) |
const { data, error } = await supabase.functions.invoke('hello', { body: { name: 'world' } })
curl -X POST "https://abcdefghijklmnopqrst.end.blackall.ai/functions/v1/hello" \
-H "Authorization: Bearer <user token or secret key>" \
-H "content-type: application/json" \
-d '{"name":"world"}'
Other routes
| Method | Path | What it does | supabase-js |
|---|---|---|---|
| POST | /kv/v1 | KV commands (Upstash style ["SET", "k", "v"]; also /kv/v1/pipeline, /kv/v1/keys, /kv/v1/usage; secret key) | @upstash/redis |
| * | /kv/v1/{key} | Read, write or delete one KV key (GET, PUT, DELETE, /incr, /expire) | |
| GET | /config/v1 | The whole edge config (secret key) | @vercel/edge-config |
| GET | /config/v1/{key} | One edge config value | get('key') |
| POST | /ai/v1/{endpoint} | AI gateway, OpenAI-compatible (chat/completions, embeddings, images/generations, audio/speech, audio/transcriptions, models) | openai SDK (baseURL) |
| GET | /health | Whether the project answers ({ project, ok }) | |
| GET | /realtime/v1/websocket | Phoenix channels (websocket) | channel('name').subscribe() |
| GET | /realtime/v1 | The plain JSON protocol (websocket) |
Errors
Errors have the shape each Supabase client reads on that route. Data errors are PostgREST’s: code (PGRST…), message, details, hint. Auth errors are GoTrue’s: code, error_code, msg (and OAuth’s error and error_description from the token endpoint). Storage errors are storage-js’s: statusCode, error, message. Messages follow Accept-Language (Korean, English, Japanese).
{ "code": "PGRST116", "message": "…", "hint": null, "details": "…" }