From ff6ead47c5b1d49d2732f97cdd5d7cfa13beec9b Mon Sep 17 00:00:00 2001 From: Leon van Zyl Date: Thu, 6 Nov 2025 08:27:52 +0200 Subject: [PATCH] update/ betterauth schema new --- create-agentic-app/package-lock.json | 4 +- create-agentic-app/package.json | 2 +- .../drizzle/0000_chilly_the_phantom.sql | 50 +++ .../template/drizzle/meta/0000_snapshot.json | 326 ++++++++++++++++++ .../template/drizzle/meta/_journal.json | 13 + .../template/src/app/api/chat/route.ts | 7 +- .../template/src/app/api/diagnostics/route.ts | 63 ++-- create-agentic-app/template/src/app/page.tsx | 4 +- .../src/components/setup-checklist.tsx | 4 +- .../template/src/hooks/use-diagnostics.ts | 2 +- create-agentic-app/template/src/lib/schema.ts | 54 +-- drizzle/0000_chilly_the_phantom.sql | 50 +++ drizzle/meta/0000_snapshot.json | 326 ++++++++++++++++++ drizzle/meta/_journal.json | 13 + src/app/api/chat/route.ts | 7 +- src/app/api/diagnostics/route.ts | 63 ++-- src/app/page.tsx | 4 +- src/components/setup-checklist.tsx | 4 +- src/hooks/use-diagnostics.ts | 2 +- src/lib/schema.ts | 54 +-- 20 files changed, 951 insertions(+), 101 deletions(-) create mode 100644 create-agentic-app/template/drizzle/0000_chilly_the_phantom.sql create mode 100644 create-agentic-app/template/drizzle/meta/0000_snapshot.json create mode 100644 create-agentic-app/template/drizzle/meta/_journal.json create mode 100644 drizzle/0000_chilly_the_phantom.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json diff --git a/create-agentic-app/package-lock.json b/create-agentic-app/package-lock.json index dacc0e7..286b30a 100644 --- a/create-agentic-app/package-lock.json +++ b/create-agentic-app/package-lock.json @@ -1,12 +1,12 @@ { "name": "create-agentic-app", - "version": "1.1.7", + "version": "1.1.10", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "create-agentic-app", - "version": "1.1.7", + "version": "1.1.10", "license": "MIT", "dependencies": { "chalk": "^5.3.0", diff --git a/create-agentic-app/package.json b/create-agentic-app/package.json index 384eb49..ce72096 100644 --- a/create-agentic-app/package.json +++ b/create-agentic-app/package.json @@ -1,6 +1,6 @@ { "name": "create-agentic-app", - "version": "1.1.7", + "version": "1.1.10", "description": "Scaffold a new agentic AI application with Next.js, Better Auth, and AI SDK", "type": "module", "bin": { diff --git a/create-agentic-app/template/drizzle/0000_chilly_the_phantom.sql b/create-agentic-app/template/drizzle/0000_chilly_the_phantom.sql new file mode 100644 index 0000000..13ad833 --- /dev/null +++ b/create-agentic-app/template/drizzle/0000_chilly_the_phantom.sql @@ -0,0 +1,50 @@ +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "image" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/create-agentic-app/template/drizzle/meta/0000_snapshot.json b/create-agentic-app/template/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..5f71157 --- /dev/null +++ b/create-agentic-app/template/drizzle/meta/0000_snapshot.json @@ -0,0 +1,326 @@ +{ + "id": "56cf4573-0efe-4f7d-908f-0c7cb0ac0739", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/create-agentic-app/template/drizzle/meta/_journal.json b/create-agentic-app/template/drizzle/meta/_journal.json new file mode 100644 index 0000000..63d838e --- /dev/null +++ b/create-agentic-app/template/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1762409965425, + "tag": "0000_chilly_the_phantom", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/create-agentic-app/template/src/app/api/chat/route.ts b/create-agentic-app/template/src/app/api/chat/route.ts index 794e714..aa5a775 100644 --- a/create-agentic-app/template/src/app/api/chat/route.ts +++ b/create-agentic-app/template/src/app/api/chat/route.ts @@ -1,9 +1,14 @@ -import { openrouter } from "@openrouter/ai-sdk-provider"; +import { createOpenRouter } from "@openrouter/ai-sdk-provider"; import { streamText, UIMessage, convertToModelMessages } from "ai"; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); + // Initialize OpenRouter with API key from environment + const openrouter = createOpenRouter({ + apiKey: process.env.OPENROUTER_API_KEY, + }); + const result = streamText({ model: openrouter(process.env.OPENROUTER_MODEL || "openai/gpt-5-mini"), messages: convertToModelMessages(messages), diff --git a/create-agentic-app/template/src/app/api/diagnostics/route.ts b/create-agentic-app/template/src/app/api/diagnostics/route.ts index eb3b579..183194b 100644 --- a/create-agentic-app/template/src/app/api/diagnostics/route.ts +++ b/create-agentic-app/template/src/app/api/diagnostics/route.ts @@ -9,7 +9,7 @@ interface DiagnosticsResponse { BETTER_AUTH_SECRET: boolean; GOOGLE_CLIENT_ID: boolean; GOOGLE_CLIENT_SECRET: boolean; - OPENAI_API_KEY: boolean; + OPENROUTER_API_KEY: boolean; NEXT_PUBLIC_APP_URL: boolean; }; database: { @@ -33,34 +33,55 @@ export async function GET(req: Request) { BETTER_AUTH_SECRET: Boolean(process.env.BETTER_AUTH_SECRET), GOOGLE_CLIENT_ID: Boolean(process.env.GOOGLE_CLIENT_ID), GOOGLE_CLIENT_SECRET: Boolean(process.env.GOOGLE_CLIENT_SECRET), - OPENAI_API_KEY: Boolean(process.env.OPENAI_API_KEY), + OPENROUTER_API_KEY: Boolean(process.env.OPENROUTER_API_KEY), NEXT_PUBLIC_APP_URL: Boolean(process.env.NEXT_PUBLIC_APP_URL), } as const; - // Database checks + // Database checks with timeout let dbConnected = false; let schemaApplied = false; let dbError: string | undefined; if (env.POSTGRES_URL) { try { - const [{ db }, { sql }, schema] = await Promise.all([ - import("@/lib/db"), - import("drizzle-orm"), - import("@/lib/schema"), - ]); - // Ping DB - await db.execute(sql`select 1`); - dbConnected = true; - try { - // Touch a known table to verify migrations - await db.select().from(schema.user).limit(1); - schemaApplied = true; - } catch { - schemaApplied = false; - } - } catch (err) { + // Add timeout to prevent hanging on unreachable database + const dbCheckPromise = (async () => { + const [{ db }, { sql }, schema] = await Promise.all([ + import("@/lib/db"), + import("drizzle-orm"), + import("@/lib/schema"), + ]); + + // Ping DB - this will actually attempt to connect + const result = await db.execute(sql`SELECT 1 as ping`); + if (!result) { + throw new Error("Database query returned no result"); + } + dbConnected = true; + + try { + // Touch a known table to verify migrations + await db.select().from(schema.user).limit(1); + schemaApplied = true; + } catch { + schemaApplied = false; + // If we can't query the user table, it's likely migrations haven't run + if (!dbError) { + dbError = "Schema not applied. Run: npm run db:migrate"; + } + } + })(); + + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("Database connection timeout (5s)")), 5000) + ); + + await Promise.race([dbCheckPromise, timeoutPromise]); + } catch { dbConnected = false; - dbError = err instanceof Error ? err.message : "Unknown database error"; + schemaApplied = false; + + // Provide user-friendly error messages + dbError = "Database not connected. Please start your PostgreSQL database and verify your POSTGRES_URL in .env"; } } else { dbConnected = false; @@ -92,7 +113,7 @@ export async function GET(req: Request) { const authConfigured = env.BETTER_AUTH_SECRET && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET; - const aiConfigured = env.OPENAI_API_KEY; // We avoid live-calling the AI provider here + const aiConfigured = env.OPENROUTER_API_KEY; // We avoid live-calling the AI provider here const overallStatus: StatusLevel = (() => { if (!env.POSTGRES_URL || !dbConnected || !schemaApplied) return "error"; diff --git a/create-agentic-app/template/src/app/page.tsx b/create-agentic-app/template/src/app/page.tsx index 1b5b6ef..6110fd3 100644 --- a/create-agentic-app/template/src/app/page.tsx +++ b/create-agentic-app/template/src/app/page.tsx @@ -77,7 +77,7 @@ export default function Home() { AI Ready

- Vercel AI SDK with OpenAI integration + Vercel AI SDK with OpenRouter integration

@@ -108,7 +108,7 @@ export default function Home() {
  • POSTGRES_URL (PostgreSQL connection string)
  • GOOGLE_CLIENT_ID (OAuth credentials)
  • GOOGLE_CLIENT_SECRET (OAuth credentials)
  • -
  • OPENAI_API_KEY (for AI functionality)
  • +
  • OPENROUTER_API_KEY (for AI functionality)
  • diff --git a/create-agentic-app/template/src/components/setup-checklist.tsx b/create-agentic-app/template/src/components/setup-checklist.tsx index 7530a73..0a0dbf1 100644 --- a/create-agentic-app/template/src/components/setup-checklist.tsx +++ b/create-agentic-app/template/src/components/setup-checklist.tsx @@ -11,7 +11,7 @@ type DiagnosticsResponse = { BETTER_AUTH_SECRET: boolean; GOOGLE_CLIENT_ID: boolean; GOOGLE_CLIENT_SECRET: boolean; - OPENAI_API_KEY: boolean; + OPENROUTER_API_KEY: boolean; NEXT_PUBLIC_APP_URL: boolean; }; database: { @@ -99,7 +99,7 @@ export function SetupChecklist() { label: "AI integration (optional)", ok: !!data?.ai.configured, detail: !data?.ai.configured - ? "Set OPENAI_API_KEY for AI chat" + ? "Set OPENROUTER_API_KEY for AI chat" : undefined, }, ] as const; diff --git a/create-agentic-app/template/src/hooks/use-diagnostics.ts b/create-agentic-app/template/src/hooks/use-diagnostics.ts index 172fcc7..48f8be5 100644 --- a/create-agentic-app/template/src/hooks/use-diagnostics.ts +++ b/create-agentic-app/template/src/hooks/use-diagnostics.ts @@ -9,7 +9,7 @@ type DiagnosticsResponse = { BETTER_AUTH_SECRET: boolean; GOOGLE_CLIENT_ID: boolean; GOOGLE_CLIENT_SECRET: boolean; - OPENAI_API_KEY: boolean; + OPENROUTER_API_KEY: boolean; NEXT_PUBLIC_APP_URL: boolean; }; database: { diff --git a/create-agentic-app/template/src/lib/schema.ts b/create-agentic-app/template/src/lib/schema.ts index f190532..112a90d 100644 --- a/create-agentic-app/template/src/lib/schema.ts +++ b/create-agentic-app/template/src/lib/schema.ts @@ -4,48 +4,58 @@ export const user = pgTable("user", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), - emailVerified: boolean("emailVerified"), + emailVerified: boolean("email_verified").default(false).notNull(), image: text("image"), - createdAt: timestamp("createdAt").notNull().defaultNow(), - updatedAt: timestamp("updatedAt").notNull().defaultNow(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), }); export const session = pgTable("session", { id: text("id").primaryKey(), - expiresAt: timestamp("expiresAt").notNull(), + expiresAt: timestamp("expires_at").notNull(), token: text("token").notNull().unique(), - createdAt: timestamp("createdAt").notNull().defaultNow(), - updatedAt: timestamp("updatedAt").notNull().defaultNow(), - ipAddress: text("ipAddress"), - userAgent: text("userAgent"), - userId: text("userId") + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), }); export const account = pgTable("account", { id: text("id").primaryKey(), - accountId: text("accountId").notNull(), - providerId: text("providerId").notNull(), - userId: text("userId") + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), - accessToken: text("accessToken"), - refreshToken: text("refreshToken"), - idToken: text("idToken"), - accessTokenExpiresAt: timestamp("accessTokenExpiresAt"), - refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), scope: text("scope"), password: text("password"), - createdAt: timestamp("createdAt").notNull().defaultNow(), - updatedAt: timestamp("updatedAt").notNull().defaultNow(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), }); export const verification = pgTable("verification", { id: text("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), - expiresAt: timestamp("expiresAt").notNull(), - createdAt: timestamp("createdAt").defaultNow(), - updatedAt: timestamp("updatedAt").defaultNow(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), }); diff --git a/drizzle/0000_chilly_the_phantom.sql b/drizzle/0000_chilly_the_phantom.sql new file mode 100644 index 0000000..13ad833 --- /dev/null +++ b/drizzle/0000_chilly_the_phantom.sql @@ -0,0 +1,50 @@ +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean DEFAULT false NOT NULL, + "image" text, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL, + CONSTRAINT "user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..5f71157 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,326 @@ +{ + "id": "56cf4573-0efe-4f7d-908f-0c7cb0ac0739", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..63d838e --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1762409965425, + "tag": "0000_chilly_the_phantom", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/src/app/api/chat/route.ts b/src/app/api/chat/route.ts index 794e714..aa5a775 100644 --- a/src/app/api/chat/route.ts +++ b/src/app/api/chat/route.ts @@ -1,9 +1,14 @@ -import { openrouter } from "@openrouter/ai-sdk-provider"; +import { createOpenRouter } from "@openrouter/ai-sdk-provider"; import { streamText, UIMessage, convertToModelMessages } from "ai"; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); + // Initialize OpenRouter with API key from environment + const openrouter = createOpenRouter({ + apiKey: process.env.OPENROUTER_API_KEY, + }); + const result = streamText({ model: openrouter(process.env.OPENROUTER_MODEL || "openai/gpt-5-mini"), messages: convertToModelMessages(messages), diff --git a/src/app/api/diagnostics/route.ts b/src/app/api/diagnostics/route.ts index eb3b579..183194b 100644 --- a/src/app/api/diagnostics/route.ts +++ b/src/app/api/diagnostics/route.ts @@ -9,7 +9,7 @@ interface DiagnosticsResponse { BETTER_AUTH_SECRET: boolean; GOOGLE_CLIENT_ID: boolean; GOOGLE_CLIENT_SECRET: boolean; - OPENAI_API_KEY: boolean; + OPENROUTER_API_KEY: boolean; NEXT_PUBLIC_APP_URL: boolean; }; database: { @@ -33,34 +33,55 @@ export async function GET(req: Request) { BETTER_AUTH_SECRET: Boolean(process.env.BETTER_AUTH_SECRET), GOOGLE_CLIENT_ID: Boolean(process.env.GOOGLE_CLIENT_ID), GOOGLE_CLIENT_SECRET: Boolean(process.env.GOOGLE_CLIENT_SECRET), - OPENAI_API_KEY: Boolean(process.env.OPENAI_API_KEY), + OPENROUTER_API_KEY: Boolean(process.env.OPENROUTER_API_KEY), NEXT_PUBLIC_APP_URL: Boolean(process.env.NEXT_PUBLIC_APP_URL), } as const; - // Database checks + // Database checks with timeout let dbConnected = false; let schemaApplied = false; let dbError: string | undefined; if (env.POSTGRES_URL) { try { - const [{ db }, { sql }, schema] = await Promise.all([ - import("@/lib/db"), - import("drizzle-orm"), - import("@/lib/schema"), - ]); - // Ping DB - await db.execute(sql`select 1`); - dbConnected = true; - try { - // Touch a known table to verify migrations - await db.select().from(schema.user).limit(1); - schemaApplied = true; - } catch { - schemaApplied = false; - } - } catch (err) { + // Add timeout to prevent hanging on unreachable database + const dbCheckPromise = (async () => { + const [{ db }, { sql }, schema] = await Promise.all([ + import("@/lib/db"), + import("drizzle-orm"), + import("@/lib/schema"), + ]); + + // Ping DB - this will actually attempt to connect + const result = await db.execute(sql`SELECT 1 as ping`); + if (!result) { + throw new Error("Database query returned no result"); + } + dbConnected = true; + + try { + // Touch a known table to verify migrations + await db.select().from(schema.user).limit(1); + schemaApplied = true; + } catch { + schemaApplied = false; + // If we can't query the user table, it's likely migrations haven't run + if (!dbError) { + dbError = "Schema not applied. Run: npm run db:migrate"; + } + } + })(); + + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("Database connection timeout (5s)")), 5000) + ); + + await Promise.race([dbCheckPromise, timeoutPromise]); + } catch { dbConnected = false; - dbError = err instanceof Error ? err.message : "Unknown database error"; + schemaApplied = false; + + // Provide user-friendly error messages + dbError = "Database not connected. Please start your PostgreSQL database and verify your POSTGRES_URL in .env"; } } else { dbConnected = false; @@ -92,7 +113,7 @@ export async function GET(req: Request) { const authConfigured = env.BETTER_AUTH_SECRET && env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET; - const aiConfigured = env.OPENAI_API_KEY; // We avoid live-calling the AI provider here + const aiConfigured = env.OPENROUTER_API_KEY; // We avoid live-calling the AI provider here const overallStatus: StatusLevel = (() => { if (!env.POSTGRES_URL || !dbConnected || !schemaApplied) return "error"; diff --git a/src/app/page.tsx b/src/app/page.tsx index 1b5b6ef..6110fd3 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -77,7 +77,7 @@ export default function Home() { AI Ready

    - Vercel AI SDK with OpenAI integration + Vercel AI SDK with OpenRouter integration

    @@ -108,7 +108,7 @@ export default function Home() {
  • POSTGRES_URL (PostgreSQL connection string)
  • GOOGLE_CLIENT_ID (OAuth credentials)
  • GOOGLE_CLIENT_SECRET (OAuth credentials)
  • -
  • OPENAI_API_KEY (for AI functionality)
  • +
  • OPENROUTER_API_KEY (for AI functionality)
  • diff --git a/src/components/setup-checklist.tsx b/src/components/setup-checklist.tsx index 7530a73..0a0dbf1 100644 --- a/src/components/setup-checklist.tsx +++ b/src/components/setup-checklist.tsx @@ -11,7 +11,7 @@ type DiagnosticsResponse = { BETTER_AUTH_SECRET: boolean; GOOGLE_CLIENT_ID: boolean; GOOGLE_CLIENT_SECRET: boolean; - OPENAI_API_KEY: boolean; + OPENROUTER_API_KEY: boolean; NEXT_PUBLIC_APP_URL: boolean; }; database: { @@ -99,7 +99,7 @@ export function SetupChecklist() { label: "AI integration (optional)", ok: !!data?.ai.configured, detail: !data?.ai.configured - ? "Set OPENAI_API_KEY for AI chat" + ? "Set OPENROUTER_API_KEY for AI chat" : undefined, }, ] as const; diff --git a/src/hooks/use-diagnostics.ts b/src/hooks/use-diagnostics.ts index 172fcc7..48f8be5 100644 --- a/src/hooks/use-diagnostics.ts +++ b/src/hooks/use-diagnostics.ts @@ -9,7 +9,7 @@ type DiagnosticsResponse = { BETTER_AUTH_SECRET: boolean; GOOGLE_CLIENT_ID: boolean; GOOGLE_CLIENT_SECRET: boolean; - OPENAI_API_KEY: boolean; + OPENROUTER_API_KEY: boolean; NEXT_PUBLIC_APP_URL: boolean; }; database: { diff --git a/src/lib/schema.ts b/src/lib/schema.ts index f190532..112a90d 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -4,48 +4,58 @@ export const user = pgTable("user", { id: text("id").primaryKey(), name: text("name").notNull(), email: text("email").notNull().unique(), - emailVerified: boolean("emailVerified"), + emailVerified: boolean("email_verified").default(false).notNull(), image: text("image"), - createdAt: timestamp("createdAt").notNull().defaultNow(), - updatedAt: timestamp("updatedAt").notNull().defaultNow(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), }); export const session = pgTable("session", { id: text("id").primaryKey(), - expiresAt: timestamp("expiresAt").notNull(), + expiresAt: timestamp("expires_at").notNull(), token: text("token").notNull().unique(), - createdAt: timestamp("createdAt").notNull().defaultNow(), - updatedAt: timestamp("updatedAt").notNull().defaultNow(), - ipAddress: text("ipAddress"), - userAgent: text("userAgent"), - userId: text("userId") + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), + ipAddress: text("ip_address"), + userAgent: text("user_agent"), + userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), }); export const account = pgTable("account", { id: text("id").primaryKey(), - accountId: text("accountId").notNull(), - providerId: text("providerId").notNull(), - userId: text("userId") + accountId: text("account_id").notNull(), + providerId: text("provider_id").notNull(), + userId: text("user_id") .notNull() .references(() => user.id, { onDelete: "cascade" }), - accessToken: text("accessToken"), - refreshToken: text("refreshToken"), - idToken: text("idToken"), - accessTokenExpiresAt: timestamp("accessTokenExpiresAt"), - refreshTokenExpiresAt: timestamp("refreshTokenExpiresAt"), + accessToken: text("access_token"), + refreshToken: text("refresh_token"), + idToken: text("id_token"), + accessTokenExpiresAt: timestamp("access_token_expires_at"), + refreshTokenExpiresAt: timestamp("refresh_token_expires_at"), scope: text("scope"), password: text("password"), - createdAt: timestamp("createdAt").notNull().defaultNow(), - updatedAt: timestamp("updatedAt").notNull().defaultNow(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), }); export const verification = pgTable("verification", { id: text("id").primaryKey(), identifier: text("identifier").notNull(), value: text("value").notNull(), - expiresAt: timestamp("expiresAt").notNull(), - createdAt: timestamp("createdAt").defaultNow(), - updatedAt: timestamp("updatedAt").defaultNow(), + expiresAt: timestamp("expires_at").notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), + updatedAt: timestamp("updated_at") + .defaultNow() + .$onUpdate(() => /* @__PURE__ */ new Date()) + .notNull(), });