Make SQLite database self-create on serverless hosts

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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VL5YUEf2B9LG4xdVUQwcSg
This commit is contained in:
Claude committed 2026-09-25 11:43:08 +00:00
1 parent 4e8a2d9037
commit 7b4f05daab
7 files changed
+63 -6

No files matched your search

+9
View File
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->
# 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.
<!-- END:nextjs-agent-rules -->
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+3 -1
View File
@@ -2,7 +2,7 @@ import { NextResponse } from "next/server";
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { z } from "zod"; import { z } from "zod";
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/prisma"; import { prisma, ensureDb } from "@/lib/prisma";
const updateSchema = z.object({ const updateSchema = z.object({
bio: z.string().trim().max(500).optional(), bio: z.string().trim().max(500).optional(),
@@ -10,6 +10,8 @@ const updateSchema = z.object({
}); });
export async function PATCH(request: Request) { export async function PATCH(request: Request) {
await ensureDb();
const session = await getServerSession(authOptions); const session = await getServerSession(authOptions);
if (!session?.user?.id) { if (!session?.user?.id) {
return NextResponse.json({ error: "Not signed in" }, { status: 401 }); return NextResponse.json({ error: "Not signed in" }, { status: 401 });
+3 -1
View File
@@ -1,7 +1,7 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { z } from "zod"; import { z } from "zod";
import { prisma } from "@/lib/prisma"; import { prisma, ensureDb } from "@/lib/prisma";
const signupSchema = z.object({ const signupSchema = z.object({
name: z.string().trim().min(1, "Name is required").max(80), 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) { export async function POST(request: Request) {
await ensureDb();
const body = await request.json().catch(() => null); const body = await request.json().catch(() => null);
if (!body) { if (!body) {
return NextResponse.json({ error: "Invalid request body" }, { status: 400 }); return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
+3 -1
View File
@@ -1,7 +1,7 @@
import { getServerSession } from "next-auth"; import { getServerSession } from "next-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { authOptions } from "@/lib/auth"; import { authOptions } from "@/lib/auth";
import { prisma } from "@/lib/prisma"; import { prisma, ensureDb } from "@/lib/prisma";
import ProfileEditor from "./profile-editor"; import ProfileEditor from "./profile-editor";
import SignOutButton from "./sign-out-button"; import SignOutButton from "./sign-out-button";
@@ -11,6 +11,8 @@ export default async function ProfilePage() {
redirect("/login"); redirect("/login");
} }
await ensureDb();
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { id: session.user.id }, where: { id: session.user.id },
select: { id: true, name: true, email: true, age: true, bio: true }, select: { id: true, name: true, email: true, age: true, bio: true },
+3 -1
View File
@@ -1,7 +1,7 @@
import type { AuthOptions } from "next-auth"; import type { AuthOptions } from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials"; import CredentialsProvider from "next-auth/providers/credentials";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import { prisma } from "@/lib/prisma"; import { prisma, ensureDb } from "@/lib/prisma";
export const authOptions: AuthOptions = { export const authOptions: AuthOptions = {
session: { strategy: "jwt" }, session: { strategy: "jwt" },
@@ -18,6 +18,8 @@ export const authOptions: AuthOptions = {
async authorize(credentials) { async authorize(credentials) {
if (!credentials?.email || !credentials?.password) return null; if (!credentials?.email || !credentials?.password) return null;
await ensureDb();
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
where: { email: credentials.email.toLowerCase() }, where: { email: credentials.email.toLowerCase() },
}); });
+41 -2
View File
@@ -1,9 +1,48 @@
import { PrismaClient } from "@prisma/client"; import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; const globalForPrisma = globalThis as unknown as {
prisma?: PrismaClient;
dbReady?: Promise<void>;
};
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") { if (process.env.NODE_ENV !== "production") {
globalForPrisma.prisma = prisma; 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<void> {
if (!globalForPrisma.dbReady) {
globalForPrisma.dbReady = prisma
.$executeRawUnsafe(CREATE_USER_TABLE)
.then(() => prisma.$executeRawUnsafe(CREATE_USER_EMAIL_INDEX))
.then(() => undefined);
}
await globalForPrisma.dbReady;
}