Input validation
Validate every untrusted value at the server boundary that acts on it. Browser validation gives people fast feedback, but it can be bypassed and TypeScript types are removed at runtime.
Forms
Use Zod for both client and server validation when both layers accept the same form contract. The browser may parse during editing or submission to show field errors, but the receiving Server Action or route handler must parse the submitted FormData or JSON again before it performs a side effect, calls Supabase, or persists data.
import { z } from 'zod';
const profileSchema = z.object({
firstName: z.string().trim().min(1).max(100),
lastName: z.string().trim().min(1).max(100),
});
export async function updateProfile(formData: FormData) {
'use server';
const result = profileSchema.safeParse({
firstName: formData.get('firstName'),
lastName: formData.get('lastName'),
});
if (!result.success) {
return { ok: false, fieldErrors: result.error.flatten().fieldErrors };
}
// Authorise the caller, then persist result.data.
}
Keep the schema beside the domain or operation that owns the contract so a form component, Server Action, and route handler can reuse it without making presentation code authoritative. If client and server inputs intentionally differ, derive separate schemas from shared primitives instead of weakening one schema to cover both.
Return safe, serialisable validation errors. Do not expose stack traces, SQL messages, raw Supabase errors, or other internal details to the browser. Test valid input plus missing, malformed, wrong-type, and boundary values, and confirm rejected input causes no side effect.
Supabase Edge Functions
An Edge Function is another server trust boundary. Where the request contains structured body, query, path, header, or option values, validate them with Zod before choosing privileged behaviour, using a service client, or calling an RPC. Keep the Zod import compatible with the project's checked-in Deno runtime and pin it consistently with the Edge Function dependency policy.
Zod validates the declared shape. Validate uploaded file size and content separately; do not trust a claimed MIME type, filename, Storage path, URL, or resource identifier. Authentication, authorisation, allow-lists, and Storage policies remain required.
Supabase RPCs
PostgreSQL RPCs cannot run Zod. Validate untrusted RPC-bound input in the calling Server Action, route handler, or Edge Function, then enforce the contract again in PostgreSQL with typed parameters, constraints, explicit checks, transaction invariants, grants, authentication, authorisation, and RLS as appropriate.
RPCs called directly from the browser must remain safe when invoked without your form. Client-side Zod can improve feedback, but only PostgreSQL can authoritatively reject an invalid or unauthorised direct RPC request.
Zod complements these controls; it does not replace database constraints, RLS, CSRF protection, permission checks, rate limiting, or business invariants.