Build an app on a team Database
A team Database has an HTTP API. A browser or a server reads and writes its tables with no server code of your own.
The API follows PostgREST, so supabase-js works for reads and writes. A sign-in service gives your app's people
their own accounts. A live channel sends the rows again when they change.
- The database decides what a request may do. A table has no access for the web until you grant it, and row-level security chooses the rows.
- A request runs as a role, the role of its key or token. The roles are
anon(a visitor),authenticated(a person who signed in) andservice_role(your server; it passes row-level security). - The address of everything below is
https://gizstack.com/db/TEAM/NAME.TEAMis the slug of your team andNAMEthe name of the Database.
Step 1: Take the keys
Open the Database page and find Data API. Writers of the team see three keys there.
| Key | Use it for |
|---|---|
anon | The browser. Safe to publish: it grants nothing until you do. |
service | Your server only. It reads and writes every table, past every policy. |
| signing key | Your server only, if you make your own tokens. Sign-in (Step 4) makes them for you. |
Step 2: Give a table to the web
A new table is closed. Grant what a visitor may do, then say which rows with a policy.
CREATE TABLE notes (id bigint PRIMARY KEY, owner text, body text);
GRANT SELECT, INSERT, UPDATE, DELETE ON notes TO authenticated;
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
CREATE POLICY own ON notes
USING (owner = current_setting('request.jwt.claim.sub', true));Each claim of the token is a setting named request.jwt.claim.NAME. A person's id is the claim sub.
Step 3: Read and write
With supabase-js:
import { createClient } from "@supabase/supabase-js";
const db = createClient("https://gizstack.com/db/TEAM/NAME", ANON_KEY);
const { data } = await db.from("notes").select("id, body").order("id");
await db.from("notes").insert({ id: 1, owner: "me", body: "hello" });
await db.from("notes").update({ body: "changed" }).eq("id", 1);
await db.from("notes").delete().eq("id", 1);With HTTP:
curl "https://gizstack.com/db/TEAM/NAME/rest/v1/notes?select=id,body&order=id" \
-H "apikey: $ANON_KEY" -H "authorization: Bearer $TOKEN"What the API understands
| You want | Write |
|---|---|
| Some columns | select=id,body |
| A filter | column=op.value with eq, neq, gt, gte, lt, lte, like, ilike, is, in (id=in.(1,2)) |
| Not | body=not.like.*draft* |
| Or, and | or=(id.eq.1,body.eq.hello), and=(...); they nest: or=(a.eq.1,and(b.gt.2,c.is.null)) |
| An order | order=id.desc.nullslast |
| A page | limit=10&offset=20 (at most 1000 rows) |
| One object | Header Accept: application/vnd.pgrst.object+json (.single()): the answer is 406 when the rows are not exactly one |
| A count | Header Prefer: count=exact ({ count: "exact" }): the total is in Content-Range |
| No rows back | Header Prefer: return=minimal |
| An upsert | POST with Prefer: resolution=merge-duplicates or ignore-duplicates, at on_conflict=COLUMNS (else the primary key) |
| A function | POST /rest/v1/rpc/NAME with the arguments as a JSON object (.rpc()) |
Values are always parameters of the statement. No text of a request becomes SQL.
An error has the shape { "code", "message", "details", "hint" }. code is the SQLSTATE of the database when it
gave the error. The status is 401 for a missing or bad key, 403 when a grant or policy refuses, 406 for one object that
is not one, and 400 for the rest.
Not supported
- Embedded resources (
select=id,author(name)): read the tables with two requests, or make a view. - The array and text-search filters (
cs,cd,ov,fts).
Step 4: Sign people in
The service is at /db/TEAM/NAME/auth/v1, with the shapes of GoTrue. People are rows of your own Database (schema
auth), not accounts of the console.
const { data } = await db.auth.signUp({ email: "ann@example.com", password: "correct horse" });
await db.auth.signInWithPassword({ email: "ann@example.com", password: "correct horse" });
const { data: { user } } = await db.auth.getUser();
await db.auth.signOut();- A session holds an access token (valid one hour) and a refresh token. Use the refresh token once: it gives a new
pair.
supabase-jsdoes this for you. - The access token is the role
authenticatedwithsubset to the person's id, so the policy of Step 2 works with no more code.supabase-jssends it with every request after sign-in. - A password has 8 to 128 characters. It is kept as a salted PBKDF2-SHA-256 hash.
- After 10 wrong passwords for one email in 15 minutes, the next sign-in answers 429 (
over_request_rate_limit) until the window passes. A right password clears the count. - Sign-up needs no confirmation mail. Magic links, OAuth and multi-factor sign-in are not part of the service.
Step 5: Follow changes live
Open one WebSocket to /db/TEAM/NAME/live. The channel is the same one every GizWeb page uses, so one connection
carries any number of tables. Browsers cannot set headers on a WebSocket: give the key as ?apikey=.
const live = new WebSocket("wss://gizstack.com/db/TEAM/NAME/live?apikey=" + TOKEN);
live.onopen = () => live.send(JSON.stringify({
v: "", s: [["notes?select=id,body&order=id", "", ""]],
}));
live.onmessage = (e) => {
const m = JSON.parse(e.data);
if (m.r) return console.error("refused", m.r); // 401 or 403: that topic ends
render(m.o[0].rows); // the rows as this person may see them
};- A topic is
TABLE?QUERY, the query of a read above. Ask for several in the lists. - The first message holds the rows now. Each change to the Database sends the rows again, read again as the same person, so row-level security decides what each person sees.
- A change made through the Data API or the Databases page tells the channel. A write over the PostgreSQL connection does not yet.
- A lost socket comes back by itself in the GizWeb kernel. In your own client, open it again and send the same message.
Where it is different from Supabase
| GizStack | |
|---|---|
| Address | /db/TEAM/NAME instead of PROJECT.supabase.co |
| Realtime | One WebSocket with the GizWeb messages above, not the Phoenix protocol: .channel().on("postgres_changes") does not connect |
| Storage and Functions | Not at this address: use GizStack Storage (S3) and apps |
See also: Use GizDB, Move to GizStack.