ANTIGRAVITY LABJP
Articles/App Development
App Development/2026-05-02Intermediate

Stopping Server Action Foot-Guns Before Antigravity Ships Them — Zod, revalidate, and Authorization Built In From the First Draft

Ask Antigravity to write a Next.js Server Action and you'll often get back code with no Zod parsing, no revalidate call, and no authorization check. Here's how to bake those in from the start instead of patching them on later.

antigravity435nextjs4server-actionszod2security16app-dev49

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 revalidatePath or revalidateTag call 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 client

Once 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.

Share

Thank You for Reading

Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

App Dev2026-06-25
An Agent Granted 'Watch an Ad to Unlock a Wallpaper' Entirely Client-Side — Re-Verifying Reward Grants with AdMob SSV
I asked an Antigravity agent to wire up 'watch a rewarded ad to unlock a wallpaper,' and it returned an implementation that wrote the unlock flag client-side only. Here is why that is not enough, how I re-verified the reward grant with AdMob server-side verification (SSV), and how I stopped double grants too.
App Dev2026-07-13
The Gate That Stops Visual Damage Before You Hand Bulk Image Optimization to an Agent
When an agent bulk re-encoded a few hundred wallpaper assets, a handful came back with dulled color. Size-reduction alone cannot catch that. Here is how to design a gate that stops bad conversions before merge using three axes — SSIM, ΔE, and file size — with a checker that runs on Pillow and scikit-image.
App Dev2026-07-09
Before an Agent's .proto Edit Silently Breaks Binary Compatibility: Gating Wire Compat with buf breaking
When an agent edits your .proto files, the text can look perfect while wire compatibility quietly breaks. Here is how to stop field-number reuse and unsafe type changes with a working gate built from buf breaking and reserved.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →