For freelance projects, Supabase is the closest thing to a cheat code I've found. You get a real Postgres database, authentication, file storage, edge functions, and realtime - all behind one client SDK, all on a free tier that covers most early-stage apps. The combination eliminates the parts of backend work that don't directly serve the client (server provisioning, auth flows, file upload plumbing, deployment pipelines) and lets me ship features that actually justify my invoice. Row Level Security is the underrated headline feature - it pushes access control into the database itself, which means a tiny frontend-heavy team can ship a multi-tenant app safely. I don't use Supabase for everything; some projects genuinely need different tools. But for the 90% of freelance work that looks like "CRUD with auth, file uploads, and a few realtime touches," nothing else gets me from kickoff to launch faster.
When you're freelancing, every hour you spend on backend setup is an hour you're not building features the client actually cares about. Clients don't pay you to provision a database server. They don't pay you to write a password reset flow from scratch. They don't pay you to think about S3 IAM policies or set up nightly backups. They pay you for the thing that solves their problem.
After years of cobbling together my own stacks - Express on Heroku with Postgres on RDS, then Firebase, then Hasura, then back to Postgres on Render - I needed a backend that:
Supabase checks every box. Two years in, I've never wanted to leave.
A real relational database with foreign keys, joins, constraints, views, materialized views, full-text search, JSON columns, and every other Postgres feature that's been battle-tested over thirty years. This matters more than people give it credit for. A "modern" document database lets you ship the first feature faster, then punishes you when your data model evolves. Postgres lets you ship the first feature in a perfectly survivable way, and rewards you when the data model grows up.
Row Level Security (RLS) is the killer feature. You write SQL policies that say "users can only see their own rows," and Postgres enforces it for every query, no matter where it comes from - your React app, your edge function, a SQL console session. That single feature eliminates an entire category of authorization bugs that plague hand-rolled APIs.
Email/password, OAuth (Google, GitHub, Apple, Twitter, Discord, dozens more), magic links, SMS OTP, phone auth, anonymous sessions - all built in. The client SDKs handle session management, refresh tokens, and persistence automatically. A typical login form in my projects is fifteen lines of code, and it's been the same fifteen lines for two years.
The piece I value most is the JWT integration with RLS. Every authenticated request automatically carries the user's identity into the database, which means RLS policies can reference auth.uid() and the right user always gets the right rows.
File uploads with bucket-level access control. Perfect for user avatars, project images, document uploads, and product photos. You can mark a bucket public or private, write RLS-style policies for object access, and the SDK gives you signed URLs with expiry built in. I've shipped projects with gigabytes of user-uploaded files and never had to think about S3 once.
When I need server-side logic - webhook handlers, third-party API calls, scheduled jobs, anything that shouldn't run in the browser - Edge Functions run on Deno at the edge. No server to provision, no Dockerfile to write, no autoscaling to configure. I write a function, run supabase functions deploy, and it's live in seconds.
The Deno runtime means I get TypeScript natively, modern ESM imports, and a much smaller cold-start surface than the equivalent Node Lambda would have.
For apps that need live updates - chat, dashboards, collaborative tools, multi-user forms - Supabase Realtime broadcasts database changes over WebSockets. You subscribe to a table or a query, and your React state updates the instant the row changes anywhere in the world. The same RLS policies apply, so users only get notified about rows they're allowed to see.
In the last year Supabase added native cron scheduling (via pg_cron) and vector search (via pg_vector). I now keep all my scheduled jobs and most of my AI embeddings in the same Postgres instance as the rest of the app data. Fewer moving parts, simpler permissions, lower bill.
The path from "kickoff call" to "client clicking a working app" usually looks like this:
supabase db diff to capture migrations once the schema settles.supabase gen types typescript. The generated types make end-to-end type safety work without a single hand-written model.@supabase/supabase-js from the React app. One client instance, imported wherever I need data.A simple authenticated fetch ends up looking like this:
const { data: posts } = useQuery({
queryKey: ['posts'],
queryFn: async () => {
const { data, error } = await supabase
.from('posts')
.select('id, title, body, created_at, profiles(name)')
.order('created_at', { ascending: false });
if (error) throw error;
return data;
},
});
Notice what's missing: no auth header to attach manually, no permission filtering in JavaScript, no error mapping. RLS handles the permission. The session token is attached automatically. TanStack Query handles caching, retries, and stale-while-revalidate behavior.
Here's a policy for a multi-tenant SaaS where each user can only read posts they own:
alter table posts enable row level security;
create policy "users can read their own posts"
on posts for select
using (auth.uid() = user_id);
create policy "users can insert their own posts"
on posts for insert
with check (auth.uid() = user_id);
That's it. The frontend doesn't need a "where user_id = current_user" filter. The backend doesn't need a middleware checking ownership. The database enforces it for every read and write, end of story.
For multi-org SaaS the policies get slightly more involved (you usually join through a memberships table), but the pattern stays the same: the rule lives once, in the database, and no client can bypass it.
Most freelance projects fit comfortably in the free tier: 500MB database, 1GB storage, 50,000 monthly active users, 5GB egress. That's enough for an early-stage SaaS, an MVP, an internal tool, or a portfolio-style site with user uploads.
When projects outgrow the free tier, the Pro plan is $25/month, which includes 8GB of database, 100GB of storage, 100,000 MAU, and daily backups. For comparison, the equivalent self-hosted setup on AWS (RDS db.t4g.small + S3 + Cognito + Lambda) runs around $80-120/month before you've written a single migration or done a single deploy.
I almost always include a line item in proposals: "Hosting and backend: approximately $25/month, billed to your account directly." Clients appreciate the transparency, and the number is small enough that no one balks.
It's not a universal answer. There are projects where I reach for something else:
For everything else - and that's 90% of freelance work - Supabase is the answer.
Three things took me longer than they should have to learn:
1. Enable RLS the moment you create a table. Always. Even on prototypes. Even on internal tools. The cost is one minute; the cost of forgetting is a data breach.
2. Generate types and use them. Don't hand-write your data models. The Supabase CLI's type generation makes your IDE catch schema mismatches at compile time, which is worth more than any documentation.
3. Use migrations, not the SQL editor. It's tempting to make schema changes in the dashboard. Don't. Use supabase db diff to capture them as migrations, commit them to git, and apply them per environment. Future-you, trying to spin up a new environment, will thank present-you.
Supabase is the rare tool that makes you faster without making you dumber. It encodes good defaults (Postgres, RLS, edge runtime, generated types) without forcing you into them. That's the bar I now hold every other backend tool to.