Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.refactkit.com/llms.txt

Use this file to discover all available pages before exploring further.

Server functions are the backbone of RefactKit’s backend. Created with createServerFn from @tanstack/react-start, they execute exclusively on the Nitro v3 server — never in the browser bundle — and are called directly from components or route loaders as if they were ordinary async functions. Every server function in the boilerplate follows the same four-step security pattern: validate, authenticate, authorize, execute.

The four-step security pattern

1

Validate input with Zod

Parse the incoming data argument through a Zod schema before touching anything else. This catches type mismatches and missing fields at the boundary, before any database calls happen.
import { z } from 'zod'

const { name, organizationId } = z
  .object({
    name: z.string().min(1),
    organizationId: z.string(),
  })
  .parse(data)
z.parse() throws a ZodError if validation fails. TanStack Start surfaces this as a rejected promise that you can catch on the client.
2

Authenticate — read the session from cookies

Call auth.api.getSession() with the current request headers. The session is stored in an encrypted JWE cookie — no database hit when the cache is warm.
import { getRequest } from '@tanstack/react-start/server'
import { auth } from '../../lib/auth'

const request = getRequest()
const session = await auth.api.getSession({ headers: request.headers })
if (!session) throw new Error('Unauthorized')
3

Authorize — verify org membership and role

Query the member table to confirm the authenticated user actually belongs to the target organization. Optionally enforce a minimum role.
import { and, eq } from 'drizzle-orm'
import { db } from '../../db/index'
import { member } from '../../db/schema'

const userMembership = await db.query.member.findFirst({
  where: and(
    eq(member.organizationId, organizationId),
    eq(member.userId, session.user.id),
  ),
})

if (!userMembership) throw new Error('Forbidden')

// For owner-only actions:
if (userMembership.role !== 'owner') throw new Error('Forbidden')
4

Execute business logic

With input validated and identity confirmed, run your database queries, storage operations, or external API calls. Return a plain serializable value — TanStack Start serializes it for the client automatically.
const result = await db
  .insert(myTable)
  .values({ name, organizationId })
  .returning()

return { item: result[0] }

GET and POST examples

Use method: 'GET' for read operations and method: 'POST' for writes, deletions, and anything with side effects. This matches HTTP semantics and allows TanStack Query to cache GET results correctly.
// src/server/dashboard-fns.ts
import { createServerFn } from '@tanstack/react-start'
import { getRequest } from '@tanstack/react-start/server'
import { and, count, eq } from 'drizzle-orm'
import { z } from 'zod'
import { db } from '../../db/index'
import { galleryImage, invitation, member } from '../../db/schema'
import { auth } from '../../lib/auth'

export const getOrgStats = createServerFn({ method: 'GET' }).handler(async ({ data }) => {
  const { organizationId } = z.object({ organizationId: z.string() }).parse(data)

  const request = getRequest()
  const session = await auth.api.getSession({ headers: request.headers })
  if (!session) throw new Error('Unauthorized')

  const [memberRes] = await db
    .select({ count: count() })
    .from(member)
    .where(eq(member.organizationId, organizationId))

  return { memberCount: memberRes.count }
})

Calling server functions from a component

Server functions are called with a { data: ... } argument. On the client, they behave like any other async function:
import { updateOrganization } from '@/server/org-fns'

async function handleSubmit(values: { organizationId: string; name: string }) {
  await updateOrganization({ data: values })
}
When used inside a TanStack Query mutation:
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { updateOrganization } from '@/server/org-fns'

const queryClient = useQueryClient()

const mutation = useMutation({
  mutationFn: (values: { organizationId: string; name: string }) =>
    updateOrganization({ data: values }),
  onSuccess: () => {
    queryClient.invalidateQueries({ queryKey: ['org'] })
  },
})

Server function files in src/server/

Each file groups functions by domain. Keep new functions in the appropriate existing file, or create a new file for a distinct domain.
FileResponsibility
auth-fns.tsSession retrieval, updating the current user’s profile
org-fns.tsCreate, read, update, delete organizations; membership validation
dashboard-fns.tsAggregate organization statistics (member count, storage usage, etc.)
gallery-fns.tsGallery image CRUD, scoped to an organization
storage-fns.tsSupabase file uploads — always server-only
query-keys.tsqueryOptions factories that pair cache keys with server function calls

Updating the query key registry

After writing a new server function, add a corresponding queryOptions entry in src/server/query-keys.ts. This keeps cache keys consistent between SSR loaders and client-side useQuery calls:
// src/server/query-keys.ts
import { queryOptions } from '@tanstack/react-query'
import { getMyData } from './my-fns'

export const myDataQuery = (orgId: string) =>
  queryOptions({
    queryKey: ['my-data', orgId] as const,
    queryFn: () => getMyData({ data: { orgId } }),
  })
Never import a server function inside a file that is also imported by client-only modules (e.g., component files that don’t use createFileRoute). TanStack Start tree-shakes server functions from the client bundle based on the use server boundary — importing them in shared modules can break this boundary and expose server code to the browser.