Next.js + Supabase for Indie Hackers | Tornic

How Indie Hackers can leverage Next.js + Supabase to build faster. Expert guide and best practices.

Why Next.js + Supabase is a fast, focused stack for indie hackers

Indie hackers win by shipping quickly, validating fast, and iterating with confidence. The next.js + supabase stack fits that mindset perfectly: modern React on the frontend, file storage and database on the backend, and batteries-included authentication. You get a unified developer experience that is fast to start and stays maintainable as your project grows from side project to revenue-generating product.

Next.js gives you server components, route handlers, and incremental static regeneration for performance without heavy orchestration. Supabase gives you Postgres with row level security, built-in Auth and Storage, and a world-class dashboard. Together, nextjs-supabase lets solo founders build data-intensive apps, SaaS dashboards, and micro-products without juggling 10 services.

This guide lays out a practical path: how to start, how to structure your app, a sane development workflow, and what a simple deployment looks like. It is written for indie-hackers who want sharp tools with minimal overhead.

Getting started guide

1) Create a Next.js app with the App Router

npx create-next-app@latest my-app --ts --eslint --use-npm
cd my-app
npm install @supabase/supabase-js @supabase/ssr

Enable the App Router if prompted. It simplifies server-first patterns and scales well as your product grows.

2) Create a Supabase project and local development setup

npm install supabase --save-dev
npx supabase init
npx supabase start

This spins up a local Postgres, Studio, and auth for fast feedback. In production, you will create a hosted project on Supabase and connect via environment variables.

3) Connect Next.js to Supabase

Add your keys to .env.local. For local dev, these are in supabase/.env after supabase start. For hosted projects, grab them from the Supabase Dashboard.

SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-public-anon-key

Create a Supabase client helper using the Next.js SSR helpers.

// lib/supabase.ts
import { createServerClient } from '@supabase/ssr'
import { cookies, headers } from 'next/headers'

export function createClient() {
  const cookieStore = cookies()
  const headerStore = headers()

  return createServerClient(
    process.env.SUPABASE_URL!,
    process.env.SUPABASE_ANON_KEY!,
    {
      cookies: {
        get(name: string) {
          return cookieStore.get(name)?.value
        },
      },
      headers: {
        get(name: string) {
          return headerStore.get(name) ?? undefined
        },
      },
    }
  )
}

4) Add authentication

Supabase Auth gives you passwordless, OAuth, and email-password with minimal work. A minimal route handler to sign in via email-link:

// app/api/auth/sign-in/route.ts
import { NextResponse } from 'next/server'
import { createClient } from '@/lib/supabase'

export async function POST(req: Request) {
  const { email } = await req.json()
  const supabase = createClient()
  const { error } = await supabase.auth.signInWithOtp({ email })
  if (error) return NextResponse.json({ error: error.message }, { status: 400 })
  return NextResponse.json({ ok: true })
}

In a server component page, fetch the user and render based on session:

// app/page.tsx
import { createClient } from '@/lib/supabase'

export default async function Home() {
  const supabase = createClient()
  const { data: { user } } = await supabase.auth.getUser()

  return (
    <main>
      {user ? <p>Signed in as {user.email}</p> : <p>Please sign in</p>}
    </main>
  )
}

5) Add your first table and enable Row Level Security

You can iterate quickly with SQL migrations that match how you ship features. Example for a multi-tenant SaaS:

-- supabase/migrations/20240401_init.sql
create table workspaces (
  id uuid primary key default gen_random_uuid(),
  name text not null,
  owner uuid references auth.users(id) not null,
  created_at timestamptz not null default now()
);

create table projects (
  id uuid primary key default gen_random_uuid(),
  workspace_id uuid not null references workspaces(id) on delete cascade,
  name text not null,
  created_at timestamptz not null default now()
);

alter table workspaces enable row level security;
alter table projects enable row level security;

-- Users can read and write rows in workspaces they belong to
create policy "workspace_members_read"
  on workspaces for select
  using (exists (
    select 1 from workspace_members wm
    where wm.workspace_id = workspaces.id
      and wm.user_id = auth.uid()
  ));

-- You can also add members table and write policies accordingly
create table workspace_members (
  workspace_id uuid references workspaces(id) on delete cascade,
  user_id uuid not null references auth.users(id),
  role text not null default 'member',
  primary key (workspace_id, user_id)
);

create policy "projects_isolation"
  on projects for select
  using (exists (
    select 1 from workspace_members wm
    where wm.workspace_id = projects.workspace_id
      and wm.user_id = auth.uid()
  ));

Test policies locally using the Supabase Studio SQL editor or Supabase CLI. Your default posture should be to enable RLS on every table, then write explicit policies that encode your business rules.

Architecture recommendations

Organize by feature, not by layer

In Next.js App Router, co-locate server actions, route handlers, and UI per feature. A simple pattern:

app/
  dashboard/
    page.tsx            // server component
    actions.ts          // server-only mutations
    data.ts             // server-only queries
  api/
    billing/
      route.ts          // webhooks or callbacks
lib/
  supabase.ts
  auth.ts

This keeps mental overhead low and helps you refactor without breaking boundaries.

Prefer server-side data access

Use server components or route handlers to query Supabase. You avoid shipping secrets to the client and reduce client bundle size. Client components should mostly handle interaction and optimistic UI. For public data, use Next.js caching and revalidation with fetch and tagged routes.

Model multi-tenancy early

Even if you are solo, plan for multiple workspaces. Store a workspace_id on every domain table and enforce isolation with RLS. Add workspace_members with roles like owner, admin, member. This makes pricing, access control, and invitations straightforward when you start selling.

Use Postgres features you already have

  • Unique constraints ensure idempotency for webhooks and background tasks.
  • Generated columns and small SQL views keep complex reads fast without over-engineering your app code.
  • JSONB for unstructured data in early versions. Migrate to normalized tables when patterns stabilize.
  • Supabase Storage for user uploads with RLS-secured buckets mapped to workspaces.

When to add an API layer

If your roadmap includes a public API, add a thin route-handler layer now. Expose only what you need, and keep it authenticated with service roles or OAuth if partners need access. For internal server-to-server calls, use service-role keys only on the server side in route handlers or server actions, never in the browser.

Development workflow

Migration-first development

Every feature that touches data should ship with a migration and a seed. Use the CLI so you can create, review, and roll back changes deterministically:

npx supabase migration new add-billing-tables
# edit SQL file under supabase/migrations
npx supabase db reset  # run from scratch locally with seeds

Add a lightweight seed script:

-- supabase/seed.sql
insert into workspaces (id, name, owner)
values ('00000000-0000-0000-0000-000000000001', 'Demo', '00000000-0000-0000-0000-000000000002');

Server-first testing

  • Write unit tests for server actions and route handlers using Vitest or Jest.
  • Run Playwright for high-value flows like sign in, workspace creation, and billing.
  • For RLS, create test helpers that sign in programmatically and assert policy behavior.

Feature flags and preview environments

Use environment variables and a feature_flags table to control access to experimental features. On every PR, spin up preview deployments and point to a staging Supabase project. Keep a strong rule: production migrations only after staging passes end-to-end tests.

Automating repeatable CLI flows

As a solo founder, you cannot afford flaky release steps. Connect your Supabase and Next.js CLIs into a single, deterministic pipeline. With Tornic, you can chain commands like supabase migration up, npm run test, and vercel deploy --prod into a plain-English workflow that runs the same way every time, with clear logs and no surprise bills.

Deployment strategy

Hosting and environments

  • Frontend: Deploy to Vercel for first-class Next.js support. Use the App Router and serverless or edge runtimes as needed.
  • Database and auth: Host on Supabase with separate projects for staging and production. Keep each project's keys isolated.
  • Environment variables: Store SUPABASE_URL and SUPABASE_ANON_KEY in Vercel environment settings. For server-only tasks, store SUPABASE_SERVICE_ROLE_KEY as a secret accessed only in route handlers or background jobs.

Build and release checklist

  1. Open a PR tied to a migration. Include screenshots or short videos for UI-heavy changes.
  2. Run unit and E2E tests on CI against a staging Supabase project.
  3. Apply migrations in staging using the CLI, verify data and RLS behavior.
  4. Promote to production by running the same migration and deploy steps. Avoid manual dashboard edits.

Caching and performance

  • Serve dashboards via server components to reduce client JS, cache stable queries with revalidateTag or time-based revalidation.
  • Use select columns explicitly instead of select * to limit payloads.
  • For high-traffic public pages, use Next.js static generation with incremental regeneration.

Background jobs

  • Scheduled tasks: Supabase Scheduled Functions for cron-like jobs such as trial expirations.
  • Webhooks: Handle Stripe or external provider webhooks in Next.js route handlers, validate signatures, and store idempotency keys in Postgres.
  • Long running work: Queue patterns with a jobs table plus a worker function that polls. Keep an index on status and run_at.

Observability and ops

  • Log structured JSON from route handlers and server actions. Surface request IDs in responses.
  • Enable database logs in Supabase and set alerts for slow queries or error rates.
  • Add a simple health endpoint and uptime monitoring early.

Practical examples and use cases

  • Micro-SaaS dashboards: Multi-tenant workspaces, per-project metrics, and subscription gates. Use RLS for isolation and Next.js streaming server components for fast page loads.
  • Public directories: Store submissions in Postgres, moderate via policies and roles, render public pages with ISR, and allow uploads via Supabase Storage.
  • Knowledge products: Gate downloads by workspace role, tag content for access control, and track usage with lightweight event tables.

For variations on this stack, compare approaches in React + Firebase for Startup Founders | Tornic and see how your constraints change with different tradeoffs. If you are collaborating with clients, explore Next.js + Supabase for Agencies | Tornic for team-oriented workflows.

Conclusion

next.js + supabase gives indie-hackers a small, sharp stack that covers authentication, data, file storage, and fast rendering out of the box. The App Router and server-first patterns keep complexity low while allowing serious scale. Supabase adds policies, migrations, and a modern dashboard so you can ship production-grade features without building infrastructure from scratch.

If you want a deeper dive on pricing, metrics, and onboarding patterns that pair well with this stack, read SaaS Fundamentals: A Complete Guide | Tornic. When you are ready to automate your release process, wire your CLI steps into a single source of truth so every deploy is crisp and repeatable.

FAQ

Should I use the Next.js App Router or Pages Router with Supabase?

Use the App Router for new projects. Server components and route handlers make server-first data fetching straightforward, reduce client bundle size, and improve cacheability. If you have an existing Pages app, migrate feature by feature rather than all at once.

How do I handle multi-tenancy securely with RLS?

Add a workspace_id column to domain tables, a workspace_members join table, and write policies that check auth.uid() is a member of the workspace. Keep a small set of roles and use SQL policies per operation, for example separate select and insert policies. Test with signed-in users in staging before production.

What is the best way to run background jobs?

Start with Supabase Scheduled Functions for cron-style tasks and route handlers for webhooks. For retries and idempotency, use a jobs table with unique constraints and a worker function that updates statuses in a transaction. Avoid premature adoption of external queues unless you hit scale bottlenecks.

How do I prevent surprise costs?

Set quotas and alerts in Supabase, enable Next.js caching to reduce redundant queries, and keep an eye on log volume. Prefer server components to avoid chatty client-side calls. Use a deterministic CI pipeline so you do not trigger duplicate deployments or migrations.

Can I adopt this stack for agencies or freelancers?

Yes. The same patterns apply with team conventions and client isolation. See Next.js + Supabase for Freelancers | Tornic and Next.js + Supabase for Startup Founders | Tornic for variations tailored to different roles.

Ready to get started?

Start automating your workflows with Tornic today.

Get Started Free