ANTIGRAVITY LABJP
Articles/Integrations
Integrations/2026-04-04Advanced

Antigravity × Supabase Complete Production Guide — Mastering Auth, RLS, Edge Functions, and Realtime for Real-World Apps

A comprehensive guide to building production-grade apps with Antigravity and Supabase. Learn Auth, RLS, Edge Functions, and Realtime channel implementation patterns to master scalable full-stack development.

supabase7antigravity429authrlsedge-functionsrealtime4fullstack5production71

Setup and context — Why Antigravity × Supabase Is a Powerful Combination

For independent developers, backend architecture has always been one of the biggest challenges. Authentication, databases, file storage, real-time communication — building and securely managing all of these from scratch is enormously time-consuming and expensive.

Supabase elegantly solves this problem. As an open-source Backend as a Service (BaaS) platform built on PostgreSQL with integrated auth, storage, Edge Functions, and Realtime, Supabase has gained rapid adoption heading into 2026.

When combined with Antigravity, Supabase development becomes dramatically faster. Antigravity's AI deeply understands your project context and provides consistent, intelligent support — from schema design and RLS policy generation to Edge Function coding — all within a single workflow.

In this guide, we'll systematically explore practical patterns for getting the most out of Supabase with Antigravity, complete with production-ready code examples. We go beyond "how to connect them" to cover the architectural thinking behind scalable, maintainable systems.

Who This Guide Is For

  • Next.js / React developers who want to use Supabase in production
  • Developers who want to tackle secure multi-tenant design with RLS
  • Those who want to accelerate Supabase project development with Antigravity
  • Developers building real-time AI features by combining Edge Functions and Realtime

Chapter 1: Project Setup and Antigravity Initial Configuration

Preparing Your Supabase Project

Start by installing the Supabase CLI and setting up a local development environment. Run the following in Antigravity's terminal:

# Install Supabase CLI
npm install supabase --save-dev
 
# Initialize the project
npx supabase init
 
# Start local Supabase (requires Docker)
npx supabase start

After startup, you'll see output like this:

Started supabase local development setup.

         API URL: http://127.0.0.1:54321
     GraphQL URL: http://127.0.0.1:54321/graphql/v1
  S3 Storage URL: http://127.0.0.1:54321/storage/v1/s3
          DB URL: postgresql://postgres:postgres@127.0.0.1:54322/postgres
      Studio URL: http://127.0.0.1:54323
    Inbucket URL: http://127.0.0.1:54324
      JWT secret: super-secret-jwt-token-with-at-least-32-characters-long
        anon key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
service_role key: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Configuring Antigravity's RULES.md

Set up your RULES.md (or .antigravity/rules.md) so Antigravity can accurately understand your project context:

# Project Rules
 
## Tech Stack
- Next.js 15+ App Router
- Supabase (PostgreSQL + Auth + Storage + Realtime)
- TypeScript (strict mode)
- Tailwind CSS
 
## Supabase Conventions
- Table names: snake_case (plural)
- Column names: snake_case
- RLS must be enabled on all tables
- service_role key is server-side only
 
## Security Rules
- Client-side code uses anon key only
- All user data access must be controlled by RLS
- service_role is only used inside Edge Functions

With this configuration, Antigravity will automatically apply security best practices during code generation.

Setting Up the TypeScript Client

// lib/supabase/client.ts — for client components
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/types/supabase'
 
export function createClient() {
  return createBrowserClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}
 
// lib/supabase/server.ts — for server components and Server Actions
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import type { Database } from '@/types/supabase'
 
export async function createClient() {
  const cookieStore = await cookies()
  return createServerClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Safe to ignore when called from Server Components
          }
        },
      },
    }
  )
}

By keeping these files in view, Antigravity will correctly choose the right client in all subsequent code generation.


Chapter 2: Full Supabase Auth Implementation

Designing the Authentication Flow

Supabase Auth provides JWT-based authentication with support for email/password, magic links, and OAuth (Google, GitHub, etc.). Here's the recommended pattern for Next.js App Router:

// app/auth/callback/route.ts — OAuth callback handler
import { createClient } from '@/lib/supabase/server'
import { NextResponse } from 'next/server'
 
export async function GET(request: Request) {
  const { searchParams, origin } = new URL(request.url)
  const code = searchParams.get('code')
  const next = searchParams.get('next') ?? '/'
 
  if (code) {
    const supabase = await createClient()
    const { error } = await supabase.auth.exchangeCodeForSession(code)
    if (!error) {
      return NextResponse.redirect(`${origin}${next}`)
    }
  }
 
  // On error, redirect to error page
  return NextResponse.redirect(`${origin}/auth/error`)
}

Session Management with Middleware

// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'
 
export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })
 
  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          )
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )
 
  // Refresh session (important: always call this)
  const {
    data: { user },
  } = await supabase.auth.getUser()
 
  // Redirect unauthenticated users away from protected routes
  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }
 
  return supabaseResponse
}
 
export const config = {
  matcher: [
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

Antigravity understands this middleware pattern, meaning a simple instruction like "add an auth guard to the dashboard" will produce the right configuration automatically.

Extending User Profiles

Since auth.users can't be modified directly, user profile data is best managed in a public.profiles table:

-- supabase/migrations/20260404_create_profiles.sql
 
-- Create profiles table
create table public.profiles (
  id uuid references auth.users on delete cascade not null primary key,
  username text unique,
  full_name text,
  avatar_url text,
  subscription_tier text default 'free' check (subscription_tier in ('free', 'pro', 'premium')),
  created_at timestamptz default now() not null,
  updated_at timestamptz default now() not null
);
 
-- Enable RLS (required)
alter table public.profiles enable row level security;
 
-- Policy: users can only view their own profile
create policy "Users can view own profile"
  on public.profiles
  for select
  using (auth.uid() = id);
 
-- Policy: users can only update their own profile
create policy "Users can update own profile"
  on public.profiles
  for update
  using (auth.uid() = id);
 
-- Trigger to automatically create a profile on user signup
create or replace function public.handle_new_user()
returns trigger as $$
begin
  insert into public.profiles (id, full_name, avatar_url)
  values (
    new.id,
    new.raw_user_meta_data->>'full_name',
    new.raw_user_meta_data->>'avatar_url'
  );
  return new;
end;
$$ language plpgsql security definer;
 
create trigger on_auth_user_created
  after insert on auth.users
  for each row execute procedure public.handle_new_user();

When having Antigravity create this migration, simply say "create a schema that automatically generates a profile on new user signup and secures it with RLS" — and it will produce equivalent code.


Chapter 3: Practical RLS Implementation Patterns

Why RLS Is Non-Negotiable

RLS is the heart of Supabase security. By enforcing access control at the database level, it eliminates the risk of data leaks caused by application code bugs.

When designing RLS with Antigravity, start by describing your access requirements in natural language. Antigravity will generate the right policies based on your description.

RLS Patterns for Multi-Tenant SaaS

-- supabase/migrations/20260404_create_teams.sql
 
-- Teams table
create table public.teams (
  id uuid default gen_random_uuid() primary key,
  name text not null,
  owner_id uuid references auth.users not null,
  plan text default 'free' check (plan in ('free', 'pro', 'enterprise')),
  created_at timestamptz default now() not null
);
 
-- Team members junction table
create table public.team_members (
  team_id uuid references public.teams on delete cascade not null,
  user_id uuid references auth.users on delete cascade not null,
  role text default 'member' check (role in ('owner', 'admin', 'member')),
  joined_at timestamptz default now() not null,
  primary key (team_id, user_id)
);
 
-- Projects table (scoped to a team)
create table public.projects (
  id uuid default gen_random_uuid() primary key,
  team_id uuid references public.teams on delete cascade not null,
  name text not null,
  description text,
  status text default 'active' check (status in ('active', 'archived')),
  created_at timestamptz default now() not null,
  updated_at timestamptz default now() not null
);
 
-- Enable RLS
alter table public.teams enable row level security;
alter table public.team_members enable row level security;
alter table public.projects enable row level security;
 
-- Helper: check if current user is a team member
create or replace function public.is_team_member(team_id uuid)
returns boolean as $$
  select exists (
    select 1 from public.team_members
    where team_members.team_id = $1
    and team_members.user_id = auth.uid()
  )
$$ language sql security definer stable;
 
-- Helper: check if current user is a team admin
create or replace function public.is_team_admin(team_id uuid)
returns boolean as $$
  select exists (
    select 1 from public.team_members
    where team_members.team_id = $1
    and team_members.user_id = auth.uid()
    and team_members.role in ('owner', 'admin')
  )
$$ language sql security definer stable;
 
-- Teams policies
create policy "Team members can view their teams"
  on public.teams for select
  using (public.is_team_member(id));
 
create policy "Team owners can update team"
  on public.teams for update
  using (owner_id = auth.uid());
 
-- Projects policies
create policy "Team members can view projects"
  on public.projects for select
  using (public.is_team_member(team_id));
 
create policy "Team admins can create projects"
  on public.projects for insert
  with check (public.is_team_admin(team_id));
 
create policy "Team admins can update projects"
  on public.projects for update
  using (public.is_team_admin(team_id));

Type-Safe Queries with RLS

// lib/supabase/queries/projects.ts
import { createClient } from '@/lib/supabase/server'
import type { Database } from '@/types/supabase'
 
type Project = Database['public']['Tables']['projects']['Row']
 
export async function getProjectsByTeam(teamId: string): Promise<Project[]> {
  const supabase = await createClient()
 
  const { data, error } = await supabase
    .from('projects')
    .select(`
      *,
      team:teams(name, plan)
    `)
    .eq('team_id', teamId)
    .eq('status', 'active')
    .order('created_at', { ascending: false })
 
  if (error) {
    console.error('Failed to fetch projects:', error)
    throw new Error(error.message)
  }
 
  // RLS automatically ensures only authorized projects are returned
  return data
}

Chapter 4: Supabase Edge Functions

When to Use Edge Functions

Edge Functions run on the Deno runtime and shine in these scenarios:

  • Receiving and processing Stripe Webhooks
  • Calling OpenAI / Gemini APIs (keeping API keys server-side)
  • Executing complex business logic
  • Integrating third-party APIs

Generating an Edge Function with Antigravity

Tell Antigravity: "Create an Edge Function that receives Stripe Webhooks and updates the user's plan when payment is complete." It will generate something like this:

// supabase/functions/stripe-webhook/index.ts
import Stripe from 'npm:stripe@14'
import { createClient } from 'npm:@supabase/supabase-js@2'
 
const stripe = new Stripe(Deno.env.get('STRIPE_SECRET_KEY')!, {
  httpClient: Stripe.createFetchHttpClient(),
})
 
const supabase = createClient(
  Deno.env.get('SUPABASE_URL')!,
  Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! // service_role is safe inside Edge Functions
)
 
Deno.serve(async (req) => {
  const signature = req.headers.get('stripe-signature')!
  const body = await req.text()
 
  let event: Stripe.Event
  try {
    event = await stripe.webhooks.constructEventAsync(
      body,
      signature,
      Deno.env.get('STRIPE_WEBHOOK_SECRET')!
    )
  } catch (err) {
    console.error('Webhook signature verification failed:', err)
    return new Response('Invalid signature', { status: 400 })
  }
 
  if (event.type === 'checkout.session.completed') {
    const session = event.data.object as Stripe.Checkout.Session
    const userId = session.metadata?.user_id
    const plan = session.metadata?.plan
 
    if (userId && plan) {
      const { error } = await supabase
        .from('profiles')
        .update({
          subscription_tier: plan,
          updated_at: new Date().toISOString(),
        })
        .eq('id', userId)
 
      if (error) {
        console.error('Failed to update subscription:', error)
        return new Response('Database error', { status: 500 })
      }
    }
  }
 
  if (event.type === 'customer.subscription.deleted') {
    const subscription = event.data.object as Stripe.Subscription
    const userId = subscription.metadata?.user_id
 
    if (userId) {
      await supabase
        .from('profiles')
        .update({ subscription_tier: 'free', updated_at: new Date().toISOString() })
        .eq('id', userId)
    }
  }
 
  return new Response(JSON.stringify({ received: true }), {
    headers: { 'Content-Type': 'application/json' },
  })
})

Deploying and Managing Edge Functions

# Serve locally for development
npx supabase functions serve stripe-webhook --env-file .env.local
 
# Deploy to production
npx supabase functions deploy stripe-webhook --no-verify-jwt
 
# Set secrets
npx supabase secrets set STRIPE_SECRET_KEY=sk_live_...
npx supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_...

Chapter 5: Real-Time Features with Supabase Realtime

How Supabase Realtime Works

Supabase Realtime is a WebSocket-based change notification system. Leveraging PostgreSQL's logical replication, it delivers database changes to clients in real time. There are three modes:

  • Broadcast: Low-latency message delivery between clients
  • Presence: Managing online user state
  • Postgres Changes: Receiving database changes in real time

Building an AI Chat App with Realtime

When implementing AI chat with Antigravity, using Realtime lets you elegantly handle streaming responses:

// hooks/useRealtimeChat.ts
'use client'
 
import { useState, useEffect, useCallback } from 'react'
import { createClient } from '@/lib/supabase/client'
import type { RealtimeChannel } from '@supabase/supabase-js'
 
interface Message {
  id: string
  content: string
  role: 'user' | 'assistant'
  created_at: string
}
 
export function useRealtimeChat(conversationId: string) {
  const [messages, setMessages] = useState<Message[]>([])
  const [isStreaming, setIsStreaming] = useState(false)
  const supabase = createClient()
 
  useEffect(() => {
    // Fetch existing messages
    const fetchMessages = async () => {
      const { data } = await supabase
        .from('messages')
        .select('*')
        .eq('conversation_id', conversationId)
        .order('created_at', { ascending: true })
      if (data) setMessages(data)
    }
    fetchMessages()
 
    // Set up Realtime subscription
    const channel: RealtimeChannel = supabase
      .channel(`conversation:${conversationId}`)
      .on(
        'postgres_changes',
        {
          event: 'INSERT',
          schema: 'public',
          table: 'messages',
          filter: `conversation_id=eq.${conversationId}`,
        },
        (payload) => {
          const newMessage = payload.new as Message
          setMessages((prev) => [...prev, newMessage])
          if (newMessage.role === 'assistant') {
            setIsStreaming(false)
          }
        }
      )
      .subscribe()
 
    return () => {
      supabase.removeChannel(channel)
    }
  }, [conversationId, supabase])
 
  const sendMessage = useCallback(async (content: string) => {
    setIsStreaming(true)
 
    // Save user message to DB
    await supabase.from('messages').insert({
      conversation_id: conversationId,
      content,
      role: 'user',
    })
 
    // Invoke Edge Function to generate AI response
    await supabase.functions.invoke('ai-chat', {
      body: { conversation_id: conversationId, message: content },
    })
  }, [conversationId, supabase])
 
  return { messages, isStreaming, sendMessage }
}

Managing Online State with Presence

// hooks/usePresence.ts
'use client'
 
import { useState, useEffect } from 'react'
import { createClient } from '@/lib/supabase/client'
 
interface UserPresence {
  user_id: string
  username: string
  online_at: string
}
 
export function usePresence(roomId: string, currentUser: { id: string; name: string }) {
  const [onlineUsers, setOnlineUsers] = useState<UserPresence[]>([])
  const supabase = createClient()
 
  useEffect(() => {
    const channel = supabase.channel(`presence:${roomId}`, {
      config: {
        presence: {
          key: currentUser.id,
        },
      },
    })
 
    channel
      .on('presence', { event: 'sync' }, () => {
        const state = channel.presenceState<UserPresence>()
        const users = Object.values(state).flat()
        setOnlineUsers(users)
      })
      .subscribe(async (status) => {
        if (status === 'SUBSCRIBED') {
          await channel.track({
            user_id: currentUser.id,
            username: currentUser.name,
            online_at: new Date().toISOString(),
          })
        }
      })
 
    return () => {
      supabase.removeChannel(channel)
    }
  }, [roomId, currentUser, supabase])
 
  return { onlineUsers }
}

Chapter 6: Supabase Storage Patterns

File Uploads with Access Control

// app/actions/upload.ts
'use server'
 
import { createClient } from '@/lib/supabase/server'
import { revalidatePath } from 'next/cache'
 
export async function uploadFile(formData: FormData) {
  const supabase = await createClient()
  const {
    data: { user },
  } = await supabase.auth.getUser()
 
  if (!user) {
    return { error: 'Authentication required' }
  }
 
  const file = formData.get('file') as File
  if (!file) {
    return { error: 'No file selected' }
  }
 
  // File size limit (5MB)
  if (file.size > 5 * 1024 * 1024) {
    return { error: 'File size must be 5MB or less' }
  }
 
  // Store in a per-user directory
  const fileName = `${Date.now()}-${file.name.replace(/[^a-zA-Z0-9.-]/g, '_')}`
  const filePath = `${user.id}/${fileName}`
 
  const { error } = await supabase.storage
    .from('user-uploads')
    .upload(filePath, file, {
      cacheControl: '3600',
      upsert: false,
    })
 
  if (error) {
    console.error('Upload error:', error)
    return { error: 'Upload failed' }
  }
 
  // Save file metadata to DB
  await supabase.from('files').insert({
    user_id: user.id,
    file_path: filePath,
    file_name: file.name,
    file_size: file.size,
    mime_type: file.type,
  })
 
  revalidatePath('/dashboard/files')
  return { success: true, path: filePath }
}
 
export async function getSignedUrl(filePath: string) {
  const supabase = await createClient()
 
  // Signed URL valid for 1 hour
  const { data, error } = await supabase.storage
    .from('user-uploads')
    .createSignedUrl(filePath, 3600)
 
  if (error) return { error: error.message }
  return { url: data.signedUrl }
}

Storage Bucket Policies

-- Users can only upload to their own directory
create policy "Users can upload own files"
  on storage.objects for insert
  to authenticated
  with check (
    bucket_id = 'user-uploads'
    and (storage.foldername(name))[1] = auth.uid()::text
  );
 
-- Users can only view their own files
create policy "Users can view own files"
  on storage.objects for select
  to authenticated
  using (
    bucket_id = 'user-uploads'
    and (storage.foldername(name))[1] = auth.uid()::text
  );
 
-- Users can only delete their own files
create policy "Users can delete own files"
  on storage.objects for delete
  to authenticated
  using (
    bucket_id = 'user-uploads'
    and (storage.foldername(name))[1] = auth.uid()::text
  );

Chapter 7: Type Generation and Developer Experience

Automatic TypeScript Type Generation

The Supabase CLI can auto-generate TypeScript types from your database schema. This lets you catch schema-related bugs at compile time:

# Generate types from local environment
npx supabase gen types typescript --local > types/supabase.ts
 
# Generate types from production (requires project ID)
npx supabase gen types typescript \
  --project-id YOUR_PROJECT_ID \
  --schema public > types/supabase.ts

Using generated types gives you full compile-time safety:

import type { Database } from '@/types/supabase'
 
type Tables = Database['public']['Tables']
type Profile = Tables['profiles']['Row']
type ProfileUpdate = Tables['profiles']['Update']
 
// ✅ Type-checked at compile time
const update: ProfileUpdate = {
  full_name: 'New Name',
  updated_at: new Date().toISOString(),
}
 
// ❌ Non-existent columns cause compile errors
// const invalid: ProfileUpdate = { nonexistent_field: 'value' }

Useful package.json Scripts

{
  "scripts": {
    "db:types": "supabase gen types typescript --local > types/supabase.ts",
    "db:migrate": "supabase db push",
    "db:reset": "supabase db reset",
    "db:seed": "npx supabase db seed"
  }
}

With these scripts in place, Antigravity can suggest the right commands in the right order when you say "update types and implement the new feature."


Chapter 8: Production Deployment and Monitoring

Environment Variable Management

# .env.local (local development)
NEXT_PUBLIC_SUPABASE_URL=http://127.0.0.1:54321
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key
 
# For Vercel / Cloudflare Workers: set via dashboard or CLI

Performance Tuning

-- Add indexes for frequently queried columns
create index idx_messages_conversation_id
  on public.messages (conversation_id, created_at desc);
 
create index idx_projects_team_id
  on public.projects (team_id, status, created_at desc);
 
-- Find slow queries with pg_stat_statements
select query, mean_exec_time, calls
from pg_stat_statements
order by mean_exec_time desc
limit 10;

Structured Audit Logging

// lib/supabase/logger.ts
export async function logEvent(
  event: string,
  metadata: Record<string, unknown>
) {
  const supabase = await createClient()
  await supabase.from('audit_logs').insert({
    event,
    metadata,
    created_at: new Date().toISOString(),
  })
}

Chapter 9: CI/CD Integration with Supabase

Automating Database Migrations

One of the most important aspects of a production Supabase workflow is automating database migrations through CI/CD. With Antigravity, you can generate the entire GitHub Actions workflow just by describing your requirements.

# .github/workflows/deploy.yml
name: Deploy to Production
 
on:
  push:
    branches: [main]
 
jobs:
  migrate-and-deploy:
    runs-on: ubuntu-latest
    env:
      SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
      SUPABASE_DB_PASSWORD: ${{ secrets.SUPABASE_DB_PASSWORD }}
      PROJECT_ID: ${{ secrets.SUPABASE_PROJECT_ID }}
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Supabase CLI
        uses: supabase/setup-cli@v1
        with:
          version: latest
 
      - name: Link Supabase project
        run: supabase link --project-ref $PROJECT_ID
 
      - name: Run database migrations
        run: supabase db push
 
      - name: Deploy Edge Functions
        run: supabase functions deploy --no-verify-jwt
 
      - name: Verify deployment
        run: |
          curl -f https://your-project.supabase.co/rest/v1/ \
            -H "apikey: ${{ secrets.SUPABASE_ANON_KEY }}" \
            || exit 1

Testing Supabase Integrations

Proper testing of Supabase interactions requires a local Supabase instance. Here's a pattern for setting up integration tests that actually run against a real database:

// tests/setup.ts
import { execSync } from 'child_process'
import { createClient } from '@supabase/supabase-js'
 
const SUPABASE_URL = 'http://127.0.0.1:54321'
const SUPABASE_SERVICE_ROLE_KEY = process.env.SUPABASE_SERVICE_ROLE_KEY!
 
export const testClient = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY)
 
export async function resetDatabase() {
  // Reset to a clean state before each test suite
  execSync('npx supabase db reset --local', { stdio: 'inherit' })
}
 
export async function seedTestData() {
  // Insert known test data
  const { data: user } = await testClient.auth.admin.createUser({
    email: 'test@example.com',
    password: 'testpassword123',
    email_confirm: true,
  })
 
  if (user.user) {
    await testClient.from('profiles').insert({
      id: user.user.id,
      username: 'testuser',
      full_name: 'Test User',
    })
  }
 
  return { userId: user.user?.id }
}
// tests/projects.test.ts
import { describe, it, expect, beforeAll } from 'vitest'
import { testClient, seedTestData } from './setup'
 
describe('Projects API', () => {
  let userId: string
 
  beforeAll(async () => {
    const seed = await seedTestData()
    userId = seed.userId!
  })
 
  it('should return only projects for the authenticated user', async () => {
    // Create a team and project for the test user
    const { data: team } = await testClient
      .from('teams')
      .insert({ name: 'Test Team', owner_id: userId })
      .select()
      .single()
 
    await testClient.from('projects').insert({
      team_id: team!.id,
      name: 'Test Project',
    })
 
    // Sign in as the test user
    const { data: session } = await testClient.auth.signInWithPassword({
      email: 'test@example.com',
      password: 'testpassword123',
    })
 
    // Create a client as the authenticated user (respects RLS)
    const userClient = createClient(SUPABASE_URL, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, {
      global: {
        headers: { Authorization: `Bearer ${session.session?.access_token}` },
      },
    })
 
    const { data: projects } = await userClient.from('projects').select('*')
 
    // RLS should ensure only this user's projects are returned
    expect(projects).toHaveLength(1)
    expect(projects![0].name).toBe('Test Project')
  })
})

This testing approach — running against a real local database rather than mocking — is the key to catching the kind of subtle RLS issues that only appear in production. Antigravity understands this philosophy and will suggest integration tests over mocks when writing Supabase-related test code.


Summary — Accelerating Solo Development with Antigravity × Supabase

In this guide, we systematically explored practical patterns for using Supabase's features to their fullest potential with Antigravity. From Auth for secure authentication and RLS for database-level access control, to Edge Functions for server-side logic, Realtime for responsive UX, and Storage for flexible file handling — combining these enables backend implementations that previously took weeks to complete in just a few days.

The real power of working with Antigravity comes from deeply contextualizing your project through RULES.md and type definitions. Instructions like "fetch all team projects while respecting RLS" will be implemented as secure, type-safe queries — without you needing to manually write every line.

Use the patterns in this guide as your foundation, and build an architecture tailored to your own project's needs.

For a deeper look at the GitHub Actions CI/CD patterns introduced in this article, check out our guide on Antigravity × GitHub Actions — Advanced CI/CD Pipeline Building. For production monitoring and observability, Antigravity × OpenTelemetry — AI Observability Pipeline Guide is an excellent companion read.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Integrations2026-04-21
Building Production-Grade Voice Agents with Gemini Live API in Antigravity — Bidirectional Audio, Screen Share, and Latency Patterns
A field guide for shipping production-ready realtime voice agents with Gemini Live API on Antigravity. Covers architecture, latency, resilience, function calling, and cost governance with working TypeScript.
App Dev2026-03-28
Building Real-Time Full-Stack Apps with Antigravity and Supabase: A Practical Guide
Learn how to combine Antigravity IDE with Supabase to build full-stack apps featuring authentication, database management, and real-time sync — step by step.
Integrations2026-05-15
Using Claude Opus 4 / Sonnet 4 in Antigravity — Model Selection Strategy and Production Patterns
A practical guide to using Claude Opus 4, Sonnet 4, and Haiku 4.5 in Antigravity. Learn the decision framework and production implementation patterns for balancing cost, speed, and quality in real projects.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →