This commit is contained in:
Nikita
2025-12-21 13:37:19 -08:00
commit 8cd4b943a5
173 changed files with 44266 additions and 0 deletions

View File

@@ -0,0 +1,70 @@
/**
* Test PlanetScale Postgres connection
*
* Run: pnpm tsx scripts/db-connect.ts
*/
import "dotenv/config"
import postgres from "postgres"
const CONNECTION_STRING = process.env.DATABASE_URL
if (!CONNECTION_STRING) {
console.error("❌ DATABASE_URL is required in .env")
process.exit(1)
}
const sql = postgres(CONNECTION_STRING, {
ssl: "require",
max: 1,
idle_timeout: 20,
connect_timeout: 10,
})
async function testConnection() {
console.log("🔌 Connecting to PlanetScale Postgres...")
try {
// Test basic connection
const [result] = await sql`SELECT NOW() as time, current_database() as db`
console.log("✅ Connected!")
console.log(` Database: ${result.db}`)
console.log(` Server time: ${result.time}`)
// List all databases
const databases = await sql`
SELECT datname FROM pg_database WHERE datistemplate = false ORDER BY datname
`
console.log(`\n📁 Databases:`)
for (const d of databases) {
console.log(` - ${d.datname}`)
}
// List tables in current db
const tables = await sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
`
if (tables.length > 0) {
console.log(`\n📋 Tables (${tables.length}):`)
for (const t of tables) {
console.log(` - ${t.table_name}`)
}
} else {
console.log("\n📋 No tables found in public schema")
}
// Show version
const [version] = await sql`SELECT version()`
console.log(`\n🐘 ${version.version}`)
} catch (err) {
console.error("❌ Connection failed:", err)
} finally {
await sql.end()
}
}
testConnection()

View File

@@ -0,0 +1,250 @@
/**
* Production Database Query Tool
* Allows CRUD operations on the production database
*
* Usage:
* DATABASE_URL="..." pnpm tsx scripts/db-query.ts
*
* Commands (interactive):
* tables - List all tables
* users - List all users
* threads - List chat threads
* sql <query> - Run raw SQL
* insert-user <email> <name> - Create a user
* delete-user <id> - Delete a user
* help - Show commands
* exit - Exit
*/
import "dotenv/config"
import postgres from "postgres"
import * as readline from "readline"
const CONNECTION_STRING = process.env.DATABASE_URL
if (!CONNECTION_STRING) {
console.error("❌ DATABASE_URL is required")
process.exit(1)
}
const sql = postgres(CONNECTION_STRING, {
ssl: "require",
max: 1,
})
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
function prompt(question: string): Promise<string> {
return new Promise((resolve) => {
rl.question(question, resolve)
})
}
async function listTables() {
const tables = await sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
`
console.log("\n📋 Tables:")
if (tables.length === 0) {
console.log(" (no tables found)")
} else {
for (const t of tables) {
const count = await sql`
SELECT COUNT(*) as count FROM ${sql(t.table_name)}
`
console.log(` - ${t.table_name} (${count[0].count} rows)`)
}
}
console.log()
}
async function listUsers() {
try {
const users = await sql`SELECT id, name, email, "createdAt" FROM users ORDER BY "createdAt" DESC LIMIT 20`
console.log("\n👥 Users:")
if (users.length === 0) {
console.log(" (no users)")
} else {
for (const u of users) {
console.log(` - ${u.id}: ${u.name} <${u.email}> (${u.createdAt})`)
}
}
console.log()
} catch (e) {
console.log(" ❌ users table not found or error:", (e as Error).message)
}
}
async function listThreads() {
try {
const threads = await sql`
SELECT id, title, user_id, created_at
FROM chat_threads
ORDER BY created_at DESC
LIMIT 20
`
console.log("\n💬 Chat Threads:")
if (threads.length === 0) {
console.log(" (no threads)")
} else {
for (const t of threads) {
console.log(` - #${t.id}: "${t.title}" (user: ${t.user_id})`)
}
}
console.log()
} catch (e) {
console.log(" ❌ chat_threads table not found or error:", (e as Error).message)
}
}
async function runSQL(query: string) {
try {
const result = await sql.unsafe(query)
console.log("\n✅ Result:")
console.log(result)
console.log()
} catch (e) {
console.log("❌ Error:", (e as Error).message)
}
}
async function insertUser(email: string, name: string) {
try {
const id = `user_${Date.now()}`
await sql`
INSERT INTO users (id, name, email, "emailVerified", "createdAt", "updatedAt")
VALUES (${id}, ${name}, ${email}, false, NOW(), NOW())
`
console.log(`✅ Created user: ${id}`)
} catch (e) {
console.log("❌ Error:", (e as Error).message)
}
}
async function deleteUser(id: string) {
try {
const result = await sql`DELETE FROM users WHERE id = ${id}`
console.log(`✅ Deleted ${result.count} user(s)`)
} catch (e) {
console.log("❌ Error:", (e as Error).message)
}
}
function showHelp() {
console.log(`
📖 Commands:
tables - List all tables with row counts
users - List users (max 20)
threads - List chat threads (max 20)
sql <query> - Run raw SQL query
insert-user <email> <name> - Create a new user
delete-user <id> - Delete a user by ID
drop-all - Drop all tables (dangerous!)
help - Show this help
exit - Exit the tool
`)
}
async function dropAll() {
const confirm = await prompt("⚠️ This will DROP ALL TABLES. Type 'DROP' to confirm: ")
if (confirm !== "DROP") {
console.log("Aborted.")
return
}
const tables = [
"thread_context_items",
"context_items",
"canvas_images",
"canvas",
"chat_messages",
"chat_threads",
"verifications",
"accounts",
"sessions",
"users",
]
for (const table of tables) {
try {
await sql`DROP TABLE IF EXISTS ${sql(table)} CASCADE`
console.log(` ✓ Dropped ${table}`)
} catch (e) {
console.log(`${table}: ${(e as Error).message}`)
}
}
console.log("\n✓ All tables dropped")
}
async function main() {
console.log("🔌 Connected to production database")
console.log('Type "help" for commands, "exit" to quit.\n')
// Check initial connection
try {
const [result] = await sql`SELECT current_database() as db`
console.log(`Database: ${result.db}\n`)
} catch (e) {
console.error("❌ Connection failed:", (e as Error).message)
process.exit(1)
}
while (true) {
const input = await prompt("db> ")
const [cmd, ...args] = input.trim().split(/\s+/)
switch (cmd.toLowerCase()) {
case "tables":
await listTables()
break
case "users":
await listUsers()
break
case "threads":
await listThreads()
break
case "sql":
await runSQL(args.join(" "))
break
case "insert-user":
if (args.length < 2) {
console.log("Usage: insert-user <email> <name>")
} else {
await insertUser(args[0], args.slice(1).join(" "))
}
break
case "delete-user":
if (args.length < 1) {
console.log("Usage: delete-user <id>")
} else {
await deleteUser(args[0])
}
break
case "drop-all":
await dropAll()
break
case "help":
showHelp()
break
case "exit":
case "quit":
case "q":
console.log("Bye!")
await sql.end()
rl.close()
process.exit(0)
case "":
break
default:
console.log(`Unknown command: ${cmd}. Type "help" for commands.`)
}
}
}
main().catch(console.error)

View File

@@ -0,0 +1,149 @@
/**
* Safe production migration script
* Usage: DATABASE_URL="..." pnpm tsx scripts/migrate-safe.ts [option]
* Options: check | auth | drizzle | both
*/
import postgres from "postgres"
const DATABASE_URL = process.env.DATABASE_URL
if (!DATABASE_URL) {
console.error("❌ DATABASE_URL is required")
process.exit(1)
}
const sql = postgres(DATABASE_URL)
async function checkConnection() {
try {
await sql`SELECT 1`
console.log("✓ Connected to database")
return true
} catch (e) {
console.error("✗ Connection failed:", (e as Error).message)
return false
}
}
async function listTables() {
const tables = await sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
`
if (tables.length === 0) {
console.log(" (no tables)")
} else {
tables.forEach((t) => console.log(" -", t.table_name))
}
}
async function checkAuthTables() {
const cols = await sql`
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'verifications'
ORDER BY ordinal_position
`
if (cols.length === 0) {
console.log(" verifications: NOT EXISTS (will be created)")
return false
}
const colNames = cols.map((c) => c.column_name)
if (colNames.includes("expiresAt")) {
console.log(" verifications: ✓ Correct (camelCase)")
return true
} else {
console.log(" verifications: ⚠ Wrong columns:", colNames.join(", "))
return false
}
}
async function createAuthTables() {
console.log("Dropping existing auth tables...")
await sql`DROP TABLE IF EXISTS verifications CASCADE`
await sql`DROP TABLE IF EXISTS accounts CASCADE`
await sql`DROP TABLE IF EXISTS sessions CASCADE`
await sql`DROP TABLE IF EXISTS users CASCADE`
console.log("Creating auth tables with camelCase columns...")
await sql.unsafe(`
CREATE TABLE users (
id text PRIMARY KEY,
name text NOT NULL,
email text NOT NULL UNIQUE,
"emailVerified" boolean NOT NULL DEFAULT false,
image text,
"createdAt" timestamp NOT NULL DEFAULT now(),
"updatedAt" timestamp NOT NULL DEFAULT now()
);
CREATE TABLE sessions (
id text PRIMARY KEY,
"expiresAt" timestamp NOT NULL,
token text NOT NULL UNIQUE,
"createdAt" timestamp NOT NULL,
"updatedAt" timestamp NOT NULL,
"ipAddress" text,
"userAgent" text,
"userId" text NOT NULL REFERENCES users(id) ON DELETE CASCADE
);
CREATE TABLE accounts (
id text PRIMARY KEY,
"accountId" text NOT NULL,
"providerId" text NOT NULL,
"userId" text NOT NULL REFERENCES users(id) ON DELETE CASCADE,
"accessToken" text,
"refreshToken" text,
"idToken" text,
"accessTokenExpiresAt" timestamp,
"refreshTokenExpiresAt" timestamp,
scope text,
password text,
"createdAt" timestamp NOT NULL,
"updatedAt" timestamp NOT NULL
);
CREATE TABLE verifications (
id text PRIMARY KEY,
identifier text NOT NULL,
value text NOT NULL,
"expiresAt" timestamp NOT NULL,
"createdAt" timestamp NOT NULL DEFAULT now(),
"updatedAt" timestamp NOT NULL DEFAULT now()
);
`)
console.log("✓ Auth tables created")
}
async function main() {
const option = process.argv[2] || "check"
if (!(await checkConnection())) {
await sql.end()
process.exit(1)
}
if (option === "check") {
console.log("\nCurrent tables:")
await listTables()
console.log("\nAuth tables status:")
await checkAuthTables()
} else if (option === "auth") {
await createAuthTables()
} else if (option === "drizzle") {
console.log("Run: DATABASE_URL=\"...\" pnpm drizzle-kit push --force")
} else if (option === "both") {
await createAuthTables()
console.log("\nNow run: DATABASE_URL=\"...\" pnpm drizzle-kit push --force")
} else {
console.log("Unknown option:", option)
console.log("Options: check | auth | drizzle | both")
}
await sql.end()
}
main().catch((e) => {
console.error(e)
sql.end()
process.exit(1)
})

View File

@@ -0,0 +1,222 @@
/**
* Push schema directly to PlanetScale Postgres
* Bypasses drizzle-kit permission issues
*
* Run: DATABASE_URL="..." pnpm tsx scripts/push-schema.ts
*
* NOTE: PlanetScale API tokens may not have CREATE permissions.
* If you get "permission denied for schema public", you need to:
* 1. Go to PlanetScale dashboard
* 2. Create a new password with "Admin" role
* 3. Use that connection string instead
* OR run the SQL manually in PlanetScale's web console
*/
import "dotenv/config"
import postgres from "postgres"
const databaseUrl = process.env.DATABASE_URL
if (!databaseUrl) {
throw new Error("DATABASE_URL is required")
}
// Allow disabling SSL for local/dev databases while keeping require for prod.
const parsed = new URL(databaseUrl)
const hostname = parsed.hostname
const explicitSsl = process.env.DATABASE_SSL?.toLowerCase()
const isLocalHost =
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname.endsWith(".local") ||
hostname.endsWith(".localtest.me")
const ssl =
explicitSsl === "disable"
? false
: explicitSsl === "require"
? "require"
: isLocalHost
? false
: "require"
const sql = postgres(databaseUrl, {
ssl,
max: 1,
})
async function pushSchema() {
console.log("🚀 Pushing schema to PlanetScale Postgres...")
// Check if we have CREATE permissions
const [user] = await sql`SELECT current_user`
console.log(` Connected as: ${user.current_user}`)
try {
// Better-auth tables (camelCase columns)
await sql`
CREATE TABLE IF NOT EXISTS "users" (
"id" text PRIMARY KEY,
"name" text NOT NULL,
"email" text NOT NULL UNIQUE,
"emailVerified" boolean NOT NULL DEFAULT false,
"image" text,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
)
`
console.log("✅ Created users table")
await sql`
CREATE TABLE IF NOT EXISTS "sessions" (
"id" text PRIMARY KEY,
"expiresAt" timestamptz NOT NULL,
"token" text NOT NULL UNIQUE,
"createdAt" timestamptz NOT NULL,
"updatedAt" timestamptz NOT NULL,
"ipAddress" text,
"userAgent" text,
"userId" text NOT NULL REFERENCES "users"("id") ON DELETE cascade
)
`
console.log("✅ Created sessions table")
await sql`
CREATE TABLE IF NOT EXISTS "accounts" (
"id" text PRIMARY KEY,
"accountId" text NOT NULL,
"providerId" text NOT NULL,
"userId" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"accessToken" text,
"refreshToken" text,
"idToken" text,
"accessTokenExpiresAt" timestamptz,
"refreshTokenExpiresAt" timestamptz,
"scope" text,
"password" text,
"createdAt" timestamptz NOT NULL,
"updatedAt" timestamptz NOT NULL
)
`
console.log("✅ Created accounts table")
await sql`
CREATE TABLE IF NOT EXISTS "verifications" (
"id" text PRIMARY KEY,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expiresAt" timestamptz NOT NULL,
"createdAt" timestamptz DEFAULT now(),
"updatedAt" timestamptz DEFAULT now()
)
`
console.log("✅ Created verifications table")
// App tables (snake_case for Electric sync)
await sql`
CREATE TABLE IF NOT EXISTS "chat_threads" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"title" text NOT NULL,
"user_id" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now()
)
`
console.log("✅ Created chat_threads table")
await sql`
CREATE TABLE IF NOT EXISTS "chat_messages" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"thread_id" integer NOT NULL REFERENCES "chat_threads"("id") ON DELETE cascade,
"role" varchar(32) NOT NULL,
"content" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now()
)
`
console.log("✅ Created chat_messages table")
await sql`
CREATE TABLE IF NOT EXISTS "canvas" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"owner_id" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"name" text NOT NULL DEFAULT 'Untitled Canvas',
"width" integer NOT NULL DEFAULT 1024,
"height" integer NOT NULL DEFAULT 1024,
"default_model" text NOT NULL DEFAULT 'gemini-2.0-flash-exp-image-generation',
"default_style" text NOT NULL DEFAULT 'default',
"background_prompt" text,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
)
`
console.log("✅ Created canvas table")
await sql`
CREATE TABLE IF NOT EXISTS "canvas_images" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"canvas_id" uuid NOT NULL REFERENCES "canvas"("id") ON DELETE cascade,
"name" text NOT NULL DEFAULT 'Untitled Image',
"prompt" text NOT NULL DEFAULT '',
"model_id" text NOT NULL DEFAULT 'gemini-2.0-flash-exp-image-generation',
"model_used" text,
"style_id" text NOT NULL DEFAULT 'default',
"width" integer NOT NULL DEFAULT 512,
"height" integer NOT NULL DEFAULT 512,
"position" jsonb NOT NULL DEFAULT '{"x": 0, "y": 0}',
"rotation" double precision NOT NULL DEFAULT 0,
"content_base64" text,
"image_url" text,
"metadata" jsonb,
"branch_parent_id" uuid,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
)
`
console.log("✅ Created canvas_images table")
await sql`
CREATE TABLE IF NOT EXISTS "context_items" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"user_id" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"type" varchar(32) NOT NULL,
"url" text,
"name" text NOT NULL,
"content" text,
"refreshing" boolean NOT NULL DEFAULT false,
"parent_id" integer,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
)
`
console.log("✅ Created context_items table")
await sql`
CREATE TABLE IF NOT EXISTS "thread_context_items" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"thread_id" integer NOT NULL REFERENCES "chat_threads"("id") ON DELETE cascade,
"context_item_id" integer NOT NULL REFERENCES "context_items"("id") ON DELETE cascade,
"created_at" timestamptz NOT NULL DEFAULT now()
)
`
console.log("✅ Created thread_context_items table")
console.log("\n🎉 All tables created successfully!")
// List tables
const tables = await sql`
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'public'
ORDER BY table_name
`
console.log("\n📋 Tables in database:")
for (const t of tables) {
console.log(` - ${t.table_name}`)
}
} catch (err) {
console.error("❌ Error:", err)
} finally {
await sql.end()
}
}
pushSchema()

View File

@@ -0,0 +1,118 @@
-- PlanetScale Postgres Schema
-- Run this in PlanetScale's web console if API token doesn't have CREATE permissions
-- Better-auth tables (camelCase columns)
CREATE TABLE IF NOT EXISTS "users" (
"id" text PRIMARY KEY,
"name" text NOT NULL,
"email" text NOT NULL UNIQUE,
"emailVerified" boolean NOT NULL DEFAULT false,
"image" text,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS "sessions" (
"id" text PRIMARY KEY,
"expiresAt" timestamptz NOT NULL,
"token" text NOT NULL UNIQUE,
"createdAt" timestamptz NOT NULL,
"updatedAt" timestamptz NOT NULL,
"ipAddress" text,
"userAgent" text,
"userId" text NOT NULL REFERENCES "users"("id") ON DELETE cascade
);
CREATE TABLE IF NOT EXISTS "accounts" (
"id" text PRIMARY KEY,
"accountId" text NOT NULL,
"providerId" text NOT NULL,
"userId" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"accessToken" text,
"refreshToken" text,
"idToken" text,
"accessTokenExpiresAt" timestamptz,
"refreshTokenExpiresAt" timestamptz,
"scope" text,
"password" text,
"createdAt" timestamptz NOT NULL,
"updatedAt" timestamptz NOT NULL
);
CREATE TABLE IF NOT EXISTS "verifications" (
"id" text PRIMARY KEY,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expiresAt" timestamptz NOT NULL,
"createdAt" timestamptz DEFAULT now(),
"updatedAt" timestamptz DEFAULT now()
);
-- App tables (snake_case for Electric sync)
CREATE TABLE IF NOT EXISTS "chat_threads" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"title" text NOT NULL,
"user_id" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS "chat_messages" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"thread_id" integer NOT NULL REFERENCES "chat_threads"("id") ON DELETE cascade,
"role" varchar(32) NOT NULL,
"content" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS "canvas" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"owner_id" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"name" text NOT NULL DEFAULT 'Untitled Canvas',
"width" integer NOT NULL DEFAULT 1024,
"height" integer NOT NULL DEFAULT 1024,
"default_model" text NOT NULL DEFAULT 'gemini-2.0-flash-exp-image-generation',
"default_style" text NOT NULL DEFAULT 'default',
"background_prompt" text,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS "canvas_images" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
"canvas_id" uuid NOT NULL REFERENCES "canvas"("id") ON DELETE cascade,
"name" text NOT NULL DEFAULT 'Untitled Image',
"prompt" text NOT NULL DEFAULT '',
"model_id" text NOT NULL DEFAULT 'gemini-2.0-flash-exp-image-generation',
"model_used" text,
"style_id" text NOT NULL DEFAULT 'default',
"width" integer NOT NULL DEFAULT 512,
"height" integer NOT NULL DEFAULT 512,
"position" jsonb NOT NULL DEFAULT '{"x": 0, "y": 0}',
"rotation" double precision NOT NULL DEFAULT 0,
"content_base64" text,
"image_url" text,
"metadata" jsonb,
"branch_parent_id" uuid,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS "context_items" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"user_id" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"type" varchar(32) NOT NULL,
"url" text,
"name" text NOT NULL,
"content" text,
"refreshing" boolean NOT NULL DEFAULT false,
"parent_id" integer,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE IF NOT EXISTS "thread_context_items" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"thread_id" integer NOT NULL REFERENCES "chat_threads"("id") ON DELETE cascade,
"context_item_id" integer NOT NULL REFERENCES "context_items"("id") ON DELETE cascade,
"created_at" timestamptz NOT NULL DEFAULT now()
);

View File

@@ -0,0 +1,350 @@
import "dotenv/config"
import crypto from "node:crypto"
import { sql, eq } from "drizzle-orm"
import { getDb, getAuthDb } from "../src/db/connection"
import {
accounts,
chat_messages,
chat_threads,
sessions,
users,
verifications,
} from "../src/db/schema"
const databaseUrl = process.env.DATABASE_URL
if (!databaseUrl) {
throw new Error("DATABASE_URL is required in packages/web/.env")
}
const appDb = getDb(databaseUrl)
const authDb = getAuthDb(databaseUrl)
async function ensureTables() {
await authDb.execute(sql`
CREATE TABLE IF NOT EXISTS "users" (
"id" text PRIMARY KEY,
"name" text NOT NULL,
"email" text NOT NULL UNIQUE,
"emailVerified" boolean NOT NULL DEFAULT false,
"image" text,
"createdAt" timestamptz NOT NULL DEFAULT now(),
"updatedAt" timestamptz NOT NULL DEFAULT now()
);
`)
await authDb.execute(sql`
CREATE TABLE IF NOT EXISTS "sessions" (
"id" text PRIMARY KEY,
"expiresAt" timestamptz NOT NULL,
"token" text NOT NULL UNIQUE,
"createdAt" timestamptz NOT NULL,
"updatedAt" timestamptz NOT NULL,
"ipAddress" text,
"userAgent" text,
"userId" text NOT NULL REFERENCES "users"("id") ON DELETE cascade
);
`)
await authDb.execute(sql`
CREATE TABLE IF NOT EXISTS "accounts" (
"id" text PRIMARY KEY,
"accountId" text NOT NULL,
"providerId" text NOT NULL,
"userId" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"accessToken" text,
"refreshToken" text,
"idToken" text,
"accessTokenExpiresAt" timestamptz,
"refreshTokenExpiresAt" timestamptz,
"scope" text,
"password" text,
"createdAt" timestamptz NOT NULL,
"updatedAt" timestamptz NOT NULL
);
`)
await authDb.execute(sql`
CREATE TABLE IF NOT EXISTS "verifications" (
"id" text PRIMARY KEY,
"identifier" text NOT NULL,
"value" text NOT NULL,
"expiresAt" timestamptz NOT NULL,
"createdAt" timestamptz DEFAULT now(),
"updatedAt" timestamptz DEFAULT now()
);
`)
// Backfill camelCase columns when an older snake_case seed created the tables.
// Add missing legacy snake_case columns first so COALESCE references are safe.
await authDb.execute(sql`
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "email_verified" boolean,
ADD COLUMN IF NOT EXISTS "created_at" timestamptz,
ADD COLUMN IF NOT EXISTS "updated_at" timestamptz
`)
await authDb.execute(sql`
ALTER TABLE "sessions"
ADD COLUMN IF NOT EXISTS "expires_at" timestamptz,
ADD COLUMN IF NOT EXISTS "created_at" timestamptz,
ADD COLUMN IF NOT EXISTS "updated_at" timestamptz,
ADD COLUMN IF NOT EXISTS "ip_address" text,
ADD COLUMN IF NOT EXISTS "user_agent" text,
ADD COLUMN IF NOT EXISTS "user_id" text
`)
await authDb.execute(sql`
ALTER TABLE "accounts"
ADD COLUMN IF NOT EXISTS "account_id" text,
ADD COLUMN IF NOT EXISTS "provider_id" text,
ADD COLUMN IF NOT EXISTS "user_id" text,
ADD COLUMN IF NOT EXISTS "access_token" text,
ADD COLUMN IF NOT EXISTS "refresh_token" text,
ADD COLUMN IF NOT EXISTS "id_token" text,
ADD COLUMN IF NOT EXISTS "access_token_expires_at" timestamptz,
ADD COLUMN IF NOT EXISTS "refresh_token_expires_at" timestamptz,
ADD COLUMN IF NOT EXISTS "created_at" timestamptz,
ADD COLUMN IF NOT EXISTS "updated_at" timestamptz
`)
await authDb.execute(sql`
ALTER TABLE "verifications"
ADD COLUMN IF NOT EXISTS "expires_at" timestamptz,
ADD COLUMN IF NOT EXISTS "created_at" timestamptz,
ADD COLUMN IF NOT EXISTS "updated_at" timestamptz
`)
await authDb.execute(sql`
ALTER TABLE "users"
ADD COLUMN IF NOT EXISTS "emailVerified" boolean DEFAULT false,
ADD COLUMN IF NOT EXISTS "createdAt" timestamptz DEFAULT now(),
ADD COLUMN IF NOT EXISTS "updatedAt" timestamptz DEFAULT now()
`)
await authDb.execute(
sql`UPDATE "users" SET "emailVerified" = COALESCE("emailVerified", "email_verified")`,
)
await authDb.execute(
sql`UPDATE "users" SET "createdAt" = COALESCE("createdAt", "created_at")`,
)
await authDb.execute(
sql`UPDATE "users" SET "updatedAt" = COALESCE("updatedAt", "updated_at")`,
)
await authDb.execute(sql`
ALTER TABLE "sessions"
ADD COLUMN IF NOT EXISTS "expiresAt" timestamptz,
ADD COLUMN IF NOT EXISTS "token" text,
ADD COLUMN IF NOT EXISTS "createdAt" timestamptz,
ADD COLUMN IF NOT EXISTS "updatedAt" timestamptz,
ADD COLUMN IF NOT EXISTS "ipAddress" text,
ADD COLUMN IF NOT EXISTS "userAgent" text,
ADD COLUMN IF NOT EXISTS "userId" text
`)
await authDb.execute(
sql`UPDATE "sessions" SET "expiresAt" = COALESCE("expiresAt", "expires_at")`,
)
await authDb.execute(
sql`UPDATE "sessions" SET "createdAt" = COALESCE("createdAt", "created_at")`,
)
await authDb.execute(
sql`UPDATE "sessions" SET "updatedAt" = COALESCE("updatedAt", "updated_at")`,
)
await authDb.execute(
sql`UPDATE "sessions" SET "ipAddress" = COALESCE("ipAddress", "ip_address")`,
)
await authDb.execute(
sql`UPDATE "sessions" SET "userAgent" = COALESCE("userAgent", "user_agent")`,
)
await authDb.execute(
sql`UPDATE "sessions" SET "userId" = COALESCE("userId", "user_id")`,
)
await authDb.execute(sql`
ALTER TABLE "accounts"
ADD COLUMN IF NOT EXISTS "accountId" text,
ADD COLUMN IF NOT EXISTS "providerId" text,
ADD COLUMN IF NOT EXISTS "userId" text,
ADD COLUMN IF NOT EXISTS "accessToken" text,
ADD COLUMN IF NOT EXISTS "refreshToken" text,
ADD COLUMN IF NOT EXISTS "idToken" text,
ADD COLUMN IF NOT EXISTS "accessTokenExpiresAt" timestamptz,
ADD COLUMN IF NOT EXISTS "refreshTokenExpiresAt" timestamptz,
ADD COLUMN IF NOT EXISTS "createdAt" timestamptz,
ADD COLUMN IF NOT EXISTS "updatedAt" timestamptz
`)
await authDb.execute(
sql`UPDATE "accounts" SET "accountId" = COALESCE("accountId", "account_id")`,
)
await authDb.execute(
sql`UPDATE "accounts" SET "providerId" = COALESCE("providerId", "provider_id")`,
)
await authDb.execute(
sql`UPDATE "accounts" SET "userId" = COALESCE("userId", "user_id")`,
)
await authDb.execute(
sql`UPDATE "accounts" SET "accessToken" = COALESCE("accessToken", "access_token")`,
)
await authDb.execute(
sql`UPDATE "accounts" SET "refreshToken" = COALESCE("refreshToken", "refresh_token")`,
)
await authDb.execute(
sql`UPDATE "accounts" SET "idToken" = COALESCE("idToken", "id_token")`,
)
await authDb.execute(
sql`UPDATE "accounts" SET "accessTokenExpiresAt" = COALESCE("accessTokenExpiresAt", "access_token_expires_at")`,
)
await authDb.execute(
sql`UPDATE "accounts" SET "refreshTokenExpiresAt" = COALESCE("refreshTokenExpiresAt", "refresh_token_expires_at")`,
)
await authDb.execute(
sql`UPDATE "accounts" SET "createdAt" = COALESCE("createdAt", "created_at")`,
)
await authDb.execute(
sql`UPDATE "accounts" SET "updatedAt" = COALESCE("updatedAt", "updated_at")`,
)
await authDb.execute(sql`
ALTER TABLE "verifications"
ADD COLUMN IF NOT EXISTS "expiresAt" timestamptz,
ADD COLUMN IF NOT EXISTS "createdAt" timestamptz,
ADD COLUMN IF NOT EXISTS "updatedAt" timestamptz
`)
await authDb.execute(
sql`UPDATE "verifications" SET "expiresAt" = COALESCE("expiresAt", "expires_at")`,
)
await authDb.execute(
sql`UPDATE "verifications" SET "createdAt" = COALESCE("createdAt", "created_at")`,
)
await authDb.execute(
sql`UPDATE "verifications" SET "updatedAt" = COALESCE("updatedAt", "updated_at")`,
)
await appDb.execute(sql`
CREATE TABLE IF NOT EXISTS "chat_threads" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"title" text NOT NULL,
"user_id" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now()
);
`)
await appDb.execute(sql`
CREATE TABLE IF NOT EXISTS "chat_messages" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"thread_id" integer NOT NULL REFERENCES "chat_threads"("id") ON DELETE cascade,
"role" varchar(32) NOT NULL,
"content" text NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT now()
);
`)
await appDb.execute(sql`
CREATE TABLE IF NOT EXISTS "context_items" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"user_id" text NOT NULL REFERENCES "users"("id") ON DELETE cascade,
"type" varchar(32) NOT NULL,
"url" text,
"name" text NOT NULL,
"content" text,
"refreshing" boolean NOT NULL DEFAULT false,
"parent_id" integer,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now()
);
`)
await appDb.execute(sql`
CREATE TABLE IF NOT EXISTS "thread_context_items" (
"id" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
"thread_id" integer NOT NULL REFERENCES "chat_threads"("id") ON DELETE cascade,
"context_item_id" integer NOT NULL REFERENCES "context_items"("id") ON DELETE cascade,
"created_at" timestamptz NOT NULL DEFAULT now()
);
`)
}
async function seed() {
await ensureTables()
const demoUserId = "demo-user"
const demoEmail = "demo@ai.chat"
await authDb
.insert(users)
.values({
id: demoUserId,
name: "Demo User",
email: demoEmail,
emailVerified: true,
image: null,
createdAt: new Date(),
updatedAt: new Date(),
})
.onConflictDoNothing({ target: users.id })
// Clear any orphaned auth rows for the demo user to keep data tidy
await authDb.delete(sessions).where(eq(sessions.userId, demoUserId))
await authDb.delete(accounts).where(eq(accounts.userId, demoUserId))
await authDb.delete(verifications).where(eq(verifications.identifier, demoEmail))
// Find or create a chat thread for the demo user
const [existingThread] = await appDb
.select()
.from(chat_threads)
.where(eq(chat_threads.user_id, demoUserId))
.limit(1)
const [thread] =
existingThread && existingThread.id
? [existingThread]
: await appDb
.insert(chat_threads)
.values({
title: "Getting started with AI chat",
user_id: demoUserId,
})
.returning()
const threadId = thread.id
await appDb
.delete(chat_messages)
.where(eq(chat_messages.thread_id, threadId))
const starterMessages = [
{
role: "user",
content: "How do I get reliable AI chat responses from this app?",
},
{
role: "assistant",
content:
"Each thread keeps your message history. You can seed demos like this one, or stream responses from your AI provider. Try adding more messages to this thread.",
},
{
role: "user",
content: "Can I hook this up to my own model API?",
},
{
role: "assistant",
content:
"Yes. Point your server-side handler at your model endpoint and persist messages into the database. Electric can sync them live to the client.",
},
]
await appDb.insert(chat_messages).values(
starterMessages.map((msg) => ({
thread_id: threadId,
role: msg.role,
content: msg.content,
created_at: new Date(),
})),
)
}
seed()
.then(() => {
console.log("Seed complete: demo user and chat thread ready.")
})
.catch((err) => {
console.error(err)
process.exit(1)
})