Reading and writing data

Which Supabase client to use where, and the server action and route handler wrappers the kit uses for writes.

Last updated

Pick the client

WhereClientRow-level security
Server components, server actions, route handlersgetSupabaseServerClient() from @repo/supabase/server-clientApplies, as the signed-in user
Client componentsuseSupabase() from @repo/supabase/hooks/use-supabase, usually with React QueryApplies, as the signed-in user
Phone app API routes (/api/v1)The client that enhanceRouteHandler passes in (bearer token or cookie)Applies, as the signed-in user
Server code that must act beyond the user's rightsgetSupabaseServerAdminClient() from @repo/supabase/server-admin-clientSkipped: check permissions yourself

Fetch data in server components where you can. With the normal clients, you do not add "is this user allowed?" checks for reads: the policies already filter the rows.

import { getSupabaseServerClient } from '@repo/supabase/server-client';

async function ProjectsList() {
  const client = getSupabaseServerClient();
  const { data } = await client.from('projects').select('*');
  // only the projects of accounts this user belongs to
}

Writes: server actions

Server actions use next-safe-action through @repo/next/safe-action:

ClientUse for
authActionClientSigned-in users. Redirects to sign-in otherwise and passes ctx.user
captchaActionClientSigned-in users, plus a Cloudflare Turnstile check
publicActionClientActions anyone may call, such as the contact form
'use server';

import * as z from 'zod';

import { authActionClient } from '@repo/next/safe-action';
import { getSupabaseServerClient } from '@repo/supabase/server-client';

const CreateProjectSchema = z.object({
  accountId: z.string().uuid(),
  name: z.string().min(1),
});

export const createProjectAction = authActionClient
  .inputSchema(CreateProjectSchema)
  .action(async ({ parsedInput }) => {
    const client = getSupabaseServerClient();

    // RLS decides whether this user may insert into this account
    await client.from('projects').insert({
      account_id: parsedInput.accountId,
      name: parsedInput.name,
    });
  });

The input schema validates what the browser sent. The insert still goes through row-level security, so a user who sends someone else's accountId is refused by the database.

Route handlers

API routes use enhanceRouteHandler from @repo/next/routes. Options: auth (require a user, default on), schema (a Zod schema for the body) and captcha. The handler receives request, the parsed body, the user and an RLS-bound client.

The agent skills for this

/server-action-builder, /service-builder and /react-form-builder write actions, services and forms in these patterns. /postgres-expert writes the table and its policies.

Live demo

Resets hourly

Get the kit