Stack is a config-first Next.js framework with an access-control engine wrapped around every database operation. Rules live in one config; the engine enforces them everywhere — so a feature your agent ships is secure by construction, not by review.
npm create opensaas-app@latest my-appThe problem
Agents multiply how much code ships. They don't multiply how much you can read. Every generated endpoint is another place for data to leak — and the bottleneck is you.
An agent turns one prompt into hundreds of lines across routes, actions, and queries. Reviewing every data path for leaks stops scaling on day one.
When every handler hand-rolls its own checks, one forgotten where clause leaks the lot. Agents forget differently than humans do — but they forget too.
Insecure code looks identical in the happy path. It compiles, the demo passes, and the leak waits quietly for production traffic.
The turn
In Stack, application code — yours or your agent's — never touches the database directly. Every operation goes through a secured context. Access rules are declared once, in config, and applied to every query, create, update, and delete. There is no unchecked path to forget.
import { config, list } from '@opensaas/stack-core'
import { text, select, relationship } from '@opensaas/stack-core/fields'
export default config({
db: { provider: 'sqlite', url: 'file:./dev.db' },
lists: {
Post: list({
fields: {
title: text({ validation: { isRequired: true } }),
status: select({
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
],
defaultValue: 'draft',
}),
author: relationship({ ref: 'User.posts' }),
},
access: {
operation: {
// Signed-in users see published posts plus their own drafts;
// anonymous visitors see published posts only.
query: ({ session }) =>
session
? {
OR: [
{ status: { equals: 'published' } },
{ authorId: { equals: session.userId } },
],
}
: { status: { equals: 'published' } },
// Only the author can change a post — a row filter, so
// everyone else simply matches nothing.
update: ({ session }) =>
session ? { authorId: { equals: session.userId } } : false,
},
},
}),
},
})
Denied operations fail silently — null for one record, []for many — so callers can't probe for rows they aren't allowed to see.
See it hold
This is the config above, doing its job. Switch who's asking and watch the engine scope reads and refuse writes — without a single check in the feature code.
// Who's asking: an anonymous visitor
const context = await getContext()
const posts = await context.db.post.findMany()→ 2 rows returned. Rows outside the access filter never leave the database — the engine merged the rule into the query's where.
npm create opensaas-app@latestHow it works
Lists, fields, and access rules in one opensaas.config.ts.
Prisma schema, TypeScript types, and a secured context — from the config.
Features talk to context.db, never raw Prisma. That includes your agent.
A themeable admin UI at /admin, enforcing the same rules.
Included
Generated from the same config, themeable down to the token, and bound by the same access rules — fields a session can't read simply aren't there.



Agent-ready
Works with any coding agent. Deepest with Claude Code.
Scaffolded projects ship the guidance an agent needs to build inside the guardrails — patterns, conventions, and the paths it must use.
Every list can be exposed to AI assistants over MCP — and every tool call passes through the same access-control engine as the app itself.
Install the Stack plugin and describe features in plain language — the agent scaffolds lists, access rules, and UI within the framework's conventions.
Coming from Keystone? Stack speaks your schema — Keystone-compatible schema generation means you can adopt an existing database without a destructive migration.
Migration guideScaffold an app, open it in your agent, and describe what you want. The guardrails are already in place.
npm create opensaas-app@latest my-appStack is in beta: the onboarding path and core APIs are stable enough to build on — pin versions for production.