Publicidad
Patrocinador Verificado
Infraestructura Cloud, GPU Clusters y APIs de IA para Desarrolladores
Despliega pipelines de inferencia y trading con latencia ultra baja. Explora planes.
Conocer Más →

Vibe Coding Full-Stack: Construye un SaaS con Next.js, Supabase y Cursor

Por Sarah Lin • Intermedio • 20 min de lectura • Actualizado 2026-09-12

Lo Que Dominarás en Este Tutorial

  • Configure Cursor Composer to orchestrate multi-file full-stack edits across frontend and backend.
  • Prompt AI to generate relational PostgreSQL schemas with strict Supabase Row Level Security (RLS).
  • Implement type-safe Server Actions with Zod validation and optimistic UI updates.
  • Safely handle Stripe webhook lifecycle events and database synchronizations.

1. Architecting the Multi-File Prompt in Cursor Composer

Cursor Composer allows editing dozens of project files simultaneously. To get production-grade output, structure your master prompt with clear architectural roles, dependency versions, and security constraints.

MARKDOWN
# Master Composer Prompt:
"""
You are an expert Next.js 15 and Supabase architect.
Task: Build an authenticated subscription dashboard for our developer analytics platform.

Files to create / modify:
1. app/dashboard/page.tsx: Server component fetching metrics from Supabase.
2. app/actions/billing.ts: Server Action creating a Stripe customer portal session.
3. supabase/migrations/20260912_metrics.sql: Table definition with strict RLS policies.

Requirements:
- Next.js 15 App Router with TypeScript strict mode.
- Use Supabase SSR client (@supabase/ssr).
- Enforce RLS: users can only SELECT and INSERT rows matching auth.uid().
- Handle loading and error boundaries gracefully.
"""
Nota: Golden Rule: List the exact files you want created or modified to prevent the AI from scattering logic into arbitrary directories.
Publicidad
Patrocinador Verificado
Trading Cuantitativo y 30 Modelos de Negocio con IA
Genera ingresos predecibles con retainers mensuales y bots automatizados.
Ver Blueprints →

2. Bulletproof Supabase RLS Generation

AI models frequently forget database security unless explicitly prompted. Always verify that generated migrations include Row Level Security and explicit tenant isolation policies.

SQL
-- Generated Supabase Migration: 20260912_metrics.sql
CREATE TABLE public.developer_metrics (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
  api_calls_count INTEGER NOT NULL DEFAULT 0,
  latency_p99_ms NUMERIC(6,2) NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

-- Enable Row Level Security (Mandatory)
ALTER TABLE public.developer_metrics ENABLE ROW LEVEL SECURITY;

-- Policy: Users can only read their own metrics
CREATE POLICY "Users can read own metrics"
  ON public.developer_metrics
  FOR SELECT
  USING (auth.uid() = user_id);

-- Policy: Users can insert their own metrics
CREATE POLICY "Users can insert own metrics"
  ON public.developer_metrics
  FOR INSERT
  WITH CHECK (auth.uid() = user_id);
Nota: Security Warning: Never deploy an AI-generated database migration without verifying that ENABLE ROW LEVEL SECURITY is executed.

Evaluación Rápida: Pon a Prueba tus Conocimientos

1. Why is Row Level Security (RLS) critical when building with Supabase and AI?

3. Paso a paso: conecta el panel con Supabase, de principio a fin

En las secciones 1 y 2 ya generaste el esquema, las políticas de RLS y la estructura de archivos. Lo que falta es justo lo que la mayoría de los tutoriales suele pasar por alto: el código real de la aplicación que se comunica con Supabase. Lo vas a construir en tres piezas —un cliente de servidor para SSR, un Server Component protegido que lee las métricas y una Server Action que las escribe— usando exactamente la tabla (developer_metrics) y las políticas que creó la migración de la sección 2.

Paso 1: crea el cliente de servidor SSR

Toda llamada desde el servidor pasa por una única fábrica que conecta la clave anónima con las cookies de la petición. Es el patrón canónico de @supabase/ssr: el cliente lee la sesión desde las cookies y es la seguridad a nivel de fila (Row Level Security), y no tu código, la que decide qué filas puede ver esa sesión. Como RLS se aplica dentro de la base de datos, la clave anónima pública se puede enviar al navegador sin riesgo; la clave de rol de servicio, en cambio, no, y no aparece en ningún momento de este tutorial.

TYPESCRIPT
// utils/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) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Called from a Server Component: safe to ignore when
            // middleware is refreshing the session (see step 4).
          }
        },
      },
    }
  )
}

Paso 2: lee las métricas en un Server Component protegido

La página del panel se ejecuta por completo en el servidor. Le pregunta a Supabase quién es el usuario, redirige a los visitantes anónimos a /login y después selecciona las métricas más recientes. Fíjate en lo que no está: no hay ningún filtro por user_id en la consulta. La política SELECT de la sección 2 lo añade automáticamente, así que ni siquiera una página con un error podría filtrar las filas de otro inquilino.

TSX
// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'

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

  const { data: { user } } = await supabase.auth.getUser()
  if (!user) redirect('/login')

  const { data: metrics, error } = await supabase
    .from('developer_metrics')
    .select('id, api_calls_count, latency_p99_ms, created_at')
    .order('created_at', { ascending: false })
    .limit(20)

  if (error) throw new Error(error.message)

  return (
    <main>
      <h1>Metrics for {user.email}</h1>
      <ul>
        {metrics.map((m) => (
          <li key={m.id}>
            {new Date(m.created_at).toLocaleString()} - {m.api_calls_count} calls,
            p99 {m.latency_p99_ms} ms
          </li>
        ))}
      </ul>
    </main>
  )
}

Paso 3: escribe filas mediante una Server Action validada

Las escrituras pasan por una Server Action y los datos se validan con Zod antes de llegar a la base de datos: ese es el patrón con seguridad de tipos que plantean los objetivos de este tutorial. La política INSERT comprueba además que user_id coincida con auth.uid(), de modo que un cliente manipulado no pueda escribir en la cuenta de otra persona.

TYPESCRIPT
// app/actions/metrics.ts
'use server'

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

const metricSchema = z.object({
  apiCallsCount: z.number().int().min(0),
  latencyP99Ms: z.number().positive(),
})

export async function logMetric(input: unknown) {
  const parsed = metricSchema.safeParse(input)
  if (!parsed.success) throw new Error('Invalid metric payload')

  const supabase = await createClient()
  const { data: { user } } = await supabase.auth.getUser()
  if (!user) throw new Error('Not authenticated')

  const { error } = await supabase.from('developer_metrics').insert({
    user_id: user.id,
    api_calls_count: parsed.data.apiCallsCount,
    latency_p99_ms: parsed.data.latencyP99Ms,
  })
  if (error) throw new Error(error.message)

  revalidatePath('/dashboard')
}

4. Un ejemplo completo

Las tres piezas funcionan, pero todavía queda un hueco: los Server Components no pueden escribir cookies, así que una sesión a punto de caducar nunca se renueva durante el renderizado de una página. El middleware cierra ese hueco. Se ejecuta antes de cada petición, renueva el token de autenticación y aleja a los usuarios anónimos de /dashboard antes incluso de que la página empiece a cargar:

TYPESCRIPT
// middleware.ts
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let response = 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)
          )
          response = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            response.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  const { data: { user } } = await supabase.auth.getUser()

  if (!user && request.nextUrl.pathname.startsWith('/dashboard')) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  return response
}

export const config = {
  matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
}

Sigue el recorrido de una petición en el ejemplo terminado: un usuario con la sesión iniciada abre /dashboard; el middleware renueva la cookie de sesión y deja pasar la petición; el Server Component crea el cliente SSR, confirma quién es el usuario y selecciona unas métricas que RLS limita en silencio a ese usuario; cuando la aplicación registra una métrica nueva, la Server Action la valida con Zod, la inserta bajo la política INSERT y vuelve a validar la página. Son cuatro fronteras de confianza, y la base de datos hace cumplir las dos más importantes.

5. Solución de errores

SQL
-- Which policies does Postgres actually enforce on the table?
select policyname, cmd, qual, with_check
from pg_policies
where tablename = 'developer_metrics';

Recibe 1 plan de agente IA cada semana

Únete al boletín gratuito de PVT: un agente de IA o automatización práctica que puedes construir y vender, cada semana. Sin spam.