The project API
A SnoutData Cloud project is a Postgres database, 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.
SnoutData Cloud is in private testing, and this half of it is the newest part. Read what is switched on, and how before you plan around it.
What is behind each path
| Path | What it is | Plan |
|---|---|---|
/rest/v1 | REST over your tables and functions, generated from the schema | Plus and Pro |
/graphql/v1 | The same data over GraphQL, the same policies | Plus and Pro |
/auth/v1 | Sign-up, sign-in and sessions, signed with your project's own secret | every plan |
/storage/v1 | Files in buckets, in object storage | every plan |
/realtime/v1 | Broadcast, presence and table changes, over a websocket | every plan |
/functions/v1 | Your own TypeScript. See Snout Functions | every plan |
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. There is no client library in the path and no SDK to keep up to date.
A client library works too, if you already use one
The six paths above and the two key roles are a frozen compatibility surface: they are shaped
the way they are on purpose, so an application already written against that shape works here by
changing one URL. @supabase/supabase-js is written against exactly it:
import { createClient } from '@supabase/supabase-js'
const client = createClient('https://<ref>.api.snoutdata.com', '<your anon key>')
It is checked rather than believed. A harness drives a real application through the real 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. As of 2026-09-11 that is 27 of 27, nothing skipped.
Your two keys
snoutdata keys # both, with what each one is
snoutdata keys --json # for a deploy script
snoutdata keys rotate --force # new ones
| Key | Who it is | Where it belongs |
|---|---|---|
anon | a signed-out visitor, and then whoever signs in | your front end, in the browser |
service_role | bypasses row-level security entirely | a 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 and any
session a user is holding, which is why rotate requires --force. 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.
Email sign-up and sign-in work, and a confirmation email is really sent (ours goes out through an already-verified sending domain). OAuth providers are not switched on yet. Google and the rest are per-project settings holding your own client credentials, and the screen for entering them is not built.
Storage
/storage/v1 holds files in object storage, with buckets, policies you write yourself as SQL on
storage.objects, and signed URLs for handing out a private file temporarily.
Writing your own storage policies works. It did not until 2026-09-11: the project owner had no
rights on the storage schema, so create policy on storage.objects, the commonest statement in
any application that stores files, failed outright. It is fixed, and a signed-in user uploading
under their own policy is one of the checks in the harness above.
Realtime
/realtime/v1 carries both halves of what Realtime does: broadcast, which is a message bus
between clients, and postgres_changes, which is your database's own inserts, updates and
deletes arriving on a websocket, filtered by the same row-level security that governs a read.
Two honest caveats, both written down rather than smoothed over:
- The first subscription on a project that has been quiet is dropped, and the next one works. If a subscribe goes silent, subscribe again.
- A schema you create after the project was set up is not covered yet. Realtime evaluates your
policy as an internal role, and that role is granted access to the schemas that existed at
bootstrap. A new schema needs us to re-run it. Tables in
publicare unaffected.
Extensions
Your database is Postgres 17, 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
Being straight about this, because it is the part most likely to catch you out.
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.
The other four are switched on by us, per project, today. The REST and GraphQL API, auth, storage and realtime are all deployed and serving, but there is no button in the dashboard or flag in the CLI that turns one on for your project yet. During private testing, ask and we will enable what you need. Each product has its own tab in the dashboard, and each one says what is answering for your project right now, distinguishing "not built yet" from "not on this plan" and from "we asked and could not tell" rather than collapsing all three into one grey light.
Also read
- Snout Functions, and the secrets they run with.
- Local development, for the same schema on your own machine.
- Limits, and what is not built, for the plan table and the honest gaps.