Skip to main content

The project API

A SnoutData Cloud project has a Postgres database at its centre, and in front of it is an HTTPS door serving the things an application actually needs: a REST and GraphQL API over your tables, authentication, file storage, realtime subscriptions, and your own functions on the edge.

https://<ref>.api.snoutdata.com

That is a different name from <ref>.db.snoutdata.com, which is the Postgres port. One project, two doors.

note

Auth, storage, the data API and Snout Functions are switches you throw, and realtime needs no switch at all. What is switched on, and how says it product by product, which is worth reading before you design around one.

What is behind each path​

PathWhat it isPlanIts page
/rest/v1REST over your tables and functions, generated from the schemaPlus, Pro and BusinessREST and GraphQL
/graphql/v1The same data over GraphQL, the same policiesPlus, Pro and BusinessREST and GraphQL
/auth/v1Sign-up, sign-in and sessions, signed with your project's own secretevery planAuthentication
/storage/v1Files in buckets, in object storageevery planFile storage
/realtime/v1Broadcast, presence and table changes, over a websocketbroadcast and presence every plan; table changes paidRealtime
/functions/v1Your own TypeScriptevery planSnout Functions
/push/v1Notifications to iPhone, Android and the webevery plan; a later send paidPush notifications

Each product has a page of its own, linked above: what it does, the code, what each plan gets and what is not built. This page is the door they share, the keys that open it, and which of them is switched on how.

The data API is the one that costs money, and the reason is running cost rather than packaging. /rest/v1 is a server per project that runs whether or not anybody calls it, and on a free project it would cost more per month than the database does. A free project asking for it is refused with a sentence about the plan, never an error that reads like a fault.

It is ordinary HTTP, and you need nothing installed​

/rest/v1 is a REST API. Two headers and a URL, from anything that can make a request:

# Read. Filters, ordering and paging are query parameters.
curl "https://<ref>.api.snoutdata.com/rest/v1/todos?select=id,title&done=eq.false&order=id.desc&limit=20" \
-H "apikey: <your anon key>" \
-H "Authorization: Bearer <your anon key>"

# Write.
curl -X POST "https://<ref>.api.snoutdata.com/rest/v1/todos" \
-H "apikey: <your anon key>" \
-H "Authorization: Bearer <your anon key>" \
-H "Content-Type: application/json" \
-H "Prefer: return=representation" \
-d '{"title":"write the docs","done":false}'

The same thing from a browser or a server, with no dependency:

const BASE = 'https://<ref>.api.snoutdata.com/rest/v1'
const KEY = '<your anon key>'

// The anon key goes in both headers signed out. Once somebody signs in, put THEIR access token
// in Authorization and leave apikey alone: that is what makes `auth.uid()` work in your policies.
const headers = { apikey: KEY, Authorization: `Bearer ${KEY}` }

const todos = await fetch(`${BASE}/todos?select=*&done=eq.false`, { headers }).then((r) => r.json())

await fetch(`${BASE}/todos`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/json', Prefer: 'return=representation' },
body: JSON.stringify({ title: 'write the docs', done: false })
})

Row-level security decides what comes back, whichever way you call it.

The client library​

@snoutdata/client is one package for all of it: the data API, sign-in, files, realtime and Snout Functions, from the project URL and a key. It has no dependencies and runs in every browser and in Node 22 or later.

npm install @snoutdata/client
import { createClient } from '@snoutdata/client'

const db = createClient('https://<ref>.api.snoutdata.com', '<your anon key>')

const { data, error } = await db.from('todos').select('id, title').eq('done', false)

Once somebody signs in with db.auth, every request carries their token for you, so your policies see them. A refusal is an error in the result, never a throw.

It is checked rather than believed. A harness drives a real application through the client against a real project: sign-up, sign-in, an insert under row-level security and a read-back, the signed-out refusal, broadcast, postgres_changes, a file up and down, a signed URL, a Snout Function and a GraphQL query, all of it passing.

Your two keys​

snoutdata keys # both, with what each one is
snoutdata keys --json # for a deploy script
snoutdata keys rotate --force # new ones
KeyWho it isWhere it belongs
anona signed-out visitor, and then whoever signs inyour front end, in the browser
service_rolebypasses row-level security entirelya server you control, and nowhere else

The two look identical in a terminal and the difference between them is your entire dataset, so the CLI says which is which every time it prints them.

Rotating breaks every key already issued, including any client shipped to a browser, which is why rotate requires --force. Every access token a user holds stops too, but signed-in users are not signed out: their session refreshes into a token signed with the new secret, so once your app carries the new anon key nobody has to sign in again. The command answers once the new keys work. Do it when a key has leaked, not on a schedule.

Row-level security is the thing that actually decides what the anon key can see. A table with no policy returns nothing to it, which is the safe default and is also the commonest reason a new project's first query comes back empty.

Auth​

/auth/v1 signs its tokens with your project's own secret, so a token it issues is a token your database understands: auth.uid() inside a policy is the user who made the request, and your users are rows in your own database. Email sign-up and sign-in work and really send mail, and Sign in with Google works with your own Google client.

Authentication is the page: the sign-up call, how the token reaches your policies, the rate limits, and what is missing.

Storage​

/storage/v1 holds files in buckets, with policies you write yourself as SQL on storage.objects and signed URLs for handing out a private file temporarily. Bytes are streamed through the storage service, never held whole in memory.

File storage is the page: buckets, a policy worth copying, image transforms, the per-plan sizes, and the storage-policy defect that was fixed on 2026-09-11.

Realtime​

/realtime/v1 carries three things on one websocket: broadcast and presence, which are a message bus between your clients and are free on every plan, and table changes, which are your database's own inserts, updates and deletes arriving as they are committed, filtered by the same row-level security that governs a read, and are paid.

Realtime is the page for the code, the two things you must set before a row reaches a subscriber, the per-plan numbers and the one known defect.

Push​

/push/v1 sends notifications to iPhone, Android and the web, and so does push.send() in SQL. The devices, the queue, the delivery log and your Apple and Firebase keys are tables in your own database, and a policy on push.messages decides who may send. It is switched on per project, on every plan, from the dashboard's Push tab.

Push notifications is the page: the keys, registering a device, sending, and what the log does and does not tell you.

Extensions​

Your database is Postgres 18, and you install extensions yourself with plain create extension, without being a superuser: pgvector, PostGIS and the rest of the usual set are available. There is no per-plan extension list yet, so anything the image ships can be switched on.

What is switched on, and how​

Snout Functions are entirely self-serve. snoutdata functions deploy works on any project, on any plan, with nothing to switch on first. See Snout Functions.

Auth, storage and the REST and GraphQL API can be switched on per project: with the switch at the top of the dashboard's Auth and Storage tabs and in its Data API tab, in Studio's project tab (Projects in Studio), and with snoutdata products enable auth|storage|data-api. The data API is on paid plans only. A switch asks for the change and it arrives within about a minute; the data API's container arrives when the project next restarts. Realtime needs no switch: it is on for every project from the start. Each product's tab in the dashboard says whether it is off, starting or on for your project right now, and "not on this plan" where that is the reason.

Also read​