homeresume
 
   

Supabase basics with Node.js

Published June 3, 2026Last updated June 3, 20266 min read

Supabase is an open-source backend platform built around managed PostgreSQL. You get a database, auto-generated REST APIs (via PostgREST), Auth, file Storage, Realtime subscriptions, and Edge Functions - with a dashboard and SQL editor on top.

Compared to running Postgres yourself, Supabase adds hosted infra, API layers, and product features without you wiring them up. Compared to an ORM-only stack, you often talk to Postgres through the Supabase client or SQL, with RLS (row level security) enforcing access at the database layer.

Prerequisites

  • Supabase account (free tier is enough for learning)
  • Node.js version 26
  • @supabase/supabase-js installed (npm i @supabase/supabase-js)

Create a project and database

  1. In the Supabase dashboard, choose New project, pick a region, and set the database password.
  2. Wait until the project is ready.
  3. Open SQL Editor and run the schema below.

The Table Editor is fine for quick experiments; SQL Editor keeps schema reproducible in git and reviews.

create table if not exists public.todos (
id bigint generated always as identity primary key,
title text not null,
done boolean not null default false,
created_at timestamptz not null default now()
);
create or replace function public.list_open_todos()
returns setof public.todos
language sql
security definer
set search_path = public
as $$
select * from public.todos where done = false order by id;
$$;
create or replace function public.mark_todo_done(todo_id bigint)
returns setof public.todos
language sql
security definer
set search_path = public
as $$
update public.todos set done = true where id = todo_id returning *;
$$;
grant execute on function public.list_open_todos() to service_role;
grant execute on function public.mark_todo_done(bigint) to service_role;

This schema skips RLS setup because this post uses the secret key from Node.js, which bypasses RLS. For browser or mobile clients, use the publishable key instead - it obeys Row Level Security when you enable it on tables.

list_open_todos is a Postgres function exposed as an RPC endpoint.

Postgres functions default to security invoker: they run with the caller's permissions, so RLS applies as that role. security definer runs the function as its owner (often a privileged database role), not as the caller.

API keys and connection

On the project Overview tab you will find:

  • Project URL (https://<ref>.supabase.co) — for SUPABASE_URL
  • Publishable key (sb_publishable_..., legacy anon) — for browsers and mobile apps where RLS should limit access; this post does not use it

For Node.js backends, use a secret key from Project SettingsAPI Keys:

  • Secret key(s) (sb_secret_...) - full data access, bypasses RLS; trusted servers only, never in client bundles or public repos. Replaces the legacy service_role key.
  • Reveal or create a secret key in the dashboard (legacy projects: Legacy anon, service_role API keys tab → service_role).

The Supabase JS client talks to PostgREST and can query tables (from) and call registered functions (rpc) only after they exist. It does not run DDL (create table, create function, grant, and similar), so apply the schema in the SQL Editor (or via Supabase CLI migrations in larger projects).

Store values in environment variables:

SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_SECRET_KEY=your-secret-key

Client setup

Create a client with the project URL and secret key. Read values from the environment in real apps; never commit the secret key.

import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_SECRET_KEY
);

Every table you create in the public schema is available as supabase.from('<table>'). Custom SQL functions are called with supabase.rpc('<function_name>').

Insert data

Insert one or more rows and return the created records with .select().

const { data, error } = await supabase
.from('todos')
.insert([
{ title: 'Learn Supabase client', done: false },
{ title: 'Run RPC example', done: false },
{ title: 'Ship demo', done: true },
])
.select();
if (error) {
throw new Error(error.message);
}
console.log(data);

Read, update, and delete

Select with filters and ordering:

const { data, error } = await supabase
.from('todos')
.select('*')
.eq('done', false)
.order('id');

Update matching rows:

const { data, error } = await supabase
.from('todos')
.update({ done: true })
.eq('id', 1)
.select();

Delete matching rows:

const { error } = await supabase
.from('todos')
.delete()
.eq('id', 1);

Chain .eq(), .in(), .limit(), and other filter helpers the same way across operations.

RPC

Call a database function by name. Our list_open_todos() returns open todos without repeating filter logic in the app.

const { data, error } = await supabase.rpc('list_open_todos');
if (error) {
throw new Error(error.message);
}
console.log(data);

Functions with parameters map to a second argument:

await supabase.rpc('mark_todo_done', { todo_id: 42 });

Define parameters in SQL (todo_id bigint) and grant execute to service_role when calling RPC from a secret-key backend.

What else matters

  • Row Level Security (RLS) - When enabled on a table, Postgres denies access until policies allow it. The publishable key is subject to RLS; the secret key is not. Add policies per role (anon, authenticated) and operation when you turn RLS on.
  • Auth - Supabase Auth stores users in auth.users. After sign-in, the client JWT includes the authenticated role so policies can use auth.uid() for per-user rows.
  • Migrations - Avoid changing production only via the dashboard. Track SQL in versioned migration files and apply with the Supabase CLI (supabase db push, linked projects) so environments stay aligned.
  • Generated types - Run supabase gen types typescript --project-id <ref> to emit TypeScript types from your schema for a type-safe client.
  • Storage - S3-compatible buckets for files (images, PDFs) with bucket policies analogous to RLS.
  • Realtime - Subscribe to insert/update/delete on tables or channels for live UI updates.
  • Edge Functions - Deno functions for webhooks, light API logic, or tasks that should not live in the database.
  • Local dev - supabase init and supabase start spin up local Postgres and services; useful for offline work, while cloud projects are fastest for a first tutorial.

Demo

Runnable scripts for this post live in the supabase-basics-demo folder in the private demos repository. Get access via code demos.