Grid
FrameworksNextjs

Supabase Forms in Next.js

Connecting React forms to Supabase Postgres with @supabase/ssr, Row Level Security, and real-time synchronization.

Preview

Supabase Postgres + RLS

Supabase User Profile Form

Connect form mutations directly to Postgres with Row Level Security (RLS) enforcement and real-time client upserts.

Row Level Security (RLS) Protected
Interactive responsive preview
Drag handles on right and bottom to resize

Overview

Supabase pairs PostgreSQL with built-in Authentication, Row Level Security (RLS), and instant auto-generated APIs. Using forms with Supabase allows developers to enforce data validation at both the application tier (Zod) and the database tier (Postgres constraints and RLS policies).

Architecture with @supabase/ssr

When building forms in Next.js App Router with Supabase, use @supabase/ssr to ensure cookies and JWT tokens are securely forwarded during mutations:

// lib/supabase/server.ts
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)
          );
        },
      },
    }
  );
}

Upsert Mutation in a Server Action

'use server';

import { createClient } from '@/lib/supabase/server';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';

const profileSchema = z.object({
  username: z.string().min(3),
  fullName: z.string().min(2),
  role: z.enum(['owner', 'engineer', 'designer', 'analyst']),
  bio: z.string().max(160).optional(),
});

export async function updateProfile(formData: FormData) {
  const supabase = await createClient();
  const { data: { user } } = await supabase.auth.getUser();

  if (!user) {
    throw new Error('Unauthorized');
  }

  const payload = profileSchema.parse(Object.fromEntries(formData));

  const { data, error } = await supabase
    .from('profiles')
    .upsert({
      id: user.id,
      ...payload,
      updated_at: new Date().toISOString(),
    })
    .select()
    .single();

  if (error) {
    return { success: false, error: error.message };
  }

  revalidatePath('/profile');
  return { success: true, data };
}

Best Practices for Supabase Forms

  1. Always Enable Row Level Security: Run alter table profiles enable row level security; on all user-facing tables.
  2. Postgres Check Constraints: Complement Zod schemas with SQL constraints (e.g. check (char_length(username) >= 3)).
  3. Optimistic Updates: Use React 19 optimistic hooks to update the UI instantly while Supabase resolves the mutation.

On this page