Tejasbyte Technologies
Start Project
Tejasbyte
HomeServicesPortfolioBlogAboutContactStart Project
Engineering

Next.js + Supabase: The Full-Stack Setup We Use for Every Client Project

Our opinionated starter: Next.js 15, Supabase, RLS, real-time subscriptions, and a custom admin panel pattern that scales.

EngineeringMay 15, 20269 min read

After building 10+ production apps with Next.js and Supabase, we've settled on an opinionated setup that handles auth, RLS, real-time, and admin panels. Here's the full setup.

Project Structure

src/
  app/
    (auth)/         # Auth pages (login, register)
    (dashboard)/    # Protected dashboard routes
    admin/          # Admin panel (separate layout)
    api/            # API routes
  lib/
    supabase/
      client.ts     # Browser client
      server.ts     # Server client (for RSC)
      middleware.ts # Auth middleware
    db/
      schema.ts     # Zod schemas matching DB
  components/
    ui/             # Reusable components

Supabase Client Setup

// lib/supabase/server.ts — for Server Components and API routes
import { createServerClient } from "@supabase/ssr";
import { cookies } from "next/headers";

export async function createClient() {
  const cookieStore = await cookies();
  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() { return cookieStore.getAll(); },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value, options }) =>
            cookieStore.set(name, value, options)
          );
        },
      },
    }
  );
}

Row Level Security Patterns

RLS is the killer feature. Data security is enforced at the database level — even if your application code has a bug, users can only ever see their own data.

-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- Users can only see their own posts
CREATE POLICY "users_own_posts" ON posts
  FOR ALL USING (auth.uid() = user_id);

-- Published posts are visible to everyone
CREATE POLICY "published_posts_public" ON posts
  FOR SELECT USING (status = 'published');

-- Admin role can see everything
CREATE POLICY "admin_all_access" ON posts
  FOR ALL USING (
    EXISTS (
      SELECT 1 FROM profiles
      WHERE id = auth.uid() AND role = 'admin'
    )
  );

Admin Panel Pattern

We build a custom admin panel using the Supabase service role key (server-side only), which bypasses RLS. Never expose this key to the browser.

// app/admin/layout.tsx — server component
import { createClient } from "@/lib/supabase/server";
import { redirect } from "next/navigation";

export default async function AdminLayout({ children }) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();
  
  if (!user) redirect("/login");
  
  const { data: profile } = await supabase
    .from("profiles")
    .select("role")
    .eq("id", user.id)
    .single();
  
  if (profile?.role !== "admin") redirect("/dashboard");
  
  return <div>{children}</div>;
}

Create a separate Supabase client using SUPABASE_SERVICE_ROLE_KEY for admin operations. This key bypasses RLS and should NEVER be sent to the browser.

Next.jsSupabasePostgreSQLTypeScript
← Back to Blog

Related Posts

Engineering

Why We Stopped Using ORMs for High-Traffic PostgreSQL Queries

Read more →

Engineering

The TypeScript Patterns We Use on Every New Project

Read more →