From 7b4f05daabc850dddb5f79f4bcca1730683d7e3b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 11:43:08 +0000 Subject: [PATCH] Make SQLite database self-create on serverless hosts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vercel's serverless functions only allow writes to /tmp, and it starts empty on every cold start. Point the database there in production and create the User table on first request if it doesn't exist yet, so the app can be deployed for a live preview without manual database setup. Note: this is a demo-friendly stand-in — data does not persist across cold starts on Vercel. A real persistent database is a later phase. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01VL5YUEf2B9LG4xdVUQwcSg --- AGENTS.md | 9 ++++++++ CLAUDE.md | 1 + src/app/api/profile/route.ts | 4 +++- src/app/api/signup/route.ts | 4 +++- src/app/profile/page.tsx | 4 +++- src/lib/auth.ts | 4 +++- src/lib/prisma.ts | 43 ++++++++++++++++++++++++++++++++++-- 7 files changed, 63 insertions(+), 6 deletions(-) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..643577d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ + + +# This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..43c994c --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +@AGENTS.md diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts index 6b23dcd..08c87e9 100644 --- a/src/app/api/profile/route.ts +++ b/src/app/api/profile/route.ts @@ -2,7 +2,7 @@ import { NextResponse } from "next/server"; import { getServerSession } from "next-auth"; import { z } from "zod"; import { authOptions } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { prisma, ensureDb } from "@/lib/prisma"; const updateSchema = z.object({ bio: z.string().trim().max(500).optional(), @@ -10,6 +10,8 @@ const updateSchema = z.object({ }); export async function PATCH(request: Request) { + await ensureDb(); + const session = await getServerSession(authOptions); if (!session?.user?.id) { return NextResponse.json({ error: "Not signed in" }, { status: 401 }); diff --git a/src/app/api/signup/route.ts b/src/app/api/signup/route.ts index 8ed8085..64c6b34 100644 --- a/src/app/api/signup/route.ts +++ b/src/app/api/signup/route.ts @@ -1,7 +1,7 @@ import { NextResponse } from "next/server"; import bcrypt from "bcryptjs"; import { z } from "zod"; -import { prisma } from "@/lib/prisma"; +import { prisma, ensureDb } from "@/lib/prisma"; const signupSchema = z.object({ name: z.string().trim().min(1, "Name is required").max(80), @@ -11,6 +11,8 @@ const signupSchema = z.object({ }); export async function POST(request: Request) { + await ensureDb(); + const body = await request.json().catch(() => null); if (!body) { return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); diff --git a/src/app/profile/page.tsx b/src/app/profile/page.tsx index 23e2b30..8e15d40 100644 --- a/src/app/profile/page.tsx +++ b/src/app/profile/page.tsx @@ -1,7 +1,7 @@ import { getServerSession } from "next-auth"; import { redirect } from "next/navigation"; import { authOptions } from "@/lib/auth"; -import { prisma } from "@/lib/prisma"; +import { prisma, ensureDb } from "@/lib/prisma"; import ProfileEditor from "./profile-editor"; import SignOutButton from "./sign-out-button"; @@ -11,6 +11,8 @@ export default async function ProfilePage() { redirect("/login"); } + await ensureDb(); + const user = await prisma.user.findUnique({ where: { id: session.user.id }, select: { id: true, name: true, email: true, age: true, bio: true }, diff --git a/src/lib/auth.ts b/src/lib/auth.ts index 6bf2a9a..38e2642 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,7 +1,7 @@ import type { AuthOptions } from "next-auth"; import CredentialsProvider from "next-auth/providers/credentials"; import bcrypt from "bcryptjs"; -import { prisma } from "@/lib/prisma"; +import { prisma, ensureDb } from "@/lib/prisma"; export const authOptions: AuthOptions = { session: { strategy: "jwt" }, @@ -18,6 +18,8 @@ export const authOptions: AuthOptions = { async authorize(credentials) { if (!credentials?.email || !credentials?.password) return null; + await ensureDb(); + const user = await prisma.user.findUnique({ where: { email: credentials.email.toLowerCase() }, }); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts index a309320..6022f84 100644 --- a/src/lib/prisma.ts +++ b/src/lib/prisma.ts @@ -1,9 +1,48 @@ import { PrismaClient } from "@prisma/client"; -const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; +const globalForPrisma = globalThis as unknown as { + prisma?: PrismaClient; + dbReady?: Promise; +}; -export const prisma = globalForPrisma.prisma ?? new PrismaClient(); +// On Vercel's serverless functions, only /tmp is writable, and it starts +// empty on every cold start. Point SQLite there instead of the repo-relative +// path used for local development. This is a demo-friendly stand-in until a +// real hosted database is wired up (data does not persist across cold starts). +const databaseUrl = process.env.VERCEL ? "file:/tmp/dev.db" : process.env.DATABASE_URL; + +export const prisma = + globalForPrisma.prisma ?? + new PrismaClient({ + datasources: { db: { url: databaseUrl } }, + }); if (process.env.NODE_ENV !== "production") { globalForPrisma.prisma = prisma; } + +const CREATE_USER_TABLE = ` +CREATE TABLE IF NOT EXISTS "User" ( + "id" TEXT NOT NULL PRIMARY KEY, + "email" TEXT NOT NULL, + "passwordHash" TEXT NOT NULL, + "name" TEXT NOT NULL, + "age" INTEGER NOT NULL, + "bio" TEXT NOT NULL DEFAULT '', + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +`; +const CREATE_USER_EMAIL_INDEX = `CREATE UNIQUE INDEX IF NOT EXISTS "User_email_key" ON "User"("email");`; + +// Idempotent, so it's safe to call before every request. Needed because on +// Vercel the /tmp SQLite file is missing entirely on a fresh cold start. +export async function ensureDb(): Promise { + if (!globalForPrisma.dbReady) { + globalForPrisma.dbReady = prisma + .$executeRawUnsafe(CREATE_USER_TABLE) + .then(() => prisma.$executeRawUnsafe(CREATE_USER_EMAIL_INDEX)) + .then(() => undefined); + } + await globalForPrisma.dbReady; +}