Security
Agent-native apps are designed to be secure by default. The framework provides automatic protections at multiple layers — you get SQL-level data isolation, parameterized queries, input validation, and authentication out of the box.
Security by Design
The framework architecture prevents common vulnerabilities when you use the standard patterns:
| Vulnerability | Framework Protection |
|---|---|
| SQL injection | Parameterized queries in db-query/db-exec and Drizzle ORM |
| XSS | React auto-escapes JSX; TipTap sanitizes rich text |
| Data leaks | SQL-level scoping via temporary views (owner_email, org_id) |
| Auth bypass | Auth guard auto-protects all defineAction endpoints |
| Input injection | Zod schema validation in defineAction |
| CSRF | SameSite=lax + httpOnly cookies |
| Secret exposure | .env files gitignored; OAuth tokens in dedicated store |
Input Validation
Use defineAction with a Zod schema: for every action. The framework validates input automatically before your code runs:
import { z } from "zod";
import { defineAction } from "@agent-native/core";
export default defineAction({
description: "Create a note",
schema: z.object({
title: z.string().min(1).max(200).describe("Note title"),
content: z.string().optional().describe("Note body"),
}),
run: async (args) => {
// args is guaranteed valid — invalid input never reaches here
},
});
Invalid input returns clear error messages (400 for HTTP, structured error for agent calls). The legacy parameters: format provides no runtime validation.
SQL Injection Prevention
The framework's db-query and db-exec tools use parameterized queries. User input is passed as arguments, never interpolated into the SQL string:
// SAFE — parameterized query (framework default)
await exec({ sql: "INSERT INTO notes (title) VALUES (?)", args: [title] });
// SAFE — Drizzle ORM (always generates parameterized queries)
await db.insert(notes).values({ title, ownerEmail: email });
// DANGEROUS — string concatenation (never do this)
await exec(`INSERT INTO notes (title) VALUES ('${title}')`);
XSS Prevention
React auto-escapes all JSX expressions. Additional guidelines:
- Never use
dangerouslySetInnerHTMLwith user-controlled content - Never use
innerHTML,eval(), ordocument.write() - For rich text editing, use TipTap (framework dependency) — it sanitizes through its schema
- For rendering markdown, use
react-markdown— it converts to React elements safely
Data Scoping
In production, the framework automatically restricts agent SQL queries to the current user's data. This is enforced at the SQL level — agents cannot bypass it.
Per-User Scoping (`owner_email`)
Every table with user-specific data must have an owner_email text column:
import { table, text, integer } from "@agent-native/core/db/schema";
export const notes = table("notes", {
id: text("id").primaryKey(),
title: text("title").notNull(),
content: text("content"),
owner_email: text("owner_email").notNull(), // REQUIRED
});
The framework creates temporary SQL views that filter queries automatically:
CREATE TEMPORARY VIEW "notes" AS
SELECT * FROM main."notes"
WHERE "owner_email" = 'alice@example.com';
INSERT statements get owner_email auto-injected when the column isn't already present.
Per-Org Scoping (`org_id`)
For multi-user apps where teams share data, add an org_id column. When both columns are present, queries are scoped by both: WHERE owner_email = ? AND org_id = ?.
Validation
pnpm action db-check-scoping # Check all tables have owner_email
pnpm action db-check-scoping --require-org # Also require org_id
Secrets Management
| Secret type | Where to store |
|---|---|
| API keys (OpenAI, Stripe, etc.) | .env file (gitignored, server-side only) |
| OAuth tokens (Google, GitHub) | oauth_tokens store via saveOAuthTokens() |
| Session tokens | Automatic (Better Auth handles this) |
Never store secrets in settings, application_state, source code, or action responses.
Authentication
Auth is automatic. See the Authentication docs for the full setup.
Key points for security:
defineActionendpoints are auto-protected by the auth guard- Custom
/api/routes must callgetSession(event)and check the result - State-changing operations should use POST (the default for actions)
SameSite=lax+httpOnlycookies prevent most CSRF attacks
A2A Identity Verification
When apps call each other via the A2A protocol, they verify identity using JWT tokens signed with a shared secret:
A2A_SECRET=your-shared-secret-at-least-32-chars
- App A signs a JWT containing
sub: "steve@example.com" - App B verifies the JWT signature with the same secret
- App B sets
AGENT_USER_EMAILfrom the verifiedsubclaim - Data scoping applies — App B only shows Steve's data
Without A2A_SECRET, A2A calls are unauthenticated (fine for local dev, not production).
Production Checklist
- Every user-facing table has
owner_email - Multi-user tables also have
org_id - All actions use
defineActionwith Zodschema: - No
dangerouslySetInnerHTMLwith user content - No string-concatenated SQL
- API keys are in
.envonly (not in source code or settings) -
BETTER_AUTH_SECRETis set to a random 32+ character string -
A2A_SECRETis set on all apps that call each other -
AUTH_MODEis not set tolocal -
pnpm action db-check-scopingpasses - Tested with two user accounts to verify data isolation