add-server-action
Scaffold a Next.js server action with auth guard, Supabase mutation, and revalidation
SKILL.md.claude/skills/add-server-action/SKILL.md
GitHubAdd Server Action Skill
Purpose
Generate a Next.js Server Action that authenticates the user, performs a Supabase mutation, and revalidates the relevant path — following the Arch Systems pattern.
Process
1. Ask for action details
- file location — Where to create the action file (e.g.
apps/portal/app/(departments)/[department]/breakdowns/actions.ts) - action names and types — List of actions with:
- Name (e.g.
createBreakdown,updateDelay) - Operation:
insert|update|delete|soft-delete - Table name (e.g.
breakdowns,operational_delays) - Input fields and types
- Name (e.g.
- revalidation path — Path to revalidate after mutation (e.g.
/engineering/breakdowns) - access control — Who can perform this action? (e.g.
admin,supervisor,operator, any authenticated user) - soft delete — Should delete set a
deleted_attimestamp instead of removing the row?
2. Generate the action file
Create the actions.ts file with "use server" directive:
"use server";
import { createServerSupabaseClient } from "@repo/supabase/server";
import { revalidatePath } from "next/cache";
// --- INSERT action ---
export async function <actionName>(formData: FormData) {
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error("Unauthorized");
}
// Extract and validate required fields
const <field1> = formData.get("<field1>") as string;
const <field2> = formData.get("<field2>") as string;
if (!<field1> || !<field2>) {
return { error: "Missing required fields" };
}
const { error } = await supabase
.from("<table>")
.insert({
<field1>,
<field2>,
// department_id from hidden field or context
});
if (error) {
throw new Error(error.message);
}
revalidatePath("<revalidationPath>");
return { success: true };
}
// --- UPDATE action ---
export async function <actionName>Update(formData: FormData) {
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error("Unauthorized");
}
const id = formData.get("id") as string;
// ... extract updatable fields
const { error } = await supabase
.from("<table>")
.update({
// ... fields to update
})
.eq("id", id);
if (error) {
throw new Error(error.message);
}
revalidatePath("<revalidationPath>");
return { success: true };
}
// --- SOFT DELETE action ---
export async function <actionName>Delete(formData: FormData) {
const supabase = await createServerSupabaseClient();
const { data: { user } } = await supabase.auth.getUser();
if (!user) {
throw new Error("Unauthorized");
}
const id = formData.get("id") as string;
const { error } = await supabase
.from("<table>")
.update({ deleted_at: new Date().toISOString() })
.eq("id", id);
if (error) {
throw new Error(error.message);
}
revalidatePath("<revalidationPath>");
return { success: true };
}
3. Usage in client components
Server actions can be called from client components in two ways:
Option A: FormData-based (no useTransition needed)
<form action={createBreakdown}>
<input type="hidden" name="department_id" value={departmentId} />
{/* ... form fields with name attributes */}
<button type="submit">Submit</button>
</form>
Option B: Programmatic with useTransition (for validation, conditional logic)
"use client";
import { useTransition } from "react";
import { createBreakdown } from "./actions";
function MyComponent() {
const [isPending, startTransition] = useTransition();
const handleSubmit = () => {
const formData = new FormData();
formData.append("field1", value1);
startTransition(async () => {
await createBreakdown(formData);
});
};
}
4. Verify
- Test the action with valid data
- Test that unauthenticated users get "Unauthorized" error
- Test that required field validation works
- Test that the page revalidates after the action
- Commit changes
Patterns to Follow
- Always start with
"use server"directive - Always authenticate:
const { data: { user } } = await supabase.auth.getUser(); if (!user) throw new Error("Unauthorized"); - Always use
createServerSupabaseClient()from@repo/supabase/server— NEVER@supabase/supabase-jsdirectly - Use
revalidatePath()after mutations to refresh server component data - For soft deletes, set
deleted_attimestamp rather than actually deleting rows - For role-based access, check the user's role via
supabase.from("employees").select("role").eq("auth_id", user.id).single()