Why Supabase Changes the Full-Stack Game
Traditional backend development requires setting up and managing: a database, an auth system, an object store, an API layer, and a real-time layer. Supabase provides all of these through a unified PostgreSQL-based platform. You get the flexibility of raw PostgreSQL with the developer experience of Firebase.
Schema Design with RLS from the Start
Row Level Security is Supabase's killer feature — database-level access control that makes it safe to query Supabase directly from the client.
-- 1. Create your tables
CREATE TABLE posts (
id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
title TEXT NOT NULL,
content TEXT NOT NULL,
status TEXT DEFAULT 'draft' CHECK (status IN ('draft', 'published')),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 2. Enable RLS (REQUIRED — disabled tables are public!)
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- 3. Define access policies
-- Anyone can read published posts
CREATE POLICY "Public posts are viewable by everyone"
ON posts FOR SELECT
USING (status = 'published');
-- Users can only see their own drafts
CREATE POLICY "Users can view their own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- Users can only create posts for themselves
CREATE POLICY "Users can create their own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- Users can only update their own posts
CREATE POLICY "Users can update their own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- Users can only delete their own posts
CREATE POLICY "Users can delete their own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);With these policies, a user querying supabase.from('posts').select('*') automatically only sees rows they're allowed to see. No application-level filtering needed.
Authentication — Magic Links & OAuth
// lib/supabase/client.ts
import { createBrowserClient } from "@supabase/ssr";
import type { Database } from "./types";
export function createClient() {
return createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
// components/auth/LoginForm.tsx
"use client";
import { createClient } from "@/lib/supabase/client";
export function LoginForm() {
const supabase = createClient();
async function handleMagicLink(formData: FormData) {
const email = formData.get("email") as string;
const { error } = await supabase.auth.signInWithOtp({
email,
options: {
emailRedirectTo: `${location.origin}/auth/callback`,
},
});
if (error) console.error(error);
else alert("Check your email for the magic link!");
}
async function handleGoogleLogin() {
await supabase.auth.signInWithOAuth({
provider: "google",
options: {
redirectTo: `${location.origin}/auth/callback`,
},
});
}
return (
<div>
<form action={handleMagicLink}>
<input name="email" type="email" required placeholder="your@email.com" />
<button type="submit">Send Magic Link</button>
</form>
<button onClick={handleGoogleLogin}>Continue with Google</button>
</div>
);
}Server-Side Auth with Next.js App Router
// lib/supabase/server.ts
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";
import type { Database } from "./types";
export async function createServerSupabaseClient() {
const cookieStore = await cookies();
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll: () => cookieStore.getAll(),
setAll: (cookiesToSet) => {
cookiesToSet.forEach(({ name, value, options }) => {
cookieStore.set(name, value, options);
});
},
},
}
);
}
// app/dashboard/page.tsx — protected server component
export default async function DashboardPage() {
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) redirect("/login");
// RLS ensures user only sees their own posts
const { data: posts } = await supabase
.from("posts")
.select("id, title, status, created_at")
.order("created_at", { ascending: false });
return <PostList posts={posts ?? []} />;
}Realtime — Live Comments
"use client";
import { useEffect, useState } from "react";
import { createClient } from "@/lib/supabase/client";
export function LiveComments({ postId }: { postId: string }) {
const [comments, setComments] = useState<Comment[]>([]);
const supabase = createClient();
useEffect(() => {
// Initial load
supabase
.from("comments")
.select("*, users(name, avatar_url)")
.eq("post_id", postId)
.order("created_at")
.then(({ data }) => setComments(data ?? []));
// Subscribe to new comments in real-time
const channel = supabase
.channel(`comments:${postId}`)
.on(
"postgres_changes",
{
event: "INSERT",
schema: "public",
table: "comments",
filter: `post_id=eq.${postId}`,
},
(payload) => {
setComments((prev) => [...prev, payload.new as Comment]);
}
)
.subscribe();
return () => supabase.removeChannel(channel);
}, [postId]);
return (
<div>
{comments.map((comment) => (
<div key={comment.id}>
<img src={comment.users.avatar_url} alt={comment.users.name} />
<p>{comment.content}</p>
</div>
))}
</div>
);
}Edge Functions — Server-Side Logic
Edge Functions run TypeScript at the edge, close to your users:
// supabase/functions/send-welcome-email/index.ts
import { serve } from "https://deno.land/std/http/server.ts";
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
serve(async (req: Request) => {
const { userId, email } = await req.json();
const supabase = createClient(
Deno.env.get("SUPABASE_URL")!,
Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")! // Service role for admin ops
);
// Send email via Resend API
const res = await fetch("https://api.resend.com/emails", {
method: "POST",
headers: {
"Authorization": `Bearer ${Deno.env.get("RESEND_API_KEY")}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
from: "Bipin <hi@bipinbaral.com.np>",
to: email,
subject: "Welcome to my blog!",
html: "<h1>Welcome!</h1><p>Thanks for joining.</p>",
}),
});
return new Response(JSON.stringify({ success: res.ok }), {
headers: { "Content-Type": "application/json" },
});
});Conclusion
Supabase eliminates the infrastructure overhead of building a backend while keeping the full power of PostgreSQL. Row Level Security is the paradigm shift — database-level policies that make client-to-database queries safe. Real-time subscriptions are built into the database via logical replication. Edge Functions handle custom server-side logic at the edge. Together, they make Supabase the most complete full-stack development platform available today.


