Skip to content

Building Resilient Apps with Supabase and Next.js

Dan Slay
Dan Slay
Founder
| 4 min read Engineering 10 November 2025 · Updated 21 February 2026

We’ve built half a dozen production SaaS products on the Supabase + Next.js stack. Some handle tens of thousands of users daily. Here are the patterns that have served us well — and the mistakes we’ve learned to avoid.

The Stack

Our go-to production stack for new SaaS products:

  • Next.js 15 with App Router
  • Supabase for auth, database, and real-time
  • Tailwind CSS for styling
  • Vercel for deployment
  • Resend for transactional email

This isn’t a tutorial stack. These are the tools we use in production, every day.

Pattern 1: Server-First Data Fetching

Stop fetching data on the client when you don’t need to. Server Components in Next.js let you query Supabase directly on the server, which means faster page loads and no loading spinners.

// app/dashboard/page.tsx — Server Component
import { createClient } from '@/lib/supabase/server';

export default async function DashboardPage() {
  const supabase = await createClient();

  const { data: projects } = await supabase
    .from('projects')
    .select('id, name, created_at, status')
    .order('created_at', { ascending: false });

  return (
    <div>
      {projects?.map(project => (
        <ProjectCard key={project.id} project={project} />
      ))}
    </div>
  );
}

No useEffect. No loading state. No client-side waterfall. The data is there when the page loads.

Pattern 2: Proper Error Boundaries

Every Supabase call can fail. Network issues, rate limits, auth expiry — handle them all gracefully.

// A simple wrapper we use in every project
async function query<T>(
  fn: () => Promise<{ data: T | null; error: any }>
): Promise<T> {
  const { data, error } = await fn();

  if (error) {
    console.error('Supabase error:', error.message);
    throw new Error(error.message);
  }

  if (!data) {
    throw new Error('No data returned');
  }

  return data;
}

// Usage
const projects = await query(() =>
  supabase.from('projects').select('*')
);

Pattern 3: Row-Level Security From Day One

We cannot stress this enough. RLS should be the first thing you configure after creating a table.

-- Every table gets RLS enabled immediately
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- Users can only see their own projects
CREATE POLICY "select_own_projects" ON projects
  FOR SELECT USING (auth.uid() = owner_id);

-- Users can only insert projects they own
CREATE POLICY "insert_own_projects" ON projects
  FOR INSERT WITH CHECK (auth.uid() = owner_id);

-- Users can only update their own projects
CREATE POLICY "update_own_projects" ON projects
  FOR UPDATE USING (auth.uid() = owner_id);

Pattern 4: Optimistic Updates for Real-Time Feel

For actions where the user expects instant feedback, update the UI before the server confirms:

'use client';

import { useTransition } from 'react';

function ToggleProjectStatus({ project }) {
  const [isPending, startTransition] = useTransition();

  async function handleToggle() {
    // Optimistically update UI
    startTransition(async () => {
      await toggleProjectStatus(project.id);
    });
  }

  return (
    <button
      onClick={handleToggle}
      disabled={isPending}
      className={isPending ? 'opacity-50' : ''}
    >
      {project.status === 'active' ? 'Pause' : 'Activate'}
    </button>
  );
}

Pattern 5: Sensible Migration Strategy

Don’t use the Supabase dashboard to make schema changes in production. Use migrations.

# Create a new migration
supabase migration new add_billing_columns

# This creates a timestamped SQL file you can version control
# Edit it, test locally, then deploy
supabase db push

What We’ve Learned the Hard Way

  • Don’t skip RLS — we said it already but it bears repeating
  • Use database functions for complex queries — they’re faster than multiple API calls
  • Set up monitoring early — Supabase’s built-in metrics are a good start
  • Plan your auth flow before building — retrofitting auth is painful
  • Use Supabase’s built-in cron (pg_cron) for scheduled tasks instead of external services

Need Help Building?

If you’re building a SaaS product on this stack and want experienced hands helping you ship, check out our SaaS Development service or book a discovery call.

supabase nextjs architecture tutorial
Dan Slay

Written by Dan Slay

Founder

Building practical software at Further Forward. Sharing insights on AI, engineering, and what it takes to ship products that actually work.

Enjoyed this article?

Get more insights delivered to your inbox weekly.