Nextjs server action validation
Skills that know your codebase. Repo-grounded, contract-validated, agent-routable.
npx -y skills add jacob-balslev/skill-graph --skill nextjs-server-action-validationAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when writing a Next.js Server Action that accepts user-submitted form data, mutation parameters, or any client-originated input. Every Server Action is a public HTTP endpoint regardless of how it is called — validate with Zod and check authentication as the first two operations before touching the database. Do NOT use for GET route handlers or Server Components that fetch data (those have no user-supplied input); do NOT use for Stripe webhook handlers (use stripe-webhook-signature-verification instead).
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
10.1 KB, ~1.1k tokens by cl100k_base, as published. Nobody here has run it
Next.js Server Action Validation
Concept of the skill
What it is: The validation and authorization discipline for Next.js Server Actions that accept client-originated input. Mental model: A Server Action is a public POST endpoint wearing function-call syntax. Why it exists: Convenience syntax can hide the HTTP boundary, leading developers to skip auth, input validation, or org scoping. What it is NOT: It is not GET route handling, Server Component data fetching, or Stripe webhook verification. Adjacent concepts: Zod schemas, auth checks, org-scoped mutations, controlled error surfaces. One-line analogy: It is the security checkpoint every form submission crosses before touching the database. Common misconception: Only the project UI can call the action; any HTTP client can call the generated endpoint.
Coverage
- The public endpoint reality —
'use server'functions are exposed as POST endpoints that any HTTP client can call directly; the Next.js call graph is not a security boundary - Validation order — auth check BEFORE Zod parse, Zod parse BEFORE database access; any other order produces an exploitable window
- Zod schema design for actions — using
.safeParse()over.parse()to return structured errors rather than throwing; returning{ error: ... }shapes that the calling Client Component can render - Org-scoping after authentication — verifying that
input.orgIdmatches the session's org, not just that the user is logged in - Error surface control — how
try/catcharound database calls prevents internal errors from propagating to the client response
Philosophy of the skill
Server Actions are a convenience feature that makes form submission feel like a function call. That convenience obscures a critical fact: the action is a public HTTP POST endpoint. A developer who writes const { data } = await myAction(formData) in a Client Component is writing what looks like a local function call, but the runtime sends an HTTP request that any script can replicate. Skipping auth or validation because "it's called from our own UI" is the same reasoning that made getServerSideProps data-fetching functions leaky in the Pages Router — the server boundary does not restrict callers.
Standard Action Pattern
"use server";
import { z } from "zod";
import { getServerSession } from "@/lib/auth";
import { orgQuery } from "@/lib/db";
const CreateOrderSchema = z.object({
orgId: z.string().uuid(),
planId: z.string().min(1),
quantity: z.number().int().positive(),
});
export async function createOrder(input: unknown) {
// 1. Auth check — before anything else
const session = await getServerSession();
if (!session?.user) {
return { error: "Unauthorized" };
}
// 2. Zod parse — structured errors, not thrown exceptions
const parsed = CreateOrderSchema.safeParse(input);
if (!parsed.success) {
return { error: parsed.error.flatten() };
}
// 3. Org-scope check — user must belong to the org they are acting on
if (session.user.orgId !== parsed.data.orgId) {
return { error: "Forbidden" };
}
// 4. Database write — inside orgQuery for RLS enforcement
try {
const [order] = await orgQuery(parsed.data.orgId, (tx) =>
tx`INSERT INTO orders (org_id, plan_id, quantity) VALUES (
${parsed.data.orgId}, ${parsed.data.planId}, ${parsed.data.quantity}
) RETURNING *`
);
return { data: order };
} catch {
return { error: "Failed to create order" };
}
}
Validation Order Rationale
| Order | Risk |
|---|---|
| Auth → Zod → orgScope → DB | Correct — each gate eliminates the next attack surface |
| Zod → Auth → DB | Attacker can probe the schema structure without authenticating |
| Auth → DB (no Zod) | Malformed input reaches the query layer; injection risk |
| DB → Auth (anywhere) | Unauthenticated database reads before rejection |
Verification
- Every
'use server'function callsgetServerSession()as its first statement - Every
'use server'function runsSchema.safeParse()before any database call - The session org ID is compared to the input org ID before the database call
- Database calls are wrapped in
orgQuery, not bare SQL - Errors returned to the client do not include stack traces or query text
Do NOT Use When
| Use instead | When |
|---|---|
stripe-webhook-signature-verification | The entry point is a webhook route handler, not a Server Action |
| (a data-fetching skill) | The function is a Server Component that fetches data, with no user-submitted input |
postgres-rls-pattern | The task is defining the database-layer policy, not the action-layer validation |