Function Middleware for Server Functions
Configure authentication and authorization middleware for TanStack Start server functions in your Supabase SaaS application.
Server logic in this kit runs through TanStack Start server functions (createServerFn). Authentication and authorization are composed from reusable middleware living in the @kit/function-middleware package, so individual functions never hand-roll auth checks.
Package shape
@kit/function-middleware exposes two entrypoints:
@kit/function-middleware/functions— pre-composed middleware tuples for the function definition site:authFunctionMiddleware— requires an authenticated userteamAccountFunctionMiddleware— requires membership on theaccountIdin the inputadminFunctionMiddleware— requires a Super Admin (is_superadmin)
@kit/function-middleware/server— the lower-level middleware and gates the tuples compose:errorMiddleware,authMiddleware,adminMiddleware,teamAccountMiddleware,withMinRole(role), andwithFeaturePermission(permission).
Each tuple always starts with errorMiddleware, which normalizes thrown errors to a plain message (so raw Postgrest/internal error objects are never serialized to the browser) while letting redirect() / notFound() pass through untouched.
Rules
- Keep
createServerFn(...)literal at the function definition site. A factory that returns acreateServerFnbuilder hides the function from the TanStack Start compiler, shipping the handler — and its Supabase/server graph — to the client bundle. - Put server functions in
*.functions.tsfiles. - Suffix each exported server function with
Function. - Use the tuple middleware instead of hand-written auth checks.
- Call server functions from the client with
useServerFn(...).
Authenticated function
import { createServerFn } from '@tanstack/react-start';import { authFunctionMiddleware } from '@kit/function-middleware/functions';export const updateAccountNameFunction = createServerFn({ method: 'POST' }) .middleware(authFunctionMiddleware) .validator(UpdateAccountNameSchema) .handler(async ({ data, context }) => { // context.user is available });Team-account function
teamAccountFunctionMiddleware validates a uuid accountId in the input and asserts membership via the has_role_on_account RPC. The function's .validator must therefore include an accountId field.
import { createServerFn } from '@tanstack/react-start';import { teamAccountFunctionMiddleware } from '@kit/function-middleware/functions';export const updateTeamSettingsFunction = createServerFn({ method: 'POST' }) .middleware(teamAccountFunctionMiddleware) .validator(UpdateTeamSettingsSchema) // includes `accountId` .handler(async ({ data, context }) => { // context.user, context.accountId });Admin function
import { createServerFn } from '@tanstack/react-start';import { adminFunctionMiddleware } from '@kit/function-middleware/functions';export const banUserFunction = createServerFn({ method: 'POST' }) .middleware(adminFunctionMiddleware) .validator(BanUserSchema) .handler(async ({ data, context }) => { // context.user is a verified Super Admin });Extra gates
Layer role or permission gates by spreading a tuple. Both gates read accountId from the input, so keep it in the .validator.
import { authFunctionMiddleware } from '@kit/function-middleware/functions';import { withFeaturePermission, withMinRole,} from '@kit/function-middleware/server';// Require a minimum role in the account's role hierarchy:export const deleteTeamAccountFunction = createServerFn({ method: 'POST' }) .middleware([...authFunctionMiddleware, withMinRole('owner')]) .validator(DeleteTeamAccountSchema) .handler(async ({ data, context }) => { ... });// Require a specific feature permission (checked via the `has_permission` RPC):export const removeMemberFunction = createServerFn({ method: 'POST' }) .middleware([...authFunctionMiddleware, withFeaturePermission('members.manage')]) .validator(RemoveMemberSchema) .handler(async ({ data, context }) => { ... });These gates are defense-in-depth: Row Level Security still enforces access in the service/data layer.
Client calls
Call server functions from client components with useServerFn(...), then drive them through a TanStack Query mutation:
import { useMutation } from '@tanstack/react-query';import { useServerFn } from '@tanstack/react-start';import { updateAccountNameFunction } from '../server/account.functions';function UpdateNameForm() { const updateName = useServerFn(updateAccountNameFunction); const mutation = useMutation({ mutationFn: (data: FormValues) => updateName({ data }), }); // derive UI from mutation.isPending / onSuccess / onError}Keep request-lifecycle state in the mutation and business logic in the server function/service — the client only orchestrates the call and the resulting UI.