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(),
});