We're in the "vibe coding" era. AI coding agents — Cursor, Claude Code, Copilot, Devin, and their growing kin — are no longer writing toy snippets. They're scaffolding entire features, refactoring modules, and wiring up API endpoints in seconds. The promise is intoxicating: describe what you want, get working code.
The reality is more nuanced. These agents are prolific, but not infallible. They hallucinate API signatures. They skip edge cases. They introduce subtle bugs that compile cleanly and pass a first glance. And crucially, they do all of this fast — far faster than any human code review can keep up with.
So here's the question that matters more than ever: what catches the mistakes an agent makes?
We Need a New Way to Evaluate a Stack
For years, we've chosen tech stacks based on developer experience, ecosystem maturity, performance characteristics, and hiring pool. These criteria still matter. But a new dimension has entered the picture: how well does the stack constrain and verify AI-generated code?
Think about it this way. When a human writes code, they bring context, intuition, and institutional knowledge. They remember the auth check that needs to go on every endpoint. They know the subtle business rule about tenant isolation. An AI agent has none of that ambient awareness — it operates on whatever context it's given, and it fills in the gaps with statistical plausibility.
This means a tech stack is no longer just a productivity tool for humans. It's a safety net for agents. The right stack catches agent mistakes before they reach production. The wrong stack lets them sail through silently.
What Makes a Stack "Agent-Friendly"?
There are three qualities that make a stack effective at catching agent errors:
1. Strong Type System = Compile-Time Guardrails
This is the most obvious one, but it's worth stating clearly. A strong type system acts as a first line of defense against the kinds of errors agents commonly make.
Consider TypeScript vs. plain JavaScript. When an agent adds a new field to a data model, TypeScript propagates that change across the codebase — every function that touches that model lights up with type errors until it's updated. In plain JS, the agent might update the model and the handler it's currently looking at, while a dozen other consumers silently break.
The same principle applies to data access. A typed ORM like Prisma or Drizzle makes it structurally impossible to write queries that reference non-existent fields or use wrong types. Raw SQL strings? The agent can generate whatever it wants, and you won't know it's wrong until runtime — or worse, until a user hits the broken path in production.
The takeaway: types turn runtime surprises into compile-time errors. For human developers, that's nice. For AI agents generating code at speed, it's essential.
2. Declarative Rules Over Imperative Logic
Here's where things get interesting. Consider how most web apps handle authorization. The typical pattern looks like this:
// In route handler A
if (user.role !== 'admin' && resource.ownerId !== user.id) {
throw new ForbiddenError();
}
// In route handler B (slightly different check)
if (user.role !== 'admin' && resource.tenantId !== user.tenantId) {
throw new ForbiddenError();
}
// In route handler C (oops, forgot the check entirely)
const data = await db.resource.findMany();
This is imperative authorization — the rules are expressed as code, scattered across dozens of files, with subtle variations that may or may not be intentional. Now imagine asking an AI agent to add a new endpoint. It needs to somehow infer the correct authorization pattern from examples, and apply it correctly. Sometimes it will. Sometimes it won't. And when it doesn't, you have a security vulnerability.
Now contrast this with a declarative approach:
model Resource {
// ...fields
@@allow('read', auth().role == 'admin' || tenantId == auth().tenantId)
@@allow('update', auth().role == 'admin' || ownerId == auth().id)
}
The rules live in one place. They're structural, not behavioral. An agent literally cannot create an endpoint that bypasses them, because the access control is enforced at the data layer, not at the route level. The more business logic lives in declarations, the less room there is for agents to introduce inconsistencies.
3. Single Source of Truth for Data Shape and Rules
AI agents perform best when context is concentrated, not scattered. This is a fundamental property of how large language models work — they reason over a context window, and the more relevant information fits within that window, the better their output.
Schema-first approaches shine here. When your data model, validation rules, and access policies all live in a single schema file, an agent can read that one file and understand the complete picture. Compare this to a codebase where the data models are in a bunch of migration files, validations are in a middleware folder, access rules are in a service layer, and business constraints are buried in utility functions. Even a skilled human developer struggles to hold all of that in their head. For an agent, it's nearly impossible.
The best stack for the AI era is one where the "spec" of your data layer fits in one place.
When the Safety Net Is Missing
To make this concrete, here are failure modes we've seen (or narrowly avoided) with AI-generated code:
The forgotten auth check. An agent writes a new API endpoint for fetching user invoices. It follows the pattern of existing endpoints — proper input validation, clean error handling, nice response formatting. But it doesn't add an authorization check, because the auth logic isn't in the ORM layer or the route middleware — it's manually applied per handler, and the agent didn't infer the pattern from context. Result: any authenticated user can read any other user's invoices.
The inconsistent validation. Your app validates email format in a Zod schema inside the signup handler, but the profile update handler has its own inline regex check, and the admin user-creation endpoint has no email validation at all. An agent is asked to add a new "invite user" endpoint. It writes one — with a slightly different email regex copied from the profile handler. Now you have four endpoints, three different validation rules, and one with none. The inconsistency is invisible at compile time and slips right through tests that only check the happy path.
The wrong tenant scope. An agent is asked to add a data export feature. It copy-pastes a query pattern from another feature, but the original query was for a single-tenant context. The new feature runs in a multi-tenant context, and the agent doesn't apply the tenant filter. Result: data leakage across tenants.
The common thread in all of these: the failures happen when rules are implicit and scattered. No single file told the agent "every query must be tenant-scoped" or "every endpoint must check ownership." The rules existed only as patterns in the codebase, and patterns are exactly what LLMs approximate — imperfectly.
How ZenStack Acts as the Safety Net
ZenStack is a TypeScript toolkit centered around a (Prisma-like) schema language called ZModel. In ZModel, you define your data model, access control rules, and validation constraints in a single file:
model Invoice {
id Int @id @default(autoincrement())
amount Float @gt(0)
owner User @relation(fields: [ownerId], references: [id])
ownerId Int
tenant Tenant @relation(fields: [tenantId], references: [id])
tenantId Int
// Access rules: declarative, co-located with the model
// no anonymous access
@@deny('all', auth() == null)
// can create for self in the context of a tenant
@@allow('create', ownerId == auth().id && tenantId == auth().tenantId)
// can read if in the same tenant
@@allow('read', tenantId == auth().tenantId)
// can update or delete if owner
@@allow('update,delete', ownerId == auth().id)
}
From this single schema, ZenStack generates a type-safe, access-controlled database client. Here's what that means for AI agents:
Authorization is structural. An agent can't "forget" to add an auth check because the check happens automatically at the data layer. Every query through ZenStack's enhanced client is filtered by the rules in the schema. The agent's job is just to write the query — the safety net handles the rest.
The context is concentrated. An agent can read the ZModel file and understand the complete data model, relationships, access rules, and validation constraints. No hunting across files to piece together the full picture.
Type safety propagates. The generated client is fully typed. If an agent writes a query that references a non-existent field or uses an invalid filter, TypeScript catches it before the code runs.
Less code to generate, less to get wrong. ZenStack and its ecosystem can automatically derive CRUD APIs, TanStack Query hooks, Zod schemas, tRPC routers, OpenAPI specs, and more. Every auto-generated artifact is one less thing an agent needs to hand-write — and one less place for bugs to hide.
Going back to the failure modes above:
- Forgotten auth check? Can't happen — ZenStack's enhanced client enforces rules automatically.
- Inconsistent validation? Unlikely — validation rules live in the schema alongside the model, so every endpoint shares the same constraints automatically.
-
Wrong tenant scope? The
tenantId == auth().tenantIdrule in the schema ensures every query is scoped correctly, regardless of which endpoint makes the query.
Shrink the Blast Radius
The broader principle here isn't specific to ZenStack. It's this: the best stacks for the AI era minimize what can go wrong when code is generated at speed.
Think of type systems as guardrails on a highway. They don't slow you down — you can still drive at full speed. But they keep you from flying off a cliff when you drift. Declarative schemas, enforced access control, and auto-generated boilerplate are the same kind of guardrails, applied to the data and security layer — which is exactly where mistakes hurt the most.
The stacks that will thrive in the agent era share a common philosophy: make the right thing automatic, and the wrong thing impossible. The more of your application's correctness invariants that are expressed as declarations rather than scattered imperative code, the safer you are — whether the code is written by a human, an agent, or some combination of both.
Choose Stacks That Protect You From Speed
AI agents are only getting faster and more autonomous. The question is no longer whether to use them — it's whether your stack can keep up safely. A good stack isn't just pleasant to work with. It absorbs the speed and imprecision of AI-generated code. It turns out that what's good for human developers is even better for AI agents: clear, concentrated, enforceable rules.
If you're evaluating your stack for the agent era, ask yourself: when an AI writes code against my data layer, what stops it from making a catastrophic mistake? If the answer is "code review" or "hoping the tests catch it," it might be time to add a stronger safety net.
Get started with ZenStack and see what a schema-first, safety-net-first stack looks like in practice.
Top comments (0)