Skip to main content

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.

note

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

PathWhat it isPlan
/rest/v1REST over your tables and functions, generated from the schemaPlus and Pro
/graphql/v1The same data over GraphQL, the same policiesPlus and Pro
/auth/v1Sign-up, sign-in and sessions, signed with your project's own secretevery plan
/storage/v1Files in buckets, in object storageevery plan
/realtime/v1Broadcast, presence and table changes, over a websocketevery plan
/functions/v1Your own TypeScript. See Snout Functionsevery 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
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 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 public are 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