Skip to main content

REST and GraphQL

Your schema becomes an API. https://<ref>.api.snoutdata.com/rest/v1 serves REST over your tables, views and functions, and /graphql/v1 serves the same data over GraphQL, both generated from the database rather than written by you, and both governed by the same row-level security as a query.

It is on the paid plans, and it is a switch you throw:

snoutdata products enable data-api

Or use the switch under the project's Data API tab in the dashboard, on its Settings tab. Its Docs tab is the reference for your own tables, with @snoutdata/client snippets for each one.

A free project asking for it is refused with a sentence about the plan, never an error that reads like a fault. The reason is running cost rather than packaging: this 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.

REST, with no dependency​

Two headers and a URL. 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>"
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}'

A few that save a round trip: Prefer: return=representation gives you the row back, Prefer: resolution=merge-duplicates makes a POST an upsert, select=*,author(*) embeds a related row through a foreign key, and Range: 0-19 with Prefer: count=exact pages with a total.

GraphQL, the same data and the same policies​

curl -X POST "https://<ref>.api.snoutdata.com/graphql/v1" \
-H "apikey: <your anon key>" \
-H "Authorization: Bearer <your anon key>" \
-H "Content-Type: application/json" \
-d '{"query":"{ todosCollection(filter: {done: {eq: false}}) { edges { node { id title } } } }"}'

The schema is derived from your tables and their foreign keys. Nothing to define, nothing to keep in step. GraphQL covers filtering, paging, mutations, functions, and the comments that rename and extend the schema, including switching introspection on for GraphiQL and code generators.

What decides who sees what​

Row-level security, on every read and every write. Not application code that could be asked to skip it:

alter table public.todos enable row level security;

create policy "a todo belongs to whoever made it"
on public.todos for select
using (owner_id = auth.uid());

A table with no policy returns nothing to the anon key. That is the safe default and it is also the commonest reason a new project's first query comes back empty, so it is worth saying outright rather than leaving somebody to debug it.

Signed out, the anon key goes in both headers. Once a user signs in, their access token goes in Authorization and apikey stays as it was: that is what makes auth.uid() the person rather than nobody. The service_role key bypasses policies entirely and belongs on a server you control and nowhere else.

Which tables appear​

The ones in the schemas the API is exposed on (public by default). A table you do not want on the API does not have to be there: keep it in a schema the API does not serve, and it is reachable from your own connection and nothing else.

How long a request may run​

A request through the data API runs as anon or authenticated, and each has a statement timeout: 3 seconds for anon, 8 seconds for a signed-in user, the same as other hosted Postgres services. Past it the request answers 500 with code 57014 ("canceling statement due to statement timeout") and the database connection is free again. A function that needs longer can say so for itself (create function ... set statement_timeout = '30s'), or you can change a role's for the whole project, which we then leave alone:

alter role authenticated set statement_timeout = '15s';
notify pgrst, 'reload config';

Your own connections (DATABASE_URL, psql, the SQL editor) are not limited by these.

Types for your codebase​

snoutdata gen types typescript > database.types.ts

Generated from the live schema, so a column you renamed shows up as a type error rather than a runtime surprise.

The one rough edge, stated​

Nothing reports that the data API is up yet. Switching it on writes a desire, and the container arrives when the project next restarts, so there is a gap between asking and answering with no field that says "nearly". If a call 404s shortly after you enabled it, that is what you are seeing.

Also read​