From d8ff5f4bb1bd0054799406ca82812fd4bf3f2349 Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Fri, 24 Apr 2026 14:33:37 -0700 Subject: [PATCH 1/7] feat: add users and sessions tables for Better Auth --- src/lib/db/schema.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/lib/db/schema.ts b/src/lib/db/schema.ts index 7335881..33407e4 100644 --- a/src/lib/db/schema.ts +++ b/src/lib/db/schema.ts @@ -9,6 +9,27 @@ import { index, } from "drizzle-orm/pg-core"; +export const users = pgTable("users", { + id: text("id").primaryKey(), + name: text("name"), + email: text("email").notNull().unique(), + emailVerified: boolean("email_verified").default(false), + image: text("image"), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + +export const sessions = pgTable("sessions", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id, { onDelete: "cascade" }), + token: text("token").notNull().unique(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at").defaultNow().notNull(), +}); + export const comparisons = pgTable( "comparisons", { From 3568e2f008c8409e5a58f137bfab02b78a275203 Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Fri, 24 Apr 2026 14:33:54 -0700 Subject: [PATCH 2/7] feat: update Better Auth config with schema and session expiry --- src/lib/auth.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/auth.ts b/src/lib/auth.ts index dd8d37b..2599b7e 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,8 +1,10 @@ import { betterAuth } from "better-auth"; import { drizzleAdapter } from "better-auth/adapters/drizzle"; import { db } from "./db"; +import * as schema from "./db/schema"; export const auth = betterAuth({ - database: drizzleAdapter(db, { provider: "pg" }), + database: drizzleAdapter(db, { provider: "pg", schema }), emailAndPassword: { enabled: true }, + session: { expiresIn: 60 * 60 * 24 * 7 }, }); From 2c2fd3547c556c2ecfca56907e8d4444cd50e2f3 Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Fri, 24 Apr 2026 14:34:13 -0700 Subject: [PATCH 3/7] feat: add auth middleware protecting /compare and /profile routes --- src/middleware.ts | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/middleware.ts b/src/middleware.ts index da56471..c88a27c 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,20 +1,29 @@ import { NextRequest, NextResponse } from "next/server"; import { auth } from "@/lib/auth"; -const publicPaths = ["/sign-in", "/sign-up", "/api/auth"]; +const publicPaths = ["/", "/explore", "/sign-in", "/sign-up", "/api/auth"]; +const protectedPaths = ["/compare", "/profile"]; export async function middleware(request: NextRequest) { const { pathname } = request.nextUrl; + if ( + pathname.startsWith("/_next") || + pathname.startsWith("/favicon") || + pathname.includes(".") + ) { + return NextResponse.next(); + } + const isPublic = publicPaths.some( (path) => pathname === path || pathname.startsWith(path + "/"), ); - if (isPublic) { - return NextResponse.next(); - } + const isProtected = protectedPaths.some( + (path) => pathname === path || pathname.startsWith(path + "/"), + ); - if (pathname.startsWith("/_next") || pathname.startsWith("/favicon")) { + if (isPublic && !isProtected) { return NextResponse.next(); } @@ -22,7 +31,7 @@ export async function middleware(request: NextRequest) { headers: request.headers, }); - if (!session) { + if (!session && isProtected) { const signInUrl = new URL("/sign-in", request.url); signInUrl.searchParams.set("callbackUrl", pathname); return NextResponse.redirect(signInUrl); From 66a2d647bbff260db2e1c468abc2266429d35515 Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Fri, 24 Apr 2026 14:34:35 -0700 Subject: [PATCH 4/7] feat: add Dockerfile and docker-compose.yml for containerized deployment --- Dockerfile | 21 +++++++++++++++++++++ docker-compose.yml | 30 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f4333aa --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:20-alpine AS base + +FROM base AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +FROM base AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . +RUN npm run build + +FROM base AS runner +WORKDIR /app +ENV NODE_ENV=production +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/standalone ./ +COPY --from=builder /app/.next/static ./.next/static +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ea7226e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,30 @@ +version: "3.8" +services: + app: + build: . + ports: + - "3000:3000" + environment: + - DATABASE_URL=postgresql://postgres:postgres@db:5432/comparaison + depends_on: + db: + condition: service_healthy + restart: unless-stopped + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: comparaison + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped +volumes: + pgdata: From 26d879c82e451aa41e46756c1a8d0223dd1331a3 Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Fri, 24 Apr 2026 14:34:50 -0700 Subject: [PATCH 5/7] feat: add standalone output to next.config.ts for Docker builds --- next.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/next.config.ts b/next.config.ts index e9ffa30..68a6c64 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,7 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + output: "standalone", }; export default nextConfig; From 3539a5f3ebed7e80e93e97d6be5c890481b9b41d Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Fri, 24 Apr 2026 14:35:19 -0700 Subject: [PATCH 6/7] feat: add .env.example with required environment variables --- .env.example | 6 ++++++ .gitignore | 1 + 2 files changed, 7 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f0f7812 --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/comparaison +BETTER_AUTH_SECRET=change-me-to-random-string +OPENAI_API_KEY= +PERPLEXITY_API_KEY= +TAVILY_API_KEY= +NEXT_PUBLIC_APP_URL=http://localhost:3000 diff --git a/.gitignore b/.gitignore index 5ef6a52..7b8da95 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel From 37c07e468d6d6dce495983c6a498745825ecb5fe Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Fri, 24 Apr 2026 14:37:28 -0700 Subject: [PATCH 7/7] fix: lazy-init OpenAI client to avoid build failure without API key --- src/lib/llm/providers/openai.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/lib/llm/providers/openai.ts b/src/lib/llm/providers/openai.ts index f3116af..dd10e4e 100644 --- a/src/lib/llm/providers/openai.ts +++ b/src/lib/llm/providers/openai.ts @@ -6,9 +6,14 @@ import type { ItemResearch, } from "../types"; -const client = new OpenAI({ - apiKey: process.env.OPENAI_API_KEY, -}); +let _client: OpenAI | null = null; + +function getClient(): OpenAI { + if (!_client) { + _client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY }); + } + return _client; +} const SYSTEM_PROMPT = `You are an expert research analyst. Your job is to compare items across multiple dimensions and produce structured, insightful comparison data. @@ -105,7 +110,7 @@ Provide a comprehensive comparison with scores, pros/cons, and a recommendation. for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) { try { - const response = await client.chat.completions.create({ + const response = await getClient().chat.completions.create({ model: "gpt-4o-mini", messages: [ { role: "system", content: SYSTEM_PROMPT },