plans/neondb-integration.md
Generated by swarm planning session on 2026-02-17
Elevate Neon from a Portal-template-only experiment to a first-class database integration on par with Supabase. Users will be able to connect Neon to any Next.js app, with full AI agent support for schema introspection, SQL execution, and code generation — all without requiring the Portal template. Next.js apps use DATABASE_URL with server-side code (API routes, Server Actions).
Today, Neon in Dyad is a second-class citizen:
portal-mini-store has requiresNeon: true — users cannot use Neon with Next.js templatesexecute_sql, get_table_schema, or get_project_info equivalents for NeonNeonConnector lives on the hub page, not per-app like SupabaseConnectorneon:create-project exists)This violates the Backend-Flexible design principle — users who want serverless Postgres without a full BaaS (Supabase) have no viable path in Dyad.
get_neon_project_info, get_neon_table_schema, execute_neon_sqladd_integration tool support for provider: "neon"<dyad-execute-sql> tag support routed to Neon when Neon is the active providerenableNeonWriteSqlMigration flagAs a user creating a new Next.js app, I want to connect my Neon account and have the AI agent set up my database schema, so that I get a working database-backed app without manual SQL or connection configuration.
As a user with an existing Neon project, I want to link it to my Dyad app by selecting it from a dropdown, so that I don't have to create a new project or copy connection strings.
As a user building a CRUD app, I want to tell the agent "add a tasks table and build a task list page" and have it execute the SQL on Neon and generate the React components, so that I get an end-to-end working feature in one step.
As a user who chose Neon over Supabase, I want the AI to recommend Neon Auth for authentication, so that I get a secure, production-ready auth solution that lives in my Neon database and branches with it.
As a user working on a feature branch, I want to select my Neon development branch and have the agent execute SQL against it, so that I can iterate safely without affecting production data.
neon:list-projects<dyad-add-integration provider="neon"> promptget_neon_project_info to check existing tables<dyad-execute-sql> tag| State | Description | Visual |
|---|---|---|
| Not Connected | "Connect to Neon" card with Neon logo and brief description | Card with connect button |
| Connected, No Project | Project selector dropdown visible, loading projects | Select component with skeleton |
| Connected, Project Selected | Project name in Badge, branch selector visible, "Open in Console" link | Full integration card |
| Loading | Skeleton placeholders for project/branch lists | Skeleton components |
| Error | Red text with retry button and actionable message | Error state with CTA |
neonProjectId from app recordaria-label (e.g., "production branch", not just color)aria-describedbyaria-live regionprefers-reduced-motionArchitecture pattern for generated Next.js apps:
Next.js (server-side access via DATABASE_URL):
┌─────────────────────────────────────────────┐
│ Layer 3: Auth (Neon Auth) │
├─────────────────────────────────────────────┤
│ Layer 2: Backend │
│ Next.js: API Routes / Server Actions │
├─────────────────────────────────────────────┤
│ Layer 1: Database │
│ @neondatabase/serverless + Drizzle ORM │
│ Connection via DATABASE_URL env var │
└─────────────────────────────────────────────┘
Within Dyad itself (management plane):
┌─────────────────────────────────────────┐
│ Agent Tools (get_neon_*, execute_sql) │
├─────────────────────────────────────────┤
│ Neon Context Module (neon_context.ts) │
├─────────────────────────────────────────┤
│ Neon Management Client (existing) │
│ @neondatabase/serverless (SQL execution) │
│ @neondatabase/api-client (project CRUD) │
├─────────────────────────────────────────┤
│ IPC Handlers (neon_handlers.ts) │
├─────────────────────────────────────────┤
│ Settings Storage (encrypted credentials) │
└─────────────────────────────────────────┘
| Component | File(s) | Change Type |
|---|---|---|
| Agent Context | src/pro/main/ipc/handlers/local_agent/tools/types.ts | Modify — add neonProjectId, neonDevelopmentBranchId, neonActiveBranchId, frameworkType |
| Agent Handler | src/pro/main/ipc/handlers/local_agent/local_agent_handler.ts | Modify — populate Neon context from chat.app.* |
| Tool Definitions | src/pro/main/ipc/handlers/local_agent/tool_definitions.ts | Modify — register 3 new Neon tools |
| Add Integration | src/pro/main/ipc/handlers/local_agent/tools/add_integration.ts | Modify — add "neon" to supported providers (Next.js) |
| Neon Context | src/neon_admin/neon_context.ts | New — getNeonClientCode(), getNeonContext(), getNeonProjectInfo(), getNeonTableSchema(), executeNeonSql() (extract SQL execution from src/ipc/utils/neon_timestamp_utils.ts) |
| Neon Project Info Tool | src/pro/main/ipc/handlers/local_agent/tools/get_neon_project_info.ts | New — mirrors get_supabase_project_info.ts |
| Neon Table Schema Tool | src/pro/main/ipc/handlers/local_agent/tools/get_neon_table_schema.ts | New — mirrors get_supabase_table_schema.ts |
| Execute Neon SQL Tool | src/pro/main/ipc/handlers/local_agent/tools/execute_neon_sql.ts | New — mirrors execute_sql.ts with @neondatabase/serverless |
| Neon System Prompt | src/prompts/neon_prompt.ts | New — DB setup, API routes (Next.js), auth guidance, security rules |
| Chat Stream Handlers | src/ipc/handlers/chat_stream_handlers.ts | Modify — inject Neon prompt when neonProjectId present |
| Response Processor | src/ipc/processors/response_processor.ts | Modify — route <dyad-execute-sql> to Neon executor when Neon is active |
| Neon IPC Handlers | src/ipc/handlers/neon_handlers.ts | Modify — add neon:list-projects, neon:set-app-project, neon:get-connection-uri, neon:execute-sql |
| Neon IPC Types | src/ipc/types/neon.ts | Modify — add new contract schemas |
| App Details Page | src/pages/app-details.tsx | Modify — add NeonProjectSelector component |
| Neon Connector | src/components/NeonConnector.tsx | Refactor — use Card components, add per-app project/branch selection |
| Neon Integration | src/components/NeonIntegration.tsx | Enhance — show project info like SupabaseIntegration |
| DB Schema | src/db/schema.ts | Modify — add neonActiveBranchId column |
Existing columns (already in apps table — no migration needed):
neonProjectId: string | nullneonDevelopmentBranchId: string | nullneonPreviewBranchId: string | nullNew column needed:
neonActiveBranchId: string | null — tracks which branch the agent's SQL execution targets (defaults to development branch)Agent context additions:
interface AgentContext {
// ... existing fields
neonProjectId: string | null;
neonDevelopmentBranchId: string | null;
neonActiveBranchId: string | null;
frameworkType: "nextjs" | "vite" | "other" | null; // Detected from appPath (e.g., check for next.config.*)
}
Dependencies for generated Next.js apps:
@neondatabase/auth (Neon Auth server SDK for Next.js)@neondatabase/neon-js (Neon Auth client SDK — provides auth, auth/react/ui)@neondatabase/serverless (serverless Postgres driver)drizzle-orm + drizzle-kit (ORM and migrations)New IPC contracts in src/ipc/types/neon.ts:
| Contract | Input | Output |
|---|---|---|
neon:list-projects | {} | { projects: NeonProject[] } |
neon:set-app-project | { appId, projectId, branchIds } | { success: boolean } |
neon:unset-app-project | { appId } | { success: boolean } |
neon:execute-sql | { appId, query } | { result: string } |
neon:get-connection-uri | { appId } | { connectionUri: string } |
neon:get-table-schema | { appId, tableName? } | { schema: string } |
neon:set-active-branch | { appId, branchId } | { success: boolean } |
Next.js apps: Connection string is a full-access credential
Neon's DATABASE_URL connection string gives full read/write database access. The system prompt MUST:
DATABASE_URL in client-side code@neondatabase/serverless in React components or browser codeapp/api/).env.local, not .env)src/db/index.ts)import { neon } from "@neondatabase/serverless";
import { drizzle } from "drizzle-orm/neon-http";
import * as schema from "./schema";
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
src/db/schema.ts)import { pgTable, uuid, text, timestamp, boolean } from "drizzle-orm/pg-core";
export const users = pgTable("users", {
id: uuid("id").defaultRandom().primaryKey(),
email: text("email").notNull().unique(),
name: text("name"),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
export const todos = pgTable("todos", {
id: uuid("id").defaultRandom().primaryKey(),
title: text("title").notNull(),
completed: boolean("completed").default(false).notNull(),
userId: uuid("user_id")
.references(() => users.id)
.notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
});
app/api/todos/route.ts)import { db } from "@/db";
import { todos } from "@/db/schema";
import { eq } from "drizzle-orm";
import { auth } from "@/lib/auth/server";
import { headers } from "next/headers";
export async function GET() {
const session = await auth.getSession({ headers: await headers() });
if (!session?.user?.id) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const userTodos = await db
.select()
.from(todos)
.where(eq(todos.userId, session.user.id));
return Response.json(userTodos);
}
export async function POST(request: Request) {
const session = await auth.getSession({ headers: await headers() });
if (!session?.user?.id) {
return Response.json({ error: "Unauthorized" }, { status: 401 });
}
const { title } = await request.json();
const [newTodo] = await db
.insert(todos)
.values({ title, userId: session.user.id })
.returning();
return Response.json(newTodo, { status: 201 });
}
lib/auth/server.ts)Neon Auth is a managed auth service powered by Better Auth. Auth data is stored in your Neon database and branches automatically with database branches.
import { createNeonAuth } from "@neondatabase/auth/next/server";
export const auth = createNeonAuth({
baseUrl: process.env.NEON_AUTH_BASE_URL!,
cookies: { secret: process.env.NEON_AUTH_COOKIE_SECRET! },
});
app/api/auth/[...path]/route.ts)import { auth } from "@/lib/auth/server";
export const { GET, POST } = auth.handler();
src/lib/auth-client.ts)import { createAuthClient } from "@neondatabase/neon-js/auth";
export const authClient = createAuthClient("/api/auth");
// Provides: useSession(), signIn.email(), signOut(), etc.
Neon Auth provides pre-built UI components for sign-in/sign-up:
import {
NeonAuthUIProvider,
AuthView,
} from "@neondatabase/neon-js/auth/react/ui";
import { authClient } from "@/lib/auth-client";
export function AuthPage() {
return (
<NeonAuthUIProvider authClient={authClient}>
<AuthView pathname="sign-in" />
</NeonAuthUIProvider>
);
}
src/components/TodoList.tsx)"use client";
import { useEffect, useState } from "react";
interface Todo {
id: string;
title: string;
completed: boolean;
}
export function TodoList() {
const [todos, setTodos] = useState<Todo[]>([]);
const [newTitle, setNewTitle] = useState("");
useEffect(() => {
fetch("/api/todos")
.then((r) => r.json())
.then(setTodos);
}, []);
async function addTodo(e: React.FormEvent) {
e.preventDefault();
const res = await fetch("/api/todos", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: newTitle }),
});
const todo = await res.json();
setTodos([...todos, todo]);
setNewTitle("");
}
return (
<div>
<form onSubmit={addTodo}>
<input
value={newTitle}
onChange={(e) => setNewTitle(e.target.value)}
placeholder="Add a todo..."
/>
<button type="submit">Add</button>
</form>
<ul>
{todos.map((todo) => (
<li key={todo.id}>
{todo.title} {todo.completed ? "(done)" : ""}
</li>
))}
</ul>
</div>
);
}
.env.local)# Neon Database (injected by Dyad)
DATABASE_URL=postgresql://user:[email protected]/dbname?sslmode=require
# Neon Auth (managed by Neon, values from Neon Console > Auth settings)
NEON_AUTH_BASE_URL=https://auth.neon.tech/... # Auth service endpoint
NEON_AUTH_COOKIE_SECRET=your-cookie-secret-here # Secret for session cookies
Next.js pattern:
// getNeonClientCode() for Next.js:
// "To connect to the Neon database, use this pattern:
//
// import { neon } from '@neondatabase/serverless';
// import { drizzle } from 'drizzle-orm/neon-http';
//
// const sql = neon(process.env.DATABASE_URL!);
// export const db = drizzle(sql);
//
// IMPORTANT: Only use this in server-side code (API routes, server actions, server components).
// NEVER import @neondatabase/serverless in client-side React components."
neonProjectId, neonDevelopmentBranchId, neonActiveBranchId, frameworkType to AgentContext in types.tslocal_agent_handler.ts from chat.app.*frameworkType via framework detection (check for next.config.* in appPath, same pattern as vercel_handlers.ts)"neon" to SUPPORTED_PROVIDERS in add_integration.ts (Next.js and React/Vite)neonActiveBranchId column to apps table in schema.tsNeonConnector to app-details page using Card components (match SupabaseConnector pattern)neon:list-projects IPC handler (wire up API client's listProjects)neon:set-app-project and neon:unset-app-project IPC handlerssrc/neon_admin/neon_context.ts with:
executeNeonSql() — uses @neondatabase/serverless (extract from neon_timestamp_utils.ts)getNeonProjectInfo() — project ID, branches, table names via information_schemagetNeonTableSchema() — columns, constraints, indexes via information_schemagetNeonClientCode(frameworkType) — generates framework-specific boilerplate: Drizzle + @neondatabase/serverless for Next.js, createClient + Data API for React/VitegetNeonContext() — full context for agent promptget_neon_project_info.ts agent tool (mirrors get_supabase_project_info.ts)get_neon_table_schema.ts agent tool (mirrors get_supabase_table_schema.ts)execute_neon_sql.ts agent tool (mirrors execute_sql.ts, uses serverless driver)tool_definitions.tsneon:execute-sql, neon:get-connection-uri, neon:get-table-schema IPC contractsresponse_processor.ts to route <dyad-execute-sql> to Neon executor when neonProjectId is setneon:set-active-branch IPC handler for branch switchingsrc/prompts/neon_prompt.ts with framework-conditional sections:
DATABASE_URL), Drizzle ORM setup, API route / Server Action patterns, @neondatabase/auth server SDK + @neondatabase/neon-js client SDKcreateClient<Database> with BetterAuthReactAdapter + auth/dataApi URLs), PostgREST-style query patterns (.from().select().eq(), .insert().select().single()), RLS policies with TO "authenticated" role required on all tables, @neondatabase/neon-js client SDK only (no server SDK), NEVER use DATABASE_URL or @neondatabase/serverlessframeworkType from AgentContextNEON_NOT_AVAILABLE_SYSTEM_PROMPT (parallel to Supabase's)chat_stream_handlers.ts (conditional on neonProjectId)neonActiveBranchId on app recordNeonIntegration.tsx settings page (show project info, usage)neonProjectId, show connected state)neon_context.ts functions with mocked @neondatabase/serverlessneon_context.ts functions with mocked @neondatabase/serverless and @neondatabase/api-clientAgentContext with neonProjectId set/unset, verify tools enable/disable correctlyneon:fake-connect fixture for Next.js and React/Vite templates<dyad-execute-sql>, verifies resultget_neon_table_schema returns correct columnsneonProjectId present, NOT when Supabase connectedDATABASE_URL usage in generated codeDATABASE_URL or @neondatabase/serverless| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Connection string exposed in client code | Medium | High | Next.js: system prompt rule #1 NEVER client-side. React/Vite: uses Data API instead, no connection string needed. |
| AI generates insecure homegrown auth | Medium | High | Prompt explicitly forbids JWT+bcrypt, recommends Neon Auth (Better Auth) only. Auth data lives in Neon DB. |
| Neon free tier quota exhaustion | Low | Medium | Document in system prompt. Consider surfacing usage in get_neon_project_info. |
| Branch switching causes data confusion | Low | Medium | Toast notification on branch change. Agent prompt mentions active branch. |
| Portal template users lose existing connection | Low | High | Detect existing neonProjectId in new flow, show connected state. |
| On-demand connection URI fetch latency | Medium | Low | Acceptable for v1 (~200-500ms). Cache in v2 if needed. |
| Missing RLS policies in React/Vite apps | Medium | High | Data API exposes full table without RLS. System prompt must generate RLS policies for every table. Agent should run ALTER TABLE ... ENABLE ROW LEVEL SECURITY as part of table creation. |
| Data API not enabled on Neon project | Medium | Medium | Agent checks Data API status via get_neon_project_info. System prompt guides user to enable it in Neon Console. |
Neon free tier limits: Should get_neon_project_info surface current usage (storage, compute hours)? This would help users and the agent avoid heavy operations that burn through quotas.
NeonConfigure preview panel: The existing NeonConfigure.tsx shows branch visualization in the preview panel for Portal apps. Should this be removed/replaced by the new branch selector in the integration card, or kept as a read-only status display?
Mutual exclusivity enforcement: When a user has Supabase connected and tries to connect Neon (or vice versa), should we show a warning dialog, or silently hide the other provider's connect button?
| Decision | Reasoning | Alternatives Considered |
|---|---|---|
| Next.js and React/Vite in v1 | Next.js uses DATABASE_URL server-side. React/Vite uses Neon Data API (managed REST proxy with JWT validation + RLS), eliminating the need for a server layer or exposed credentials. | Next.js only (unnecessarily restrictive now that Data API exists) |
| Recommend Neon Auth | Neon now provides built-in auth via Better Auth. Auth data stored in the Neon database, branches with database. No external auth providers needed. | NextAuth.js (separate config + secrets); Clerk (external SaaS); homegrown JWT+bcrypt (security risk) |
| Full branch selector in v1 | Branching is Neon's key differentiator over Supabase. Color-coded badges provide clear visual hierarchy. | Default to dev only (simpler); Read-only display (compromise) |
| Mutually exclusive providers per app | Agent prompt can't cleanly handle both Supabase and Neon contexts. Avoids ambiguity in SQL execution target. | Allow both (complex, no clear user value) |
| SQL execution in Dyad (management plane) | Matches Supabase pattern. Agent needs to create tables and seed data during build. Credentials already stored. | Only in generated app (limits agent capabilities) |
| Fetch connection URI on-demand | Consistent with existing neon_timestamp_utils.ts pattern. Avoids credential rotation complexity. | Cache (faster but more complex) |
@neondatabase/serverless for SQL | Already a dependency. HTTP-based, works in Electron. Lower latency than Management API for queries. | Management API SQL endpoint (higher latency, fewer features) |
Generated by dyad:swarm-to-plan