Backend

Supabase Guide: Building a Modern Backend Without the Pain

Ahmet BulutAhmet Bulut
April 27, 2026
10 dk

A friend pinged me last week. "I need to ship an MVP in three days and I haven't written a backend yet." My reply was one line. Open Supabase, start.

Three days later the product was live. Auth worked, the database was clean, file uploads were in. I have been writing backend code for six years and this speed still surprises me. Supabase makes backend boring in the best way for new builders, and removes the repetitive parts for experienced ones.

So here is what we are doing. I will explain what Supabase actually is in plain English, walk you through your first project step by step, and warn you about the hidden traps that make most beginners give up. Read the RLS section twice. That is where 9 out of 10 people get stuck on day one.

What Supabase actually is

One sentence: Supabase is an open source Firebase alternative. But that sentence is misleading because the underlying tech is very different. Firebase uses a NoSQL document database. Supabase is built entirely on PostgreSQL. That means relational, SQL queryable, transactional, a real database.

On top of that the Supabase team layered authentication, row level security, file storage, realtime data streams, edge functions, and auto generated REST and GraphQL APIs. All of it managed through one dashboard.

Why do people pick Supabase? A few reasons, and I have lived through each one. There is no vendor lock-in; moving away to your own PostgreSQL server is an afternoon of work. The free tier is genuinely usable, with 500 MB database, 50k monthly active users, and 1 GB storage. That is enough for most MVPs. And the most important reason for me, SQL is back. Writing queries in Firebase makes you feel like you are in middle school. In Supabase you sit down and write proper joins.

Your first project in five minutes

Do not treat that as marketing copy. It really is five minutes. I timed myself while writing this. Three minutes and 40 seconds.

Go to supabase.com and sign in with GitHub. Click "New project". Pick a name, a strong database password (write this down somewhere right now, recovering it later is painful), and a region close to you. If you are in Europe, Frankfurt or London makes sense.

While the project provisions (about two minutes) run this in your terminal:

npm create vite@latest my-app -- --template react-ts
cd my-app
npm install @supabase/supabase-js

Once the project is ready, go to Settings → API and copy two things: Project URL and anon public key. Now create src/lib/supabase.ts:

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

const url = import.meta.env.VITE_SUPABASE_URL const anonKey = import.meta.env.VITE_SUPABASE_ANON_KEY

export const supabase = createClient(url, anonKey)

Congratulations, you have a backend. I am not joking. Everything from here on is creating tables and querying data.

The real power: PostgreSQL

The Supabase dashboard has a Table Editor where you can click your way to a table. Skip it. From day one, get used to the SQL Editor. Because if you create the table in code, you can rerun the same SQL when you move to production. Clicks vanish.

Let us build a simple tasks table:

create table tasks (
  id uuid primary key default gen_random_uuid(),
  title text not null,
  is_done boolean default false,
  user_id uuid references auth.users not null,
  created_at timestamptz default now()
);

create index tasks_user_id_idx on tasks(user_id);

Three things to notice. The user_id column links directly to the built in auth.users table, which is the standard way to associate data with a user. Adding an index looks optional, but you will regret skipping it once you cross 10k rows. Using gen_random_uuid() instead of integer IDs is also worth it, especially when multiple clients generate data offline.

To fetch data:

const { data, error } = await supabase
  .from('tasks')
  .select('*')
  .order('created_at', { ascending: false })

You will most likely get an empty array back. You added rows, you can see them in the dashboard, but the code returns nothing. Welcome to RLS.

Auth and RLS: the biggest trap

Row Level Security is Supabase's most powerful feature, and the most confusing one. Let me back up.

Supabase expects you to embed the anon public key in the browser. Technically anyone can hit your database with that key. So how is it safe? Through RLS. PostgreSQL's row level access control filters every request based on who the user is. If RLS is enabled and you have not written any policy, nothing comes back. So your first failure was probably this: you created the table, RLS got enabled automatically, and because you wrote no rule, no data returns.

Add these policies:

alter table tasks enable row level security;

create policy "Users can read their own tasks" on tasks for select using (auth.uid() = user_id);

create policy "Users can insert their own tasks" on tasks for insert with check (auth.uid() = user_id);

create policy "Users can update their own tasks" on tasks for update using (auth.uid() = user_id);

Now for auth. Sign up with email and password:

const { data, error } = await supabase.auth.signUp({
  email: '[email protected]',
  password: 'a-strong-password-123'
})

When a user signs up Supabase sends a confirmation email by default. During development this gets annoying; you can temporarily disable "Confirm email" under Authentication → Settings, but turn it back on for production. Magic links, Google, GitHub, and Apple sign in all flow through the same API and take a few clicks to enable. If you are working with a team on the same project, managing auth providers from a shared workspace is smart, since giving Supabase dashboard access to only one person is a common bottleneck.

Storage: where your files live

Profile pictures, PDF reports, audio recordings, whatever you have, Supabase Storage gives you an S3-like bucket. Create a bucket from the dashboard (say avatars), decide if it is public or private.

Uploading is this simple:

const file = e.target.files[0]
const { data, error } = await supabase.storage
  .from('avatars')
  .upload(${userId}/avatar.png, file, {
    upsert: true
  })

Storage has its own RLS policies. Beginners typically manage to upload, then hit "unauthorized" when downloading. The bucket is private and the read policy is missing. Just like with tables, you write policies on storage.objects. Once you grasp this, you start to appreciate how consistent Supabase is. Everything leans on the same security model.

Realtime and Edge Functions in one breath

Realtime listens to PostgreSQL's WAL (write ahead log) and pushes changes to clients over WebSocket. So if you want a task added by one user to appear instantly on another user's screen, three lines do it:

supabase
  .channel('tasks-changes')
  .on('postgres_changes', 
    { event: '*', schema: 'public', table: 'tasks' },
    payload => console.log('Change:', payload)
  )
  .subscribe()

Edge Functions are serverless functions running on Deno. Use them for things that must happen on a server: receiving Stripe webhooks, calling OpenAI without exposing your secret key, triggering emails. You write them locally with the CLI and ship with supabase functions deploy. Once you go to production these become non negotiable. You cannot do everything from the client.

The real mistakes beginners make

Buckle in, this part will save you actual time. There are three main mistakes and each one cost me at least one late night.

The first mistake is enabling RLS without writing policies. You will burn hours debugging code that is not the problem. The fix: as soon as you create a table, write select, insert, update, and delete policies. Do not say "I'll add them later". You will forget.

The second mistake is mixing up the anon key and the service role key. The service role bypasses all RLS. If you accidentally ship that to your frontend, your database is wide open. Use service role only on the server or inside an Edge Function. If you want to keep access reports, tracking which key your team uses where is genuinely worth the effort.

The third mistake is creating tables without foreign keys. Then you try to join later, performance tanks, and your data becomes inconsistent. PostgreSQL is a relational database, lean into the relations. references is your friend.

A bonus fourth: not tracking migrations. People click through the dashboard to create tables, then panic when they cannot replicate the schema in another environment. Install the Supabase CLI, generate migration files with supabase db diff, and commit them to git. Future you will be grateful.

What changes when you work with a team

Solo, Supabase flows nicely. Add two more people and things get tangled. Who changed which table? Which policy got broken? What is the migration order? In that situation a project management tool reduces the chaos by tying every schema change to a task and giving code review a place to live.

That is exactly why we built Poitim. The backend engineer writes a Supabase migration while the frontend engineer waits on the same linked task for the client side change. Seeing deploy days on a sprint calendar keeps communication clean. If you want a setup like that for your team, take a look at the demo.

The best way to learn Supabase is to pick a small project and put it live on day one. Do not aim for perfection. A note app, a reading list, a tiny blog. Ship, then improve based on real feedback. That window where the backend is invisible and you only think about the product, that is precious. Earn it.

Frequently Asked Questions

Supabase Guide: Modern Backend for New Builders