mirror of
https://github.com/jakejarvis/sofa.git
synced 2026-08-29 01:35:39 -04:00
Switch from Node.js + pnpm to Bun runtime and package manager
Replace @libsql/client with bun:sqlite for zero-dependency SQLite access, swap drizzle-orm/libsql adapter for drizzle-orm/bun-sqlite, and rewrite Dockerfile to use oven/bun:1-alpine. The raw Database instance is no longer exported via Proxy (native C++ methods lose `this` binding through Reflect.get); instead, a closeDatabase() helper handles graceful shutdown and the health check uses Drizzle's db.run() directly. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@ TMDB_API_READ_ACCESS_TOKEN=
|
|||||||
|
|
||||||
# ─── Auth (required) ───────────────────────────────────────────────────────
|
# ─── Auth (required) ───────────────────────────────────────────────────────
|
||||||
# Random secret for session encryption (min 32 chars)
|
# Random secret for session encryption (min 32 chars)
|
||||||
# Generate one with `npx @better-auth/cli secret` or `openssl rand -base64 32`
|
# Generate one with `bunx @better-auth/cli secret` or `openssl rand -base64 32`
|
||||||
BETTER_AUTH_SECRET=
|
BETTER_AUTH_SECRET=
|
||||||
# Public URL of your instance, especially important if reverse proxy is used
|
# Public URL of your instance, especially important if reverse proxy is used
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
BETTER_AUTH_URL=http://localhost:3000
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
|
|||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev # Start Next.js dev server
|
bun dev # Start Next.js dev server
|
||||||
pnpm build # Production build
|
bun build # Production build
|
||||||
pnpm lint # Biome lint check
|
bun lint # Biome lint check
|
||||||
pnpm format # Biome format (auto-fix)
|
bun format # Biome format (auto-fix)
|
||||||
pnpm db:push # Push schema changes to SQLite database
|
bun db:push # Push schema changes to SQLite database
|
||||||
pnpm db:generate # Generate Drizzle migration files
|
bun db:generate # Generate Drizzle migration files
|
||||||
pnpm db:migrate # Run Drizzle migrations
|
bun db:migrate # Run Drizzle migrations
|
||||||
pnpm db:studio # Open Drizzle Studio (visual DB browser)
|
bun db:studio # Open Drizzle Studio (visual DB browser)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@@ -22,7 +22,7 @@ pnpm db:studio # Open Drizzle Studio (visual DB browser)
|
|||||||
### Stack
|
### Stack
|
||||||
|
|
||||||
- **Framework**: Next.js 16 (App Router), React 19, TypeScript
|
- **Framework**: Next.js 16 (App Router), React 19, TypeScript
|
||||||
- **Database**: SQLite via @libsql/client + Drizzle ORM (WAL mode, singleton via `globalThis`, async queries)
|
- **Database**: SQLite via bun:sqlite + Drizzle ORM (WAL mode, singleton via `globalThis`, sync queries)
|
||||||
- **Auth**: Better Auth with Drizzle adapter, email/password
|
- **Auth**: Better Auth with Drizzle adapter, email/password
|
||||||
- **Styling**: Tailwind CSS v4, shadcn components, dark cinema theme with warm primary accents
|
- **Styling**: Tailwind CSS v4, shadcn components, dark cinema theme with warm primary accents
|
||||||
- **Fonts**: DM Serif Display (display), DM Sans (body), Geist Mono (mono)
|
- **Fonts**: DM Serif Display (display), DM Sans (body), Geist Mono (mono)
|
||||||
|
|||||||
+23
-21
@@ -1,49 +1,51 @@
|
|||||||
# syntax=docker/dockerfile:1
|
# syntax=docker/dockerfile:1
|
||||||
FROM node:24-alpine AS base
|
FROM oven/bun:1-alpine AS base
|
||||||
RUN corepack enable && corepack prepare pnpm@10 --activate
|
|
||||||
|
|
||||||
# --- Dependencies ---
|
# --- Dependencies ---
|
||||||
FROM base AS deps
|
FROM base AS deps
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY package.json pnpm-lock.yaml ./
|
|
||||||
RUN pnpm install --frozen-lockfile
|
COPY package.json bun.lock ./
|
||||||
|
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
# --- Builder ---
|
# --- Builder ---
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY --from=deps /app/node_modules ./node_modules
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
ARG GIT_COMMIT_SHA
|
ARG GIT_COMMIT_SHA
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV GIT_COMMIT_SHA=${GIT_COMMIT_SHA}
|
ENV GIT_COMMIT_SHA=${GIT_COMMIT_SHA}
|
||||||
RUN pnpm build
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
|
||||||
|
RUN bun run build
|
||||||
|
|
||||||
# --- Runner ---
|
# --- Runner ---
|
||||||
FROM node:24-alpine AS runner
|
FROM base AS runner
|
||||||
RUN apk add --no-cache tini
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
ENV HOSTNAME=0.0.0.0
|
ENV HOSTNAME=0.0.0.0
|
||||||
ENV PORT=3000
|
ENV PORT=3000
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
ENV DATA_DIR=/data
|
ENV DATA_DIR=/data
|
||||||
|
|
||||||
RUN addgroup --system --gid 1001 nodejs \
|
RUN mkdir -p /data \
|
||||||
&& adduser --system --uid 1001 nextjs \
|
&& chown bun:bun /data
|
||||||
&& mkdir -p /data \
|
|
||||||
&& chown -R nextjs:nodejs /data
|
|
||||||
|
|
||||||
COPY --from=builder --link --chown=nextjs:nodejs /app/public ./public
|
COPY --from=builder --chown=bun:bun /app/public ./public
|
||||||
COPY --from=builder --link --chown=nextjs:nodejs /app/.next/standalone ./
|
COPY --from=builder --chown=bun:bun /app/package.json ./package.json
|
||||||
COPY --from=builder --link --chown=nextjs:nodejs /app/.next/static ./.next/static
|
COPY --from=builder --chown=bun:bun /app/.next/standalone ./
|
||||||
COPY --from=builder --link --chown=nextjs:nodejs /app/drizzle ./drizzle
|
COPY --from=builder --chown=bun:bun /app/.next/static ./.next/static
|
||||||
|
COPY --from=builder --chown=bun:bun /app/drizzle ./drizzle
|
||||||
|
|
||||||
USER nextjs
|
USER bun
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
VOLUME /data
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
||||||
CMD wget -qO /dev/null http://localhost:3000/api/health
|
CMD ["bun", "-e", "fetch('http://localhost:3000/api/health').then(r => process.exit(+!r.ok)).catch(() => process.exit(1))"]
|
||||||
|
|
||||||
ENTRYPOINT ["/sbin/tini", "--"]
|
CMD ["bun", "server.js"]
|
||||||
CMD ["node", "server.js"]
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { client } from "@/lib/db/client";
|
import { db } from "@/lib/db/client";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
await client.execute("SELECT 1");
|
db.run(sql`SELECT 1`);
|
||||||
|
|
||||||
return NextResponse.json({ status: "healthy" }, { status: 200 });
|
return NextResponse.json({ status: "healthy" }, { status: 200 });
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.4.4/schema.json",
|
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
|
|||||||
+5
-5
@@ -1,13 +1,13 @@
|
|||||||
|
import path from "node:path";
|
||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
const DATA_DIR = process.env.DATA_DIR || "./data";
|
|
||||||
const DATABASE_URL = process.env.DATABASE_URL || `file:${DATA_DIR}/sqlite.db`;
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
dialect: "turso",
|
dialect: "sqlite",
|
||||||
schema: "./lib/db/schema.ts",
|
schema: "./lib/db/schema.ts",
|
||||||
out: "./drizzle",
|
out: "./drizzle",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: DATABASE_URL,
|
url:
|
||||||
|
process.env.DATABASE_URL ||
|
||||||
|
path.join(process.env.DATA_DIR || "./data", "sqlite.db"),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE `account` ADD `idToken` text;
|
||||||
File diff suppressed because it is too large
Load Diff
+4
-5
@@ -13,22 +13,22 @@ export async function register() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Run database migrations on startup
|
// Run database migrations on startup
|
||||||
if (process.env.NODE_ENV === "production") {
|
|
||||||
const { runMigrations } = await import("@/lib/db/migrate");
|
const { runMigrations } = await import("@/lib/db/migrate");
|
||||||
await runMigrations();
|
runMigrations();
|
||||||
|
|
||||||
|
// Initialize job scheduler
|
||||||
const { initJobs } = await import("@/lib/jobs/init");
|
const { initJobs } = await import("@/lib/jobs/init");
|
||||||
initJobs();
|
initJobs();
|
||||||
|
|
||||||
// Graceful shutdown — stop jobs and close DB on container stop
|
// Graceful shutdown — stop jobs and close DB on container stop
|
||||||
const { scheduler } = await import("@/lib/jobs/scheduler");
|
const { scheduler } = await import("@/lib/jobs/scheduler");
|
||||||
const { client } = await import("@/lib/db/client");
|
const { closeDatabase } = await import("@/lib/db/client");
|
||||||
|
|
||||||
const shutdown = () => {
|
const shutdown = () => {
|
||||||
console.log("[shutdown] Stopping scheduler...");
|
console.log("[shutdown] Stopping scheduler...");
|
||||||
scheduler.stop();
|
scheduler.stop();
|
||||||
console.log("[shutdown] Closing database...");
|
console.log("[shutdown] Closing database...");
|
||||||
client.close();
|
closeDatabase();
|
||||||
console.log("[shutdown] Clean shutdown complete");
|
console.log("[shutdown] Clean shutdown complete");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
};
|
};
|
||||||
@@ -37,4 +37,3 @@ export async function register() {
|
|||||||
process.on("SIGINT", shutdown);
|
process.on("SIGINT", shutdown);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
+25
-16
@@ -1,23 +1,34 @@
|
|||||||
import { createClient } from "@libsql/client";
|
import { Database } from "bun:sqlite";
|
||||||
import { drizzle } from "drizzle-orm/libsql";
|
import path from "node:path";
|
||||||
|
import { drizzle } from "drizzle-orm/bun-sqlite";
|
||||||
import * as schema from "./schema";
|
import * as schema from "./schema";
|
||||||
|
|
||||||
|
// Lazy-init singleton via globalThis. Next.js evaluates module-level code at
|
||||||
|
// build time (when no database exists) and re-imports modules on HMR in dev
|
||||||
|
// (which would create duplicate connections). Stashing the instances on
|
||||||
|
// globalThis and wrapping `db` in a Proxy defers all real work to the first
|
||||||
|
// property access at runtime, sidestepping both problems.
|
||||||
|
//
|
||||||
|
// Only the Drizzle instance (`db`) is exported as a Proxy — the raw bun:sqlite
|
||||||
|
// Database is kept internal because its native C++ methods lose their `this`
|
||||||
|
// binding when accessed through Reflect.get, so a Proxy around it would break.
|
||||||
|
// Use `closeDatabase()` for graceful shutdown instead.
|
||||||
|
|
||||||
const globalForDb = globalThis as unknown as {
|
const globalForDb = globalThis as unknown as {
|
||||||
_db: ReturnType<typeof drizzle> | undefined;
|
_db: ReturnType<typeof drizzle> | undefined;
|
||||||
_client: ReturnType<typeof createClient> | undefined;
|
_client: Database | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DATA_DIR = process.env.DATA_DIR || "./data";
|
const DATABASE_URL =
|
||||||
const DATABASE_URL = process.env.DATABASE_URL || `file:${DATA_DIR}/sqlite.db`;
|
process.env.DATABASE_URL ||
|
||||||
|
path.join(process.env.DATA_DIR || "./data", "sqlite.db");
|
||||||
|
|
||||||
function getClient() {
|
function getClient() {
|
||||||
if (!globalForDb._client) {
|
if (!globalForDb._client) {
|
||||||
globalForDb._client = createClient({
|
globalForDb._client = new Database(DATABASE_URL);
|
||||||
url: DATABASE_URL,
|
globalForDb._client.run("PRAGMA journal_mode = WAL");
|
||||||
});
|
globalForDb._client.run("PRAGMA foreign_keys = ON");
|
||||||
globalForDb._client.execute("PRAGMA journal_mode = WAL");
|
globalForDb._client.run("PRAGMA busy_timeout = 5000");
|
||||||
globalForDb._client.execute("PRAGMA foreign_keys = ON");
|
|
||||||
globalForDb._client.execute("PRAGMA busy_timeout = 5000");
|
|
||||||
}
|
}
|
||||||
return globalForDb._client;
|
return globalForDb._client;
|
||||||
}
|
}
|
||||||
@@ -35,8 +46,6 @@ export const db = new Proxy({} as ReturnType<typeof drizzle>, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const client = new Proxy({} as ReturnType<typeof createClient>, {
|
export function closeDatabase() {
|
||||||
get(_, prop) {
|
globalForDb._client?.close();
|
||||||
return Reflect.get(getClient(), prop);
|
}
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
import { migrate } from "drizzle-orm/libsql/migrator";
|
import { migrate } from "drizzle-orm/bun-sqlite/migrator";
|
||||||
import { db } from "./client";
|
import { db } from "./client";
|
||||||
|
|
||||||
export async function runMigrations() {
|
export function runMigrations() {
|
||||||
console.log("[migrate] Running database migrations...");
|
console.log("[migrate] Running database migrations...");
|
||||||
await migrate(db, { migrationsFolder: "./drizzle" });
|
migrate(db, { migrationsFolder: "./drizzle" });
|
||||||
console.log("[migrate] Database migrations complete");
|
console.log("[migrate] Database migrations complete");
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-9
@@ -3,9 +3,9 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "bun --bun next dev",
|
||||||
"build": "next build",
|
"build": "bun --bun next build",
|
||||||
"start": "next start",
|
"start": "bun --bun next start",
|
||||||
"lint": "biome check",
|
"lint": "biome check",
|
||||||
"format": "biome format --write",
|
"format": "biome format --write",
|
||||||
"check-types": "tsc --noEmit",
|
"check-types": "tsc --noEmit",
|
||||||
@@ -16,10 +16,9 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.2.0",
|
"@base-ui/react": "^1.2.0",
|
||||||
"@libsql/client": "^0.17.0",
|
"@tabler/icons-react": "^3.38.0",
|
||||||
"@tabler/icons-react": "^3.37.1",
|
|
||||||
"@tanstack/react-hotkeys": "^0.3.1",
|
"@tanstack/react-hotkeys": "^0.3.1",
|
||||||
"better-auth": "^1.5.0",
|
"better-auth": "^1.5.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
@@ -27,7 +26,7 @@
|
|||||||
"drizzle-orm": "1.0.0-beta.15-859cf75",
|
"drizzle-orm": "1.0.0-beta.15-859cf75",
|
||||||
"embla-carousel-react": "^8.6.0",
|
"embla-carousel-react": "^8.6.0",
|
||||||
"jotai": "^2.18.0",
|
"jotai": "^2.18.0",
|
||||||
"motion": "^12.34.3",
|
"motion": "^12.34.5",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"node-vibrant": "^4.0.4",
|
"node-vibrant": "^4.0.4",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
@@ -44,7 +43,8 @@
|
|||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "2.4.4",
|
"@biomejs/biome": "2.4.5",
|
||||||
|
"@types/bun": "^1.3.10",
|
||||||
"@tailwindcss/postcss": "^4.2.1",
|
"@tailwindcss/postcss": "^4.2.1",
|
||||||
"@types/node": "^25.3.3",
|
"@types/node": "^25.3.3",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
@@ -53,5 +53,6 @@
|
|||||||
"drizzle-kit": "1.0.0-beta.15-859cf75",
|
"drizzle-kit": "1.0.0-beta.15-859cf75",
|
||||||
"tailwindcss": "^4.2.1",
|
"tailwindcss": "^4.2.1",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
}
|
},
|
||||||
|
"packageManager": "bun@1.3.10"
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
-7731
File diff suppressed because it is too large
Load Diff
@@ -1,8 +0,0 @@
|
|||||||
ignoredBuiltDependencies:
|
|
||||||
- msw
|
|
||||||
- sharp
|
|
||||||
- unrs-resolver
|
|
||||||
|
|
||||||
onlyBuiltDependencies:
|
|
||||||
- better-sqlite3
|
|
||||||
- esbuild
|
|
||||||
Reference in New Issue
Block a user