Files
Giancarlo Buomprisco 7ebff31475 Next.js Supabase V3 (#463)
Version 3 of the kit:
- Radix UI replaced with Base UI (using the Shadcn UI patterns)
- next-intl replaces react-i18next
- enhanceAction deprecated; usage moved to next-safe-action
- main layout now wrapped with [locale] path segment
- Teams only mode
- Layout updates
- Zod v4
- Next.js 16.2
- Typescript 6
- All other dependencies updated
- Removed deprecated Edge CSRF
- Dynamic Github Action runner
2026-03-24 13:40:38 +08:00

4.1 KiB

Server Action Reference

enhanceAction API

import { enhanceAction } from '@kit/next/actions';

export const myAction = enhanceAction(
  async function (data, user) {
    // data: validated input (typed from schema)
    // user: authenticated user object (if auth: true)

    return { success: true, data: result };
  },
  {
    auth: true,           // Require authentication (default: false)
    schema: ZodSchema,    // Zod schema for validation (optional)
  },
);

Options

Option Type Default Description
auth boolean false Require authenticated user
schema ZodSchema - Zod schema for input validation

Handler Parameters

Parameter Type Description
data z.output<Schema> Validated input data
user User Authenticated user (if auth: true)

enhanceRouteHandler API

import { enhanceRouteHandler } from '@kit/next/routes';
import { NextResponse } from 'next/server';

export const POST = enhanceRouteHandler(
  async function ({ body, user, request }) {
    // body: validated request body
    // user: authenticated user (if auth: true)
    // request: original NextRequest

    return NextResponse.json({ success: true });
  },
  {
    auth: true,
    schema: ZodSchema,
  },
);

export const GET = enhanceRouteHandler(
  async function ({ user, request }) {
    const url = new URL(request.url);
    const param = url.searchParams.get('param');

    return NextResponse.json({ data: result });
  },
  {
    auth: true,
  },
);

Common Zod Patterns

import * as z from 'zod';

// Basic schema
export const CreateItemSchema = z.object({
  name: z.string().min(1, 'Name is required'),
  description: z.string().optional(),
  accountId: z.string().uuid('Invalid account ID'),
});

// With transforms
export const SearchSchema = z.object({
  query: z.string().trim().min(1),
  page: z.coerce.number().int().positive().default(1),
  limit: z.coerce.number().int().min(1).max(100).default(10),
});

// With refinements
export const DateRangeSchema = z.object({
  startDate: z.coerce.date(),
  endDate: z.coerce.date(),
}).refine(
  (data) => data.endDate > data.startDate,
  { message: 'End date must be after start date' }
);

// Enum values
export const StatusSchema = z.object({
  status: z.enum(['active', 'inactive', 'pending']),
});

Revalidation

import { revalidatePath } from 'next/cache';
import { revalidateTag } from 'next/cache';

// Revalidate specific path
revalidatePath('/home/[account]/items');

// Revalidate with dynamic segment
revalidatePath(`/home/${accountSlug}/items`);

// Revalidate by tag
revalidateTag('items');

Redirect

import { redirect } from 'next/navigation';

// Redirect after action
redirect('/success');

// Redirect with dynamic path
redirect(`/home/${accountSlug}/items/${itemId}`);

Logging

import { getLogger } from '@kit/shared/logger';

const logger = await getLogger();

// Context object for all logs
const ctx = {
  name: 'action-name',
  userId: user.id,
  accountId: data.accountId,
};

// Log levels
logger.info(ctx, 'Starting operation');
logger.warn({ ...ctx, warning: 'details' }, 'Warning message');
logger.error({ ...ctx, error }, 'Operation failed');

Supabase Clients

// Standard client (RLS enforced)
import { getSupabaseServerClient } from '@kit/supabase/server-client';
const client = getSupabaseServerClient();

// Admin client (bypasses RLS - use sparingly)
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
const adminClient = getSupabaseServerAdminClient();

Error Handling

import { isRedirectError } from 'next/dist/client/components/redirect-error';

try {
  await operation();
  redirect('/success');
} catch (error) {
  if (!isRedirectError(error)) {
    // Handle actual error
    logger.error({ error }, 'Operation failed');
    throw error;
  }
  throw error; // Re-throw redirect
}