TL;DR: Sign up at supabase.com, create a project, copy the Project URL and anon key from Settings β API into your env vars, and call createClient(url, key). Database, auth, storage, and a JS SDK all come from that one signup. Free tier: 500 MB DB, 50K active users.
Postgres + Auth + Storage + a JS SDK, one signup. Most "I just need a backend" projects start here. Free tier is real β 500 MB DB, 50K active users, 1 GB storage. Ten minutes to a working sign-up flow.
Sign up at supabase.com β (free; GitHub OAuth or email).
Click New Project. Pick:
Provisioning takes ~2 min. Then you're in the project dashboard.
Project dashboard β Project Settings β API. Copy two values:
https://<ref>.supabase.coBackend env:
SUPABASE_URL=https://<ref>.supabase.co
SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIs...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIs... # backend ONLY
npm install @supabase/supabase-js
Other languages: Python β, Dart/Flutter β, Swift β, Kotlin β, C# β.
import { createClient } from "@supabase/supabase-js";
export const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
);
This client is safe to use in both frontend and backend. For backend code that needs to bypass row-level security (admin operations), create a separate client with the service-role key.
Supabase dashboard β Table Editor β New table. Or use SQL Editor:
CREATE TABLE notes (
id BIGSERIAL PRIMARY KEY,
user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
body TEXT,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Enable Row Level Security (RLS)
ALTER TABLE notes ENABLE ROW LEVEL SECURITY;
-- Allow users to read their own notes
CREATE POLICY "users read own notes" ON notes
FOR SELECT USING (auth.uid() = user_id);
-- Allow users to insert their own notes
CREATE POLICY "users insert own notes" ON notes
FOR INSERT WITH CHECK (auth.uid() = user_id);
Row Level Security (RLS) is the most important Supabase concept. Without RLS, your anon key lets anyone read/write everything. With RLS, the database itself enforces who-can-do-what. Always enable RLS on tables that store user data.
// Read
const { data: notes, error } = await supabase
.from("notes")
.select("*")
.order("created_at", { ascending: false });
// Write
const { data: newNote, error } = await supabase
.from("notes")
.insert({ title: "Hello", body: "First note" })
.select()
.single();
// Update
await supabase
.from("notes")
.update({ title: "Updated title" })
.eq("id", newNote.id);
// Delete
await supabase.from("notes").delete().eq("id", newNote.id);
If the user isn't signed in (next step), these will return empty arrays because the RLS policies require auth.uid(). That's the system working correctly.
Supabase Auth supports email/password, magic links, phone, and dozens of OAuth providers β all from the same SDK.
// Sign up
const { data, error } = await supabase.auth.signUp({
email: "[email protected]",
password: "secure-password",
});
// Sign in
const { data, error } = await supabase.auth.signInWithPassword({
email: "[email protected]",
password: "secure-password",
});
// Sign out
await supabase.auth.signOut();
// Get current user (returns null if not signed in)
const { data: { user } } = await supabase.auth.getUser();
After sign-in, the SDK stores a JWT in local storage. Every subsequent supabase.from(...) call carries it; RLS policies use auth.uid() to filter.
Project dashboard β Authentication β Providers. Toggle Google, GitHub, Apple, etc. Each provider asks for Client ID + Client Secret from that provider's console β see Sign in with Google and Sign in with GitHub for how to get those.
Then in your app:
await supabase.auth.signInWithOAuth({
provider: "google",
options: { redirectTo: "https://yourapp.com/auth/callback" },
});
That's the entire flow β Supabase handles the redirect dance, JWT issuance, and session refresh.
supabase.auth.onAuthStateChange((event, session) => {
// event: "SIGNED_IN" | "SIGNED_OUT" | "TOKEN_REFRESHED" | ...
if (event === "SIGNED_IN") {
console.log("Signed in as", session.user.email);
}
});
Subscribe once at app startup. Useful for syncing UI state (show "Sign out" button when signed in, etc.).
Supabase publishes Postgres changes over WebSockets. Subscribe to a table:
const channel = supabase
.channel("notes-changes")
.on(
"postgres_changes",
{ event: "*", schema: "public", table: "notes" },
(payload) => {
console.log("Change:", payload);
}
)
.subscribe();
// Later, to disconnect:
supabase.removeChannel(channel);
Combine with React state: live multiplayer cursors, real-time chat, collaborative docs β all without writing a WebSocket server. Realtime docs β.
Supabase ships an S3-compatible object store. For file uploads inside Supabase apps:
// Create a bucket once in the dashboard: Storage β Create bucket
await supabase.storage
.from("avatars")
.upload(`${user.id}/profile.png`, fileBlob, { upsert: true });
const { data } = supabase.storage
.from("avatars")
.getPublicUrl(`${user.id}/profile.png`);
console.log(data.publicUrl); // ready to put in an <img>
If you're already on AWS or want zero egress, see Upload files to S3 or Cloudflare R2. Otherwise Supabase Storage is the easiest path within the same project.
For real projects, develop against a local Supabase instance, not production:
brew install supabase/tap/supabase # or via npm: npm i -g supabase
supabase init # in your project root
supabase start # spins up Postgres + Studio + Auth in Docker
Run migrations as SQL files in supabase/migrations/. Push to production with supabase db push. CLI docs β.
ALTER TABLE foo ENABLE ROW LEVEL SECURITY; immediately after creating a user-data table.
Free tier: 500 MB database, 50,000 monthly active users, 1 GB file storage, 5 GB bandwidth, 2 free projects. Pauses inactive projects after a week of no requests (you can wake them up; not destroyed).
Pro tier: $25/mo β 8 GB DB, 100K MAU, 100 GB storage, projects never pause. Supabase pricing β.
Self-hosted: full source is open. Self-hosting docs β. Production self-hosting is real work; most people stay on managed.