Docs menu

Backend

Backend gives a web app or a website Postgres with its extensions (pgvector, pg_cron, pg_net, pg_graphql), the Data API, sign-in, file storage, live updates (Realtime) and functions, all on your computer and at the app's own address, with Supabase's Studio to look after it and an import that brings an app across from hosted Supabase. An app or website written with supabase-js works as it is, with the environment names it already reads.

Turn it on

In the app's folder:

$ secretbase backend on

The app's database becomes Backend's Postgres, with its data and the same DATABASE_URL (vector, pg_cron, pg_net and pg_graphql are there). From another folder: secretbase backend my-app on. The first time, it downloads Backend's images. A running app starts again with Backend.

On the dashboard: the app's Backend tab, its on/off switch (recommended: off until your app's code uses the Data API, sign-in or storage). Once sign-in, the Data API and file storage all run, they reserve about 1 GB more memory.

From a laptop, add --to <server> to every backend command on this page. It needs a token for that app: an App or co-developer token, or a Server token with full control; a limited Server token only reads the URL and the publishable key. Connect from a laptop shows how to add one as server:

$ secretbase backend my-app on --to server
  • The database: Postgres 17, with pgvector, pg_cron, pg_net, pg_graphql and PostGIS. Postgres is turned on first if it was off, and an app with data keeps it. pg_net reaches only the app and its own Backend, never the internet.
  • The Data API, sign-in and storage answer at the app's address, under /rest/v1, /graphql/v1, /auth/v1 and /storage/v1, live updates, once on, under /realtime/v1, and functions, once on, under /functions/v1. Every other path goes to your app as before.
  • secretbase backend off stops the Data API, sign-in, storage, live updates, functions and Studio. The database stays Backend's Postgres, with its data.

Moving an app from Supabase: import brings its database, users and files across in one command, its Edge Functions run as functions, and Studio opens from your computer.

On a website

Backend is for web apps (an app with its own server, such as Next.js, Node, Python or Go) and for websites: plain files, or a site built with Vite, Astro or Create React App, such as what Lovable makes. A website gets the same database, Data API, sign-in and storage at its own address, and its files are served as before.

$ secretbase backend my-site on

Then publish it again, so its build gets the URL and the publishable key. On the dashboard: the website's Backend tab, as for an app.

  • A website Secretbase builds gets the URL and the publishable key in its build, under the names supabase-js projects read: VITE_SUPABASE_URL, VITE_SUPABASE_PUBLISHABLE_KEY, VITE_SUPABASE_ANON_KEY, NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY. Never the secret key. Publish it again after turning Backend on, so it is built again with them (and again after a new publishable key). A key of the website's own with one of these names wins.
  • A website of ready-made files (no build) gets nothing baked in: copy the URL and the publishable key from the Backend tab's URL and keys (or secretbase backend my-site keys) and paste them into its code. After a new publishable key, paste the new one in and publish the files again.
  • Plain Postgres or Redis on a website stays refused: a website gets a database only with Backend (see Databases).

Everyone who opens the website can read its publishable key. Before the website uses it, turn row-level security on for every table: a table without it is open to anyone with that key.

The URL and the keys

$ secretbase backend keys

It prints:

URL: <address>
Publishable key: sb_publishable_<…>   (fine in the browser: row-level security decides what it reads)
Secret key: hidden. Show it: `secretbase backend my-app keys --show-secret`.
S3 endpoint: <address>/storage/v1/s3 · region: local
S3 keys: hidden. Show them: `secretbase backend my-app keys --show-secret`.
  • The URL is the app's own address: the one your app's client talks to.
  • The publishable key may be in the browser. On a table with row-level security on, it reads and writes only what the table's policies allow. A table with row-level security off is open to anyone with the publishable key.
  • The secret key goes past row-level security. Keep it on the server: never in the browser, never in git.

A web app gets them by itself, as environment variables: SUPABASE_URL and NEXT_PUBLIC_SUPABASE_URL (the URL), SUPABASE_ANON_KEY and NEXT_PUBLIC_SUPABASE_ANON_KEY (the publishable key) and SUPABASE_SERVICE_ROLE_KEY (the secret key). Its build sees only the two NEXT_PUBLIC_ ones, never the secret key. A key you set with the same name wins. A website gets only the URL and the publishable key, in its build (On a website).

secretbase backend keys rotate publishable (or secret, or s3) makes a new key, and the old one stops working at once. The app gets a new publishable or secret key by itself.

On the dashboard, the Backend tab's URL and keys has the URL and the publishable key with Copy, the secret key behind Show, and Rotate next to each key.

Row-level security

The publishable key is public, so the database itself must decide who sees which rows. A table in the public schema with row-level security off is open to anyone with the publishable key: it reads and writes whatever the table's grants give the key's role, anon, and a migration made with supabase db diff grants anon everything. So turn row-level security on for every table in public before your app or website uses the key, and add a policy for what each kind of visitor may do. With row-level security on and no policy, the publishable key reads nothing from that table.

alter table todos enable row level security;

create policy "Users see their own todos"
on todos for select
to authenticated
using ( (select auth.uid()) = user_id );

Use it from your app

In a web app (Next.js here):

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)

On the server side of your app, process.env.SUPABASE_SERVICE_ROLE_KEY in place of the publishable key acts with full access.

In a website built with Vite (what Lovable makes), the build puts in the values:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY
)

A website has no server side: it only ever has the publishable key, so row-level security is what guards its data.

Sign-in

Email and password, magic links, password resets, and Google and GitHub, through supabase-js's auth. Backend: sign-in shows how to set up its mail and the providers.

File storage

Uploads, public and signed links, image resizing and the S3-style API, through supabase-js's storage. Storage starts by itself on the app's first upload. The files stay on this computer, count in the app's disk, and are in its backups.

$ secretbase backend storage

It says how much the app's files take. secretbase backend storage on starts it now; secretbase backend storage off stops it and keeps the files, and it stays off until you turn it on again. On the dashboard: the Backend tab's File storage switch (recommended: leave it to start by itself on your app's first upload). Buckets and their policies are your app's, made with supabase-js.

For a tool that speaks S3, use the S3 endpoint and the S3 keys from secretbase backend keys --show-secret (or S3-style API, Show on the tab), with the region local. The S3 keys give full access to the app's files: keep them on the server.

Live updates

Live updates are Supabase's Realtime, for supabase-js's channel(): broadcast between visitors, presence (who is online) and database changes. They are off at first; turn them on when your app's code uses channels:

$ secretbase backend realtime on

They reserve about 256 MB more memory while on, and run while the app does. secretbase backend realtime says whether they are on; secretbase backend realtime off stops them. On the dashboard: the Backend tab's Live updates switch (recommended: off until your app uses supabase-js's channels).

supabase-js connects with the app's URL and publishable key as it is, at /realtime/v1. Database changes reach a visitor only for tables in the supabase_realtime publication, and row-level security decides which rows they see:

alter publication supabase_realtime add table messages;

What anyone can reach, as on Supabase:

  • Public channels are open. Anyone with the publishable key (it is in your app's page) can join a public channel, send broadcasts to it and see its presence. For a channel only signed-in visitors should use, make it private: supabase.channel('room', { config: { private: true } }), and allow who may read and send with row-level security policies on the realtime.messages table.
  • Database changes follow the publication and row-level security. Only tables you added to supabase_realtime send changes, and a visitor gets an insert or update only for rows their policies let them read.
  • Deletes aren't filtered. Row-level security can't check a row that is gone, so every visitor listening to a table gets its delete events. With row-level security on, a delete event carries only the row's primary key.

Turning Backend off stops live updates too. They start again with Backend unless you turn them off, which works while Backend is off.

Functions

Functions are your repo's supabase/functions/<name>/index.ts, run on Supabase's own Deno runtime, for supabase-js's functions.invoke(). They are off at first; turn them on when your app has functions:

$ secretbase backend functions on

They reserve about 64 MB more memory while on, and run while the app does. secretbase backend functions lists the functions in the live release; a publish brings new and changed ones. On the dashboard: the Backend tab's Functions switch (recommended: off until your app has supabase/functions).

A function answers at /functions/v1/<name> with the app's key, as on Supabase. For one that must answer without a key (a payment provider's webhook), say so in supabase/config.toml, as on Supabase:

[functions.stripe-webhook]
verify_jwt = false

Each function gets SUPABASE_URL, SUPABASE_ANON_KEY, SUPABASE_SERVICE_ROLE_KEY and the app's own keys (set with secretbase keys), and can reach the internet and import npm: and jsr: modules. Each call runs on its own with up to 150 MB of memory.

Studio

Studio is Supabase's own dashboard for your app's database: its tables, the SQL editor, its users and its files. It opens in your browser through a port on your own computer, never at your app's address:

$ secretbase backend my-app studio --to home

It prints the address to open:

Studio for my-app: <local address>/secretbase-open/k5Jw…Qe (open it in your browser).
Open until you press Ctrl-C. Studio starts when you first open it (it can take a minute) and stops after 30 minutes unused.

Studio has no sign-in of its own, so the address holds a name and a key made for this run (a name that only your own computer answers to): open that exact address, and your browser is let in for as long as the command runs. The address is the key: anyone you give it to gets in while the command runs, so don't share it. Without it, Studio answers 401 and lets nothing in.

On the server itself, leave out --to home. Studio reserves no memory: it runs only while you use it (about 300 MB then), and stops after 30 minutes with nothing open. Studio signs in as the database's admin, so only you, a Server token with full control, and that app's own App and co-developer tokens with full control open it. On the dashboard, the Backend tab's Studio card shows the command, with Copy.

Import from Supabase

One command brings an app on hosted Supabase into a new app's Backend: its tables with their data, grants and policies, its sign-in users with their passwords (nobody has to reset), the triggers it put on its users, and, with its secret key, its files and its confirm-email setting. Turn Backend on for the new app first; the import goes only into an empty database.

$ secretbase backend my-app import --to home

It asks for two values, then says what came across:

The Supabase project's connection string (Connect › Session pooler, with its password):
Its secret key, for its files and sign-in settings (in Supabase: Settings › API Keys; Enter skips):
Importing the Supabase project into my-app (this can take a few minutes)…
my-app has the Supabase project now: 120 sign-in users (their passwords work as before), 14 tables, 310 files in 2 buckets.
Confirm email is on, as it was there.
Google sign-in was on there: turn it on here with its client ID and secret (Supabase never shows the secret again): `secretbase backend my-app auth google on --to home`.

Both values are asked for at hidden prompts (or read as two lines from a file: … import < supabase.txt), sent only to your server, and never kept or logged. In Supabase, the connection string is under Connect › Session pooler (put the database password in it). On the dashboard, the Backend tab's Import from Supabase has the same two fields. An AI agent can't run it (it needs the database password): it gives you the command. If the database part fails, nothing of it is kept; files already copied stay, so running it again into the still-empty app finishes. It goes only into an empty app: no sign-in users, tables or files (other than those an import of it already copied).

Who can change it

You, a Server token with full control, and that app's App and co-developer tokens. A limited Server token, or an App token made with --scope read, sees the URL and the publishable key, never the secret key or the S3 keys; on the dashboard, a sign-in without full control sees "This sign-in can’t see the secret keys: they need full control." See Tokens.

If something goes wrong

  • "This project is a website, so it has no app to use Postgres on its own." (HB-DATABASE-010): a website gets its database with Backend: secretbase backend my-app on.
  • "my-app has no keys while its Supabase features are off." (HB-DATABASE-017): run secretbase backend my-app on.
  • "Live updates are off for this app." (a channel that never joins): run secretbase backend my-app realtime on.
  • "Functions are off for this app.": run secretbase backend my-app functions on. "There is no function by that name.": the function isn't in the live release; publish again.
  • "couldn't get the Backend image" (HB-DATABASE-009): nothing was changed; try again later.
  • "You have a key named DATABASE_URL…" (HB-DATABASE-007): remove that key first, then turn Backend on.
  • supabase-js says "Invalid API key": the key was rotated; use the new one from secretbase backend keys.
  • "That isn't a Supabase connection string." (HB-DATABASE-027): copy the Session pooler string from Supabase's Connect. "…already has…" (HB-DATABASE-028): import into a new app. "Reading the Supabase project stopped" (HB-DATABASE-029): check the password in the string; nothing here was changed.