You ask Antigravity for "a Server Action that handles this form submission," paste the result, and only later notice that there is no Zod parsing, no revalidatePath, and no authorization check anywhere in the file. If that has happened to you, you are not alone. Server Actions are easy to write — and that ease is exactly why vague AI prompts so often turn into production security holes.
This article is about how I stopped letting that happen. Specifically: how I prompt Antigravity for Server Actions, what guardrails I require in the first draft, and the review checklist I run before any of them ship. The examples assume Next.js 15 App Router, but the same problem surfaces in Remix and SvelteKit form actions too.
Why AI-generated Server Actions tend to be unsafe
A Server Action is a function that becomes callable from the browser the moment you put "use server" at the top of the file. That is enormously convenient, and it is also the reason you cannot trust anything that arrives in the function's arguments. When I ask an LLM to "take form values and save them to the database," it produces the shortest path that runs. That shortest path is missing, more often than not:
- Type and presence checks on form values (Zod or similar)
- An authentication check for the current user
- An authorization check that the user actually owns the resource being mutated
- A
revalidatePathorrevalidateTagcall after the write - Error handling that prevents raw database errors from leaking to the client
In my experience, an unguided Antigravity prompt produces one or two of those five. Catching the missing pieces by hand, every single time, ends up taking longer than writing the action yourself — and that defeats the whole point of using the AI.
The asymmetry is what makes this dangerous. A missing UI prop is loud — the page renders wrong and you fix it before merging. A missing authorization check is silent — the action ships, the form works in your manual test, and you only find the hole the day a curious user crafts a request that mutates someone else's row. Server Actions reward you for moving fast and punish you for moving fast in exactly the same place.
A one-page prompt template kills 90% of the misses
The fix that turned this around for me is embarrassingly small: I keep a short template in AGENTS.md (or the project's .rules) that the agent reads before writing any Server Action. Antigravity references this file automatically when you ask it to make changes to the project, so you do not need to paste the rules into chat each time.
# Server Action requirements
Whenever you create a new Server Action in this project:
1. Place "use server" at the top of the file
2. Define inputs with a Zod schema and validate via safeParse()
3. Call auth() to fetch the current session; if missing, return early
4. For resource mutations, verify that the resource owner equals the session user
5. Call revalidatePath() or revalidateTag() after every successful write
6. Wrap DB calls in try/catch and return { ok: false, message: "..." }
7. Never return raw DB error messages or stack traces to the clientOnce those rules are in AGENTS.md, asking "write a Server Action that creates a post from this form" reliably produces a draft that satisfies all seven points. The cognitive load on you drops to verifying, not authoring.
What the safe minimum looks like
Following that template, Antigravity will reliably land on something close to the shape below. This is a slightly simplified version of the skeleton I actually keep in production projects.
// app/actions/posts.ts
"use server";
import { z } from "zod";
import { auth } from "@/lib/auth";
import { db } from "@/lib/db";
import { revalidatePath } from "next/cache";
const PostInput = z.object({
title: z.string().min(1).max(120),
body: z.string().min(1).max(20_000),
});
export async function createPost(_prev: unknown, formData: FormData) {
// 1. Authentication
const session = await auth();
if (!session?.user) {
return { ok: false, message: "You must be signed in." } as const;
}
// 2. Input validation
const parsed = PostInput.safeParse({
title: formData.get("title"),
body: formData.get("body"),
});
if (!parsed.success) {
return {
ok: false,
message: "Some fields are invalid.",
issues: parsed.error.flatten().fieldErrors,
} as const;
}
// 3. Database write (swallow exceptions)
try {
await db.post.create({
data: { ...parsed.data, authorId: session.user.id },
});
} catch (e) {
console.error("createPost failed", e);
return { ok: false, message: "Could not save your post." } as const;
}
// 4. Cache invalidation
revalidatePath("/posts");
return { ok: true } as const;
}The thing I look for during review is not the details — it is the order. Every Server Action in the codebase walks through the same four phases: authenticate, validate, write, revalidate. When that order is consistent, reading a Server Action becomes mechanical: scan top-to-bottom and confirm each phase is present.
A second habit that pays off: return values are always typed as const. That tiny annotation gives the client a discriminated union it can narrow on state.ok, which is the difference between TypeScript actually checking the error path and just nodding at it. When you let Antigravity skip as const, you also lose the compile-time safety on the client — and once that is gone, the prompt template's discipline does not propagate forward.
Mirror the discipline on the client with useActionState
Because every action returns the same { ok: boolean } shape, useActionState lets you write the client side the same way every time. You get a uniform pattern for showing errors and reacting to success.
"use client";
import { useActionState } from "react";
import { createPost } from "@/app/actions/posts";
export function PostForm() {
const [state, formAction, pending] = useActionState(createPost, null);
return (
<form action={formAction}>
<input name="title" required />
<textarea name="body" required />
<button disabled={pending}>{pending ? "Posting..." : "Post"}</button>
{state && !state.ok && <p role="alert">{state.message}</p>}
{state?.ok && <p>Your post is live.</p>}
</form>
);
}The under-appreciated detail here is the issues field returned by the action. Because Zod's flatten().fieldErrors produces a per-field error map, the client can render messages right under each input with state?.issues?.title?.[0]. Asking Antigravity to "also surface field-level errors under each input" is a one-line request once the shape is in place.
Make Antigravity audit its own output
I do not eyeball Server Actions anymore. After a draft lands, I run an inline-chat review (Cmd+K on the file) with a fixed prompt:
Review the Server Action(s) in this file against these criteria:
- "use server" at the top of the file
- All inputs validated with Zod (safeParse, not parse)
- Authentication via auth() with early return for unauthenticated users
- Authorization check (resource owner === session user) for mutations
- revalidatePath() or revalidateTag() after every successful write
- DB calls wrapped in try/catch
- No stack traces, SQL, or raw DB errors exposed in return values
Call out any failing item explicitly. If anything is missing, propose a diff that fixes only the gap.
Saving this as a snippet means every new Server Action gets a 30-second audit. When the agent finds something missing, it returns a diff I can apply with a single click. Server Actions are short enough that the model can hold the whole function in context — the format plays to the AI's strengths, so let it do the work.
One practical note: keep this prompt static. The temptation is to grow it whenever you discover a new failure mode, but a checklist that bloats past ten items is a checklist nobody completes properly — including the model. When a new concern shows up consistently, prefer adding it to AGENTS.md (so the first draft already accounts for it) over stuffing the audit prompt. Treat the audit as a final filter, not as the place where new policy lives.
If you want to deepen this further, our companion piece on accelerating schema-driven development with Zod in Antigravity covers the validation half in more depth, and the practical patterns for never dropping a webhook in Antigravity walk through the same trust-boundary thinking on inbound webhooks. Together they cover most of the surface area where untrusted input enters a Next.js app.
Where to start
Pick exactly one Server Action you have already written, paste the seven-rule template into your project's AGENTS.md, and ask Antigravity to rewrite that action against the rules. Then run the review prompt over the result. Once you have done that loop once, every future Server Action inherits the same shape — and the seven things you used to weigh in your head become zero things you have to remember.
If you adopt only one of the suggestions in this piece, make it the AGENTS.md entry. The audit prompt and the discriminated-union return shape both compound on top of it, but neither will save you if the first draft Antigravity produces is missing the bones. Get the bones right once, and the rest of the work is verification rather than authorship — which is exactly the relationship you want to have with an AI pairing partner on code that runs against a database.