How to Structure a Next.js + Supabase Project for AI-Assisted Development with Claude Code
AI coding agents like Claude Code are powerful, but they can create inconsistent patterns and missing security measures if your project structure doesn't enforce constraints. Here's how to structure Next.js + Supabase projects for reliable AI-assisted development.
The Problem With AI-Generated Code at Scale
If you're using Claude Code or other AI agents to accelerate development, you've probably experienced this: the agent writes working code that solves the immediate problem. But three days later, you realize it's using a different connection pattern to Supabase than the code from yesterday, your authentication is inconsistent between pages, and there's zero observability into what's actually happening at runtime.
This isn't Claude Code's fault. It's a fundamental problem with unstructured AI development. Without clear conventions and architectural constraints in your codebase, AI agents will generate valid but inconsistent solutions to the same problems repeatedly.
The Hacker News discussion about "How I use Claude Code: Separation of planning and execution" highlighted exactly this issue. Teams that treat AI agents as code generators without structural guardrails end up with fragile applications that work locally but break in production.
Why Project Structure Matters for AI Development
When you're working with human developers, code review catches inconsistencies. When you're using AI agents to generate dozens of features simultaneously, you need the codebase itself to enforce patterns.
Here's what happens without structure:
The solution is a deliberate project structure that makes the right patterns the easiest patterns for an AI agent to follow.
The Production-Ready Next.js + Supabase Structure
Here's the pattern that prevents these problems:
```
my-saas-app/
├── src/
│ ├── app/ # Next.js app router (pages & layouts only)
│ ├── server/ # Server-only code
│ │ ├── db/
│ │ │ ├── client.ts # Single shared Supabase instance
│ │ │ ├── rls-safe.ts # RLS-enforced queries
│ │ │ └── schema.ts # Type-safe table definitions
│ │ ├── auth/
│ │ │ ├── session.ts # Session management
│ │ │ └── middleware.ts # Auth middleware
│ │ └── actions/ # Server actions (the only way to DB)
│ ├── client/ # Client-side code
│ │ ├── components/ # UI components only
│ │ ├── hooks/ # Client hooks
│ │ └── lib/ # Client utilities
│ ├── api/ # Optional: API routes for integrations
│ ├── agents/ # Claude Code operating knowledge
│ │ ├── CLAUDE.md # System prompt & constraints
│ │ ├── ARCHITECTURE.md # Project structure rules
│ │ └── PATTERNS.md # Copy-paste examples
│ └── types/ # Shared TypeScript types
├── .env.local # Secrets (Supabase keys)
└── supabase/
└── migrations/ # Database schema version control
```
The critical insight here: the structure itself teaches the AI agent where code belongs. Agents follow directory patterns. If they can't write to `src/app/`, they write server actions instead.
Locking Down Data Access
The biggest security failure with AI-generated code is missing Row Level Security (RLS). Without it, anyone with your public Supabase key can read your entire database.
Create a single `src/server/db/rls-safe.ts` that serves as the only interface to your database:
```typescript
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL,
process.env.SUPABASE_SERVICE_ROLE_KEY // Service role, never expose to client
);
export async function getOrgData(orgId: string, userId: string) {
// RLS policy checks orgId matches user's org AND user has permission
const { data, error } = await supabase
.from('organizations')
.select('*')
.eq('id', orgId)
.single();
if (error) throw error;
return data;
}
export async function updateUserProfile(userId: string, updates: Record<string, any>) {
// RLS policy checks that user can only update their own profile
const { data, error } = await supabase
.from('profiles')
.update(updates)
.eq('id', userId)
.single();
if (error) throw error;
return data;
}
```
Every database query goes through this module. When an AI agent needs to fetch data, it calls these functions. It can't accidentally create direct queries.
Then in Supabase, enable RLS on all tables and create policies:
```sql
CREATE POLICY "users_can_read_own_profile" ON profiles
FOR SELECT
USING (auth.uid() = id);
CREATE POLICY "orgs_select_members_only" ON organizations
FOR SELECT
USING (id IN (SELECT org_id FROM org_members WHERE user_id = auth.uid()));
```
Now when an agent tries to query, RLS policies enforce it at the database layer.
Observability Before It's A Problem
AI agents write working code, not instrumented code. Add observability hooks in your server actions layer before your agents create blind spots.
```typescript
// src/server/actions/user.ts
import { createServerAction } from '@/lib/safe-action';
import { logAction } from '@/server/observability';
export const updateUserAction = createServerAction()
.action(async ({ userId, updates }) => {
logAction('user_update_started', { userId, updateKeys: Object.keys(updates) });
try {
const result = await updateUserProfile(userId, updates);
logAction('user_update_success', { userId, resultId: result.id });
return result;
} catch (error) {
logAction('user_update_failed', { userId, error: error.message });
throw error;
}
});
```
This pattern ensures that every data mutation gets logged before the agent even writes the feature. You'll see what's happening at runtime.
Creating Agent Constraints
Store your project's architectural rules in source control so Claude Code agents (or any future developers) follow them:
Create `src/agents/CLAUDE.md`:
```
You are building a SaaS application with Next.js and Supabase.
ROUTING RULES:
DATABASE RULES:
AUTH RULES:
When writing new features, copy patterns from PATTERNS.md.
```
Create `src/agents/PATTERNS.md` with real examples from your codebase:
```
PATTERN: Creating a server action with RLS safety
export const myAction = createServerAction()
.action(async ({ userId, data }) => {
return await updateUserProfile(userId, data);
});
PATTERN: Fetching protected data
const data = await getOrgData(orgId, currentUserId);
// RLS checks this automatically
PATTERN: Client component using server action
export default function MyComponent() {
const [result, action] = useServerAction(myAction);
return <button onClick={() => action({ userId, data })}>Save</button>;
}
```
Now when Claude Code generates a new feature, it's copying tested patterns instead of improvising.
Why This Matters for Production
The structure above solves the three critical problems with AI-generated code:
This isn't theoretical. Teams using ZipBuild to generate SaaS scaffolds with this structure report 40% fewer post-generation security fixes and significantly faster onboarding for both human developers and AI agents.
Start With the Right Foundation
The biggest mistake is applying this structure after you've built features ad-hoc. It's exponentially harder to enforce later.
If you're starting a new project or adding AI-assisted development to an existing one, define your structure first, then let the agents work within constraints.
The agents are more powerful when the codebase itself enforces what's right.
Try the free discovery chat at zipbuild.dev to see how properly structured scaffolding accelerates AI-assisted development while keeping your application secure and maintainable.
Written by ZipBuild Team
Ready to build with structure?
Try the free discovery chat and see how ZipBuild architects your idea.
Start Building