diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4209292..d03d5d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,9 @@ on: pull_request: branches: [main, develop] +env: + BUN_VERSION: "1.3.10" + jobs: test: name: Test @@ -15,7 +18,8 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: ${{ env.BUN_VERSION }} + cache: true - name: Install dependencies run: bun install --frozen-lockfile @@ -36,7 +40,8 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: ${{ env.BUN_VERSION }} + cache: true - name: Install dependencies run: bun install --frozen-lockfile @@ -54,7 +59,8 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 with: - bun-version: latest + bun-version: ${{ env.BUN_VERSION }} + cache: true - name: Install dependencies run: bun install --frozen-lockfile diff --git a/Dockerfile.project b/Dockerfile.project index aa29501..deef6d3 100644 --- a/Dockerfile.project +++ b/Dockerfile.project @@ -15,7 +15,7 @@ # ---------------------------------------------------------------------------- # Stage 1: Base # ---------------------------------------------------------------------------- -FROM oven/bun:1.3.9-debian AS base +FROM oven/bun@sha256:5ee6c5be4575d5ba079b5a9afb24d4600f75ccb1a92602f079ee99560b9dcee9 AS base LABEL maintainer="Betterbase Team" LABEL description="Betterbase Project - AI-Native Backend Platform" @@ -54,14 +54,10 @@ RUN bun install --frozen-lockfile # ---------------------------------------------------------------------------- # Stage 3: Builder # ---------------------------------------------------------------------------- -FROM base AS builder +FROM deps AS builder WORKDIR /app -# Copy lockfile and install all dependencies -COPY package.json bun.lock ./ -RUN bun install --frozen-lockfile - # Copy source code COPY . . diff --git a/apps/dashboard/src/components/auth/SetupGuard.tsx b/apps/dashboard/src/components/auth/SetupGuard.tsx index 0496bd9..3959cfd 100644 --- a/apps/dashboard/src/components/auth/SetupGuard.tsx +++ b/apps/dashboard/src/components/auth/SetupGuard.tsx @@ -1,22 +1,15 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router"; +import { checkSetup } from "../lib/api"; export function SetupGuard({ children }: { children: React.ReactNode }) { const navigate = useNavigate(); const [checking, setChecking] = useState(true); useEffect(() => { - // Try hitting /admin/auth/setup without a token. - // If setup is complete, login page is appropriate. - // If setup is not done, /admin/auth/setup returns 201, not 410. - fetch(`${import.meta.env.VITE_API_URL ?? "http://localhost:3001"}/admin/auth/setup`, { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ _check: true }), // Will fail validation but we only care about 410 - }) - .then((res) => { - if (res.status === 410) { - // Setup complete — redirect to login + checkSetup() + .then((isSetup) => { + if (isSetup) { navigate("/login", { replace: true }); } setChecking(false); diff --git a/apps/dashboard/src/lib/api.ts b/apps/dashboard/src/lib/api.ts index b1e95be..9eaceb8 100644 --- a/apps/dashboard/src/lib/api.ts +++ b/apps/dashboard/src/lib/api.ts @@ -1,4 +1,4 @@ -const API_BASE = import.meta.env.VITE_API_URL ?? "http://localhost:3001"; +const API_BASE = import.meta.env.VITE_API_URL; export class ApiError extends Error { constructor( @@ -94,3 +94,10 @@ export const api = { return res.blob(); }, }; + +export async function checkSetup(): Promise { + const res = await fetch(`${API_BASE}/admin/auth/setup/check`, { + method: "GET", + }); + return res.status !== 410; +} diff --git a/apps/dashboard/src/vite-env.d.ts b/apps/dashboard/src/vite-env.d.ts index 6bb3492..42039ea 100644 --- a/apps/dashboard/src/vite-env.d.ts +++ b/apps/dashboard/src/vite-env.d.ts @@ -1,7 +1,7 @@ /// interface ImportMetaEnv { - readonly VITE_API_URL?: string; + readonly VITE_API_URL: string; } interface ImportMeta { diff --git a/bun.lock b/bun.lock index ff88c63..e50c12e 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "betterbase", @@ -133,6 +134,7 @@ "@hono/zod-validator": "^0.4.0", "bcryptjs": "^2.4.3", "hono": "^4.0.0", + "hono-rate-limit": "^1.0.2", "inngest": "^3.0.0", "jose": "^5.0.0", "nanoid": "^5.0.0", @@ -859,6 +861,18 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], + "@redis/bloom": ["@redis/bloom@1.2.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg=="], + + "@redis/client": ["@redis/client@1.6.1", "", { "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", "yallist": "4.0.0" } }, "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw=="], + + "@redis/graph": ["@redis/graph@1.1.1", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw=="], + + "@redis/json": ["@redis/json@1.0.7", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ=="], + + "@redis/search": ["@redis/search@1.2.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw=="], + + "@redis/time-series": ["@redis/time-series@1.1.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g=="], + "@repeaterjs/repeater": ["@repeaterjs/repeater@3.0.6", "", {}, "sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA=="], "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.27", "", {}, "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA=="], @@ -1219,6 +1233,8 @@ "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + "cmdk": ["cmdk@1.1.1", "", { "dependencies": { "@radix-ui/react-compose-refs": "^1.1.1", "@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-id": "^1.1.0", "@radix-ui/react-primitive": "^2.0.2" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg=="], "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], @@ -1341,6 +1357,8 @@ "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "generic-pool": ["generic-pool@3.9.0", "", {}, "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g=="], + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -1369,6 +1387,8 @@ "hono": ["hono@4.12.0", "", {}, "sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA=="], + "hono-rate-limit": ["hono-rate-limit@1.0.2", "", { "dependencies": { "redis": "^4.6.13" } }, "sha512-egxPn/+825taWqg7NU/4jrKO7m48rfvrcB5vv2IMd0EfLLzOEBICfoTqI6oQYRNoKZGs03vG5IkoAPmbHLeOlg=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], @@ -1589,6 +1609,8 @@ "recharts-scale": ["recharts-scale@0.4.5", "", { "dependencies": { "decimal.js-light": "^2.4.1" } }, "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w=="], + "redis": ["redis@4.7.1", "", { "dependencies": { "@redis/bloom": "1.2.0", "@redis/client": "1.6.1", "@redis/graph": "1.1.1", "@redis/json": "1.0.7", "@redis/search": "1.2.0", "@redis/time-series": "1.1.0" } }, "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-in-the-middle": ["require-in-the-middle@8.0.1", "", { "dependencies": { "debug": "^4.3.5", "module-details-from-path": "^1.0.3" } }, "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ=="], @@ -1733,7 +1755,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], @@ -1893,6 +1915,8 @@ "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "@betterbase/core/@libsql/client/@libsql/core": ["@libsql/core@0.17.2", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-L8qv12HZ/jRBcETVR3rscP0uHNxh+K3EABSde6scCw7zfOdiLqO3MAkJaeE1WovPsjXzsN/JBoZED4+7EZVT3g=="], "@betterbase/core/@libsql/client/libsql": ["libsql@0.5.28", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.28", "@libsql/darwin-x64": "0.5.28", "@libsql/linux-arm-gnueabihf": "0.5.28", "@libsql/linux-arm-musleabihf": "0.5.28", "@libsql/linux-arm64-gnu": "0.5.28", "@libsql/linux-arm64-musl": "0.5.28", "@libsql/linux-x64-gnu": "0.5.28", "@libsql/linux-x64-musl": "0.5.28", "@libsql/win32-x64-msvc": "0.5.28" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-wKqx9FgtPcKHdPfR/Kfm0gejsnbuf8zV+ESPmltFvsq5uXwdeN9fsWn611DmqrdXj1e94NkARcMA2f1syiAqOg=="], diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 50ba660..d3b398a 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,5 +1,3 @@ -version: "3.9" - # Local development: runs Inngest dev server only. # BetterBase server runs outside Docker via: bun run dev # diff --git a/docker-compose.self-hosted.yml b/docker-compose.self-hosted.yml index e1309ca..67df7b3 100644 --- a/docker-compose.self-hosted.yml +++ b/docker-compose.self-hosted.yml @@ -148,7 +148,7 @@ services: volumes: - ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro healthcheck: - test: ["CMD", "nginx", "-t"] + test: ["CMD", "wget", "-qO-", "http://localhost/health"] interval: 30s timeout: 10s retries: 3 diff --git a/docker-compose.yml b/docker-compose.yml index 111aa14..25f28c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -31,13 +31,14 @@ services: # MinIO (S3-compatible storage) minio: - image: minio/minio:latest + image: bitnami/minio:2025.1.16 container_name: betterbase-minio-local restart: unless-stopped command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-betterbase} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-betterbase_dev_password} + MINIO_DATA_DIR: /data ports: - "9000:9000" - "9001:9001" @@ -51,7 +52,7 @@ services: # MinIO Init (Create bucket on startup) minio-init: - image: minio/mc:latest + image: minio/mc:RELEASE.2025-08-13T08-35-41Z container_name: betterbase-minio-init-local depends_on: minio: diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ce7127e..0f72631 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,5 +1,5 @@ -import { Command, CommanderError } from "commander"; import chalk from "chalk"; +import { Command, CommanderError } from "commander"; import packageJson from "../package.json"; import { runAuthAddProviderCommand, runAuthSetupCommand } from "./commands/auth"; import { runBranchCommand } from "./commands/branch"; @@ -29,13 +29,24 @@ import { runWebhookCommand } from "./commands/webhook"; import * as logger from "./utils/logger"; // Commands that don't require authentication -const PUBLIC_COMMANDS = ["login", "logout", "version", "help", "init"]; +const PUBLIC_COMMANDS = [ + "login", + "logout", + "version", + "help", + "init", + "--version", + "-v", + "--help", + "-h", + "-V", +]; /** * Check if the user is authenticated before running a command. */ -async function checkAuthHook(): Promise { - const commandName = process.argv[2]; +async function checkAuthHook(this: Command, actionCommand?: Command): Promise { + const commandName = actionCommand?.name() ?? this.args?.[0] ?? process.argv[2] ?? ""; // Skip auth check for public commands if (PUBLIC_COMMANDS.includes(commandName)) { @@ -120,7 +131,7 @@ export function createProgram(): Command { .description("Initialize a BetterBase project with BetterBase template (betterbase/ functions)") .option("--no-iac", "Use interactive mode instead of BetterBase template (for legacy projects)") .argument("[project-name]", "project name") - .action(async (options: { iac?: boolean }, projectName?: string) => { + .action(async (projectName: string | undefined, options: { iac?: boolean }) => { await runInitCommand({ projectName, ...options }); }); @@ -476,9 +487,10 @@ export function createProgram(): Command { .argument("", "function name") .option("--sync-env", "Sync environment variables from .env") .argument("[project-root]", "project root directory", process.cwd()) - .action(async (name: string, options: { syncEnv?: boolean; projectRoot?: string }) => { - const projectRoot = options.projectRoot ?? process.cwd(); - await runFunctionCommand(["deploy", name, options.syncEnv ? "--sync-env" : ""], projectRoot); + .action(async (name: string, projectRootArg: string, options: { syncEnv?: boolean }) => { + const args = ["deploy", name]; + if (options.syncEnv) args.push("--sync-env"); + await runFunctionCommand(args, projectRootArg); }); // ── bb login — STAGED FOR ACTIVATION ──────────────────────────────────────── @@ -541,9 +553,8 @@ export function createProgram(): Command { }); branch - .argument("[project-root]", "project root directory", process.cwd()) .option("-p, --project-root ", "project root directory", process.cwd()) - .action(async (options) => { + .action(async (options: { projectRoot?: string }) => { const projectRoot = options.projectRoot || process.cwd(); await runBranchCommand([], projectRoot); }); diff --git a/packages/client/src/iac/provider.tsx b/packages/client/src/iac/provider.tsx index 891c435..a679f96 100644 --- a/packages/client/src/iac/provider.tsx +++ b/packages/client/src/iac/provider.tsx @@ -25,32 +25,62 @@ export function BetterbaseProvider({ const [wsReady, setWsReady] = React.useState(false); useEffect(() => { - const wsUrl = `${config.url.replace(/^http/, "ws")}/betterbase/ws?project=${config.projectSlug ?? "default"}`; - const ws = new WebSocket(wsUrl); + let timeoutId: ReturnType | null = null; + let isCleaned = false; + let reconnectDelayMs = 3_000; + const maxReconnectDelayMs = 30_000; - ws.onopen = () => { - setWsReady(true); - }; - ws.onclose = () => { + function connect() { + if (isCleaned) return; setWsReady(false); - // Reconnect after 3 seconds - setTimeout(() => { - wsRef.current = new WebSocket(wsUrl); - }, 3_000); - }; + const baseUrl = config.url.replace(/^http/, "ws"); + const projectPart = `project=${config.projectSlug ?? "default"}`; + let wsUrl = `${baseUrl}/betterbase/ws?${projectPart}`; - wsRef.current = ws; + const token = config.getToken?.(); + if (token) { + wsUrl += `&token=${encodeURIComponent(token)}`; + } - // Handle pings - ws.onmessage = (event) => { - const msg = JSON.parse(event.data); - if (msg.type === "ping") ws.send(JSON.stringify({ type: "pong" })); - }; + const ws = new WebSocket(wsUrl); + wsRef.current = ws; + + ws.onopen = () => { + if (!isCleaned) { + setWsReady(true); + reconnectDelayMs = 3_000; + } + }; + ws.onerror = (err) => { + if (isCleaned) return; + console.error("WebSocket error", err); + }; + ws.onclose = () => { + if (isCleaned) return; + wsRef.current = null; + timeoutId = setTimeout(connect, reconnectDelayMs); + reconnectDelayMs = Math.min(reconnectDelayMs * 2, maxReconnectDelayMs); + }; + + ws.onmessage = (event) => { + try { + const msg = JSON.parse(event.data); + if (msg.type === "ping") ws.send(JSON.stringify({ type: "pong" })); + } catch { + return; + } + }; + } + + connect(); return () => { - ws.close(); + isCleaned = true; + setWsReady(false); + if (timeoutId !== null) clearTimeout(timeoutId); + wsRef.current?.close(); }; - }, [config.url, config.projectSlug]); + }, [config.url, config.projectSlug, config.getToken]); return ( , + userRole?: string | null, ): boolean { // Handle simple boolean policies if (policyExpression === "true") { @@ -59,10 +60,14 @@ export function evaluatePolicy( const roleMatch = policyExpression.match(/auth\.role\(\)\s*=\s*'([^']+)'/); if (roleMatch) { const requiredRole = roleMatch[1]; - // In a full implementation, we'd get the user's role from the session - // For now, we'll check if userId starts with the role prefix - // This is a simplified implementation - return false; // Deny by default if role check not implemented + + // If userRole is not provided, deny by default + if (userRole === null || userRole === undefined) { + return false; + } + + // Check if the user's role matches the required role + return userRole === requiredRole; } // Unknown policy format - deny by default for security @@ -83,6 +88,7 @@ export function applyRLSSelect( rows: Record[], policies: PolicyDefinition[], userId: string | null, + userRole?: string | null, ): Record[] { // If no policies, return all rows (or none for non-authenticated if needed) if (policies.length === 0) { @@ -107,7 +113,7 @@ export function applyRLSSelect( // If ANY policy allows access, the row passes return selectPolicies.some((policy) => { const policyExpr = policy.select || policy.using; - return evaluatePolicy(policyExpr!, userId, "select", row); + return evaluatePolicy(policyExpr!, userId, "select", row, userRole); }); }); } @@ -124,6 +130,7 @@ export function applyRLSInsert( policy: string | undefined, userId: string | null, record: Record, + userRole?: string | null, ): void { // If no policy, check authentication requirement if (!policy) { @@ -134,7 +141,7 @@ export function applyRLSInsert( } // Evaluate the policy - const allowed = evaluatePolicy(policy, userId, "insert", record); + const allowed = evaluatePolicy(policy, userId, "insert", record, userRole); if (!allowed) { throw new UnauthorizedError("Insert denied by RLS policy"); @@ -153,6 +160,7 @@ export function applyRLSUpdate( policy: string | undefined, userId: string | null, record: Record, + userRole?: string | null, ): void { // If no policy, check authentication requirement if (!policy) { @@ -164,7 +172,7 @@ export function applyRLSUpdate( // Evaluate the policy - use "using" or "withCheck" expression const policyExpr = policy; - const allowed = evaluatePolicy(policyExpr, userId, "update", record); + const allowed = evaluatePolicy(policyExpr, userId, "update", record, userRole); if (!allowed) { throw new UnauthorizedError("Update denied by RLS policy"); @@ -183,6 +191,7 @@ export function applyRLSDelete( policy: string | undefined, userId: string | null, record: Record, + userRole?: string | null, ): void { // If no policy, check authentication requirement if (!policy) { @@ -193,7 +202,7 @@ export function applyRLSDelete( } // Evaluate the policy - const allowed = evaluatePolicy(policy, userId, "delete", record); + const allowed = evaluatePolicy(policy, userId, "delete", record, userRole); if (!allowed) { throw new UnauthorizedError("Delete denied by RLS policy"); @@ -206,16 +215,22 @@ export function applyRLSDelete( * * @param policies - Array of policy definitions * @param getUserId - Function to get current user ID from request context + * @param getUserRole - Optional function to get current user role from request context * @returns RLS middleware functions */ -export function createRLSMiddleware(policies: PolicyDefinition[], getUserId: () => string | null) { +export function createRLSMiddleware( + policies: PolicyDefinition[], + getUserId: () => string | null, + getUserRole?: () => string | null, +) { return { /** * Apply RLS to SELECT operations */ select: (rows: Record[]) => { const userId = getUserId(); - return applyRLSSelect(rows, policies, userId); + const userRole = getUserRole?.(); + return applyRLSSelect(rows, policies, userId, userRole); }, /** @@ -223,8 +238,9 @@ export function createRLSMiddleware(policies: PolicyDefinition[], getUserId: () */ insert: (record: Record) => { const userId = getUserId(); + const userRole = getUserRole?.(); const policy = policies.find((p) => p.insert || p.withCheck); - applyRLSInsert(policy?.insert || policy?.withCheck, userId, record); + applyRLSInsert(policy?.insert || policy?.withCheck, userId, record, userRole); }, /** @@ -232,8 +248,9 @@ export function createRLSMiddleware(policies: PolicyDefinition[], getUserId: () */ update: (record: Record) => { const userId = getUserId(); + const userRole = getUserRole?.(); const policy = policies.find((p) => p.update || p.using); - applyRLSUpdate(policy?.update || policy?.using, userId, record); + applyRLSUpdate(policy?.update || policy?.using, userId, record, userRole); }, /** @@ -241,8 +258,9 @@ export function createRLSMiddleware(policies: PolicyDefinition[], getUserId: () */ delete: (record: Record) => { const userId = getUserId(); + const userRole = getUserRole?.(); const policy = policies.find((p) => p.delete); - applyRLSDelete(policy?.delete, userId, record); + applyRLSDelete(policy?.delete, userId, record, userRole); }, }; } diff --git a/packages/server/package.json b/packages/server/package.json index 8ed95e5..de43691 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -19,6 +19,7 @@ "@hono/zod-validator": "^0.4.0", "bcryptjs": "^2.4.3", "hono": "^4.0.0", + "hono-rate-limit": "^1.0.2", "inngest": "^3.0.0", "jose": "^5.0.0", "nanoid": "^5.0.0", diff --git a/packages/server/src/index.ts b/packages/server/src/index.ts index 6fc5829..30955bd 100644 --- a/packages/server/src/index.ts +++ b/packages/server/src/index.ts @@ -2,6 +2,7 @@ import { Hono } from "hono"; import { cors } from "hono/cors"; import { logger } from "hono/logger"; import { serve } from "inngest/hono"; +import { csrfMiddleware } from "./lib/csrf"; import { getPool } from "./lib/db"; import { validateEnv } from "./lib/env"; import { allInngestFunctions, inngest } from "./lib/inngest"; @@ -54,11 +55,13 @@ app.use( cors({ origin: env.CORS_ORIGINS.split(","), credentials: true, - allowHeaders: ["Content-Type", "Authorization"], + allowHeaders: ["Content-Type", "Authorization", "x-csrf-token"], allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], }), ); +app.use("*", csrfMiddleware); + // Health check — used by Docker HEALTHCHECK app.get("/health", (c) => c.json({ status: "ok", timestamp: new Date().toISOString() })); diff --git a/packages/server/src/lib/auth.ts b/packages/server/src/lib/auth.ts index 6467da9..acd0178 100644 --- a/packages/server/src/lib/auth.ts +++ b/packages/server/src/lib/auth.ts @@ -4,7 +4,8 @@ import type { Pool } from "pg"; const getSecret = () => new TextEncoder().encode(process.env.BETTERBASE_JWT_SECRET!); -const TOKEN_EXPIRY = "30d"; +// 7 days - reduced from 30d for better security. Consider refresh tokens for longer sessions. +const TOKEN_EXPIRY = "7d"; const BCRYPT_ROUNDS = 12; // --- Password --- diff --git a/packages/server/src/lib/csrf.ts b/packages/server/src/lib/csrf.ts new file mode 100644 index 0000000..24fd594 --- /dev/null +++ b/packages/server/src/lib/csrf.ts @@ -0,0 +1,63 @@ +import type { Context, MiddlewareHandler } from "hono"; + +const CSRF_COOKIE_NAME = "csrf-token"; +const CSRF_HEADER_NAME = "x-csrf-token"; + +/** Extract a cookie value from the Cookie header */ +function getCookie(req: Request, name: string): string | null { + const cookieHeader = req.headers.get("cookie"); + if (!cookieHeader) return null; + const match = cookieHeader.match(new RegExp(`(?:^|;\\s*)${name}=([^;]*)`)); + return match ? decodeURIComponent(match[1]) : null; +} + +const CSRF_TOKEN_EXPIRY_MS = 24 * 60 * 60 * 1000; // 24 hours + +const csrfMiddleware: MiddlewareHandler = async (c: Context, next) => { + const path = new URL(c.req.url).pathname; + const method = c.req.method; + + const isSafeMethod = method === "GET" || method === "HEAD" || method === "OPTIONS"; + const isHealthCheck = path === "/health"; + const isInngest = path === "/api/inngest"; + const isWebSocket = path === "/betterbase/ws" && c.req.header("Upgrade") === "websocket"; + const isSetupCheck = path === "/admin/auth/setup/check"; + + if (isSafeMethod || isHealthCheck || isInngest || isWebSocket || isSetupCheck) { + if (!isSafeMethod) { + await next(); + return; + } + + let token = getCookie(c.req.raw, CSRF_COOKIE_NAME); + const tokenExpiry = c.req.header("X-CSRF-Token-Expiry"); + + if (!token || !tokenExpiry || Number(tokenExpiry) < Date.now()) { + token = crypto.randomUUID(); + const expiry = Date.now() + CSRF_TOKEN_EXPIRY_MS; + c.header( + "Set-Cookie", + `${CSRF_COOKIE_NAME}=${token}; SameSite=Strict; Path=/; Max-Age=86400`, + ); + c.header("X-CSRF-Token-Expiry", expiry.toString()); + } + + await next(); + return; + } + + const cookieToken = getCookie(c.req.raw, CSRF_COOKIE_NAME); + const headerToken = c.req.header(CSRF_HEADER_NAME); + + if (!cookieToken || !headerToken) { + return new Response("CSRF token missing", { status: 403 }); + } + + if (cookieToken !== headerToken) { + return new Response("CSRF token mismatch", { status: 403 }); + } + + await next(); +}; + +export { csrfMiddleware }; diff --git a/packages/server/src/lib/inngest.ts b/packages/server/src/lib/inngest.ts index b1a54fa..a290373 100644 --- a/packages/server/src/lib/inngest.ts +++ b/packages/server/src/lib/inngest.ts @@ -18,7 +18,7 @@ const escapeCSVValue = (value: unknown): string => { }; // Helper to validate schema name - prevents SQL injection -const validateSchemaName = (slug: string): string => { +export const validateSchemaName = (slug: string): string => { // Only allow lowercase alphanumeric and underscores if (!/^[a-z][a-z0-9_]*$/.test(slug)) { throw new Error(`Invalid project slug: ${slug}`); @@ -58,7 +58,16 @@ const insertWebhookDelivery = async ( AND event_type = $2 AND payload_hash = $4 AND payload = $3::jsonb`, - [webhookId, eventType, payloadJson, payloadHash, status, responseCode, responseBody, durationMs], + [ + webhookId, + eventType, + payloadJson, + payloadHash, + status, + responseCode, + responseBody, + durationMs, + ], ); await pool.query("COMMIT"); } catch (error) { @@ -149,15 +158,15 @@ export const deliverWebhook = inngest.createFunction( }, { event: "betterbase/webhook.deliver" }, async ({ event, step }) => { - const { - webhookId, - webhookName, - url, - secret: eventSecret, - eventType, - tableName, - payload, - } = event.data; + const { + webhookId, + webhookName, + url, + secret: eventSecret, + eventType, + tableName, + payload, + } = event.data; // Step 1: Resolve secret from database if not provided in event const resolvedSecret = await step.run("resolve-secret", async () => { @@ -177,18 +186,16 @@ export const deliverWebhook = inngest.createFunction( // Step 2: Send the HTTP request with timeout // step.run is a code-level transaction: retries automatically on throw, // runs only once on success, state persisted between retries. - const deliveryResult: - | { - httpStatus: number; - durationMs: number; - responseBody: string; - } - = await (async () => { - try { - return await step.run("send-http-request", async () => { - const body = JSON.stringify({ - id: crypto.randomUUID(), - webhook_id: webhookId, + const deliveryResult: { + httpStatus: number; + durationMs: number; + responseBody: string; + } = await (async () => { + try { + return await step.run("send-http-request", async () => { + const body = JSON.stringify({ + id: crypto.randomUUID(), + webhook_id: webhookId, table: tableName, type: eventType, record: payload, @@ -247,36 +254,36 @@ export const deliverWebhook = inngest.createFunction( } finally { clearTimeout(timeoutId); } - }); - } catch (err: any) { - await step.run("log-failed-delivery", async () => { - await insertWebhookDelivery( - webhookId, - eventType, - payload, - "failed", - null, - String(err?.message ?? err), - null, - ); - }); - throw err; - } - })(); + }); + } catch (err: any) { + await step.run("log-failed-delivery", async () => { + await insertWebhookDelivery( + webhookId, + eventType, + payload, + "failed", + null, + String(err?.message ?? err), + null, + ); + }); + throw err; + } + })(); // Step 2: Persist the delivery record with response_body // This step only runs after the HTTP request succeeds. - await step.run("log-successful-delivery", async () => { - await insertWebhookDelivery( - webhookId, - eventType, - payload, - "success", - deliveryResult.httpStatus, - deliveryResult.responseBody, - deliveryResult.durationMs, - ); - }); + await step.run("log-successful-delivery", async () => { + await insertWebhookDelivery( + webhookId, + eventType, + payload, + "success", + deliveryResult.httpStatus, + deliveryResult.responseBody, + deliveryResult.durationMs, + ); + }); return { success: true, diff --git a/packages/server/src/routes/admin/auth.ts b/packages/server/src/routes/admin/auth.ts index 3144a27..2df36b3 100644 --- a/packages/server/src/routes/admin/auth.ts +++ b/packages/server/src/routes/admin/auth.ts @@ -1,5 +1,6 @@ import { zValidator } from "@hono/zod-validator"; import { Hono } from "hono"; +import type { Context, MiddlewareHandler } from "hono"; import { z } from "zod"; import { extractBearerToken, @@ -9,6 +10,58 @@ import { } from "../../lib/auth"; import { getPool } from "../../lib/db"; +// ─── Simple in-memory rate limiter ───────────────────────────────────────── +const loginAttempts = new Map(); +const RATE_LIMIT_WINDOW_MS = 15 * 60 * 1000; // 15 minutes +const RATE_LIMIT_MAX_ATTEMPTS = 5; + +function resolveClientIP(c: Context): string { + const realIP = c.req.header("X-Real-IP"); + if (realIP) return realIP; + + const forwardedFor = c.req.header("X-Forwarded-For"); + if (forwardedFor) { + const parts = forwardedFor.split(","); + const firstIP = parts[0]?.trim(); + if (firstIP) return firstIP; + } + + return c.req.header("Host") ?? "unknown"; +} + +function cleanupExpiredEntries(): void { + const now = Date.now(); + for (const [ip, entry] of loginAttempts.entries()) { + if (entry.resetAt <= now) { + loginAttempts.delete(ip); + } + } +} + +function loginRateLimiter(c: Context): Response | null { + cleanupExpiredEntries(); + + const ip = resolveClientIP(c); + const now = Date.now(); + const entry = loginAttempts.get(ip); + + if (entry && entry.resetAt > now) { + if (entry.count >= RATE_LIMIT_MAX_ATTEMPTS) { + return c.json({ error: "Too many login attempts. Please try again in 15 minutes." }, 429); + } + entry.count++; + } else { + loginAttempts.set(ip, { count: 1, resetAt: now + RATE_LIMIT_WINDOW_MS }); + } + return null; +} + +function clearLoginAttempts(c: Context): void { + const ip = resolveClientIP(c); + loginAttempts.delete(ip); +} + +// ─── Auth Routes ─────────────────────────────────────────────────────────── export const authRoutes = new Hono(); // POST /admin/auth/login @@ -22,6 +75,10 @@ authRoutes.post( }), ), async (c) => { + // Rate limit check + const rateLimitResponse = loginRateLimiter(c); + if (rateLimitResponse) return rateLimitResponse; + const { email, password } = c.req.valid("json"); const pool = getPool(); @@ -39,11 +96,23 @@ authRoutes.post( return c.json({ error: "Invalid credentials" }, 401); } + clearLoginAttempts(c); + const token = await signAdminToken(admin.id); return c.json({ token, admin: { id: admin.id, email: admin.email } }); }, ); +// GET /admin/auth/setup/check — CSRF-exempt setup status check +authRoutes.get("/setup/check", async (c) => { + const pool = getPool(); + const { rows } = await pool.query( + "SELECT COUNT(*)::int as count FROM betterbase_meta.admin_users", + ); + const hasAdmins = rows[0].count > 0; + return c.json({ setupComplete: hasAdmins }); +}); + // GET /admin/auth/me (requires token) authRoutes.get("/me", async (c) => { const token = extractBearerToken(c.req.header("Authorization")); diff --git a/packages/server/src/routes/betterbase/index.ts b/packages/server/src/routes/betterbase/index.ts index cd16806..9c29c89 100644 --- a/packages/server/src/routes/betterbase/index.ts +++ b/packages/server/src/routes/betterbase/index.ts @@ -11,6 +11,7 @@ import { z } from "zod"; import { extractBearerToken, verifyAdminToken } from "../../lib/auth"; import { getPool } from "../../lib/db"; import { validateEnv } from "../../lib/env"; +import { validateSchemaName } from "../../lib/inngest"; // Import WS handler for stats import { getWSStats } from "./ws"; @@ -53,7 +54,7 @@ betterbaseRouter.post("/:kind/*", async (c) => { // Build DB context const pool = getPool(); const projectSlug = c.req.header("X-Project-Slug") ?? "default"; - const dbSchema = `project_${projectSlug}`; + const dbSchema = validateSchemaName(projectSlug); try { let result: unknown; @@ -93,12 +94,17 @@ betterbaseRouter.post("/:kind/*", async (c) => { // Storage context builder function buildStorageCtx(pool: any, projectSlug: string): StorageCtx { const env = validateEnv(); + + if (!env.STORAGE_ACCESS_KEY || !env.STORAGE_SECRET_KEY) { + throw new Error("Storage credentials not configured"); + } + return new StorageCtx({ pool, projectSlug, endpoint: env.STORAGE_ENDPOINT ?? "http://minio:9000", - accessKey: env.STORAGE_ACCESS_KEY ?? "minioadmin", - secretKey: env.STORAGE_SECRET_KEY ?? "minioadmin", + accessKey: env.STORAGE_ACCESS_KEY, + secretKey: env.STORAGE_SECRET_KEY, bucket: env.STORAGE_BUCKET ?? "betterbase", publicBase: env.STORAGE_PUBLIC_BASE, }); @@ -181,15 +187,40 @@ betterbaseRouter.post("/storage/generate-upload-url", async (c) => { const projectSlug = c.req.header("X-Project-Slug") ?? "default"; const storageId = `st_${nanoid(20)}`; const ext = filename?.split(".").pop() ?? ""; - const s3Key = `project_${projectSlug}/${storageId}${ext ? "." + ext : ""}`; + + // Validate filename - prevent path traversal + if (filename && !/^[a-zA-Z0-9_.-]+$/.test(filename)) { + return c.json( + { error: "Invalid filename: only alphanumeric, dash, underscore, and dot allowed" }, + 400, + ); + } + + // Validate extension + if (ext && !/^[a-zA-Z0-9]{1,10}$/.test(ext)) { + return c.json({ error: "Invalid file extension" }, 400); + } + + // Validate contentType if provided + if (contentType && !/^[a-zA-Z0-9.-]+\/[a-zA-Z0-9.-]+$/.test(contentType)) { + return c.json({ error: "Invalid content type" }, 400); + } + const env = validateEnv(); + // Validate credentials are set - fail fast instead of using defaults + if (!env.STORAGE_ACCESS_KEY || !env.STORAGE_SECRET_KEY) { + return c.json({ error: "Storage credentials not configured" }, 500); + } + + const s3Key = `project_${projectSlug}/${storageId}${ext ? "." + ext : ""}`; + const s3 = new S3Client({ endpoint: env.STORAGE_ENDPOINT ?? "http://minio:9000", region: "us-east-1", credentials: { - accessKeyId: env.STORAGE_ACCESS_KEY ?? "minioadmin", - secretAccessKey: env.STORAGE_SECRET_KEY ?? "minioadmin", + accessKeyId: env.STORAGE_ACCESS_KEY, + secretAccessKey: env.STORAGE_SECRET_KEY, }, forcePathStyle: true, }); @@ -206,8 +237,9 @@ betterbaseRouter.post("/storage/generate-upload-url", async (c) => { // Record the pending upload in the DB so getUrl() works after upload const pool = getPool(); + const validatedSchema = validateSchemaName(projectSlug); await pool.query( - `INSERT INTO "project_${projectSlug}"._iac_storage + `INSERT INTO "${validatedSchema}"._iac_storage (storage_id, s3_key, bucket, content_type) VALUES ($1, $2, $3, $4) ON CONFLICT (storage_id) DO NOTHING`, [ diff --git a/packages/server/src/routes/betterbase/ws.ts b/packages/server/src/routes/betterbase/ws.ts index 7b93906..193475e 100644 --- a/packages/server/src/routes/betterbase/ws.ts +++ b/packages/server/src/routes/betterbase/ws.ts @@ -5,6 +5,7 @@ import { subscriptionTracker, } from "@betterbase/core"; import { nanoid } from "nanoid"; +import { extractBearerToken, verifyAdminToken } from "../../lib/auth"; const HEARTBEAT_INTERVAL_MS = 15_000; // ping every 15s const HEARTBEAT_TIMEOUT_MS = 30_000; // disconnect after 30s without pong @@ -137,9 +138,33 @@ export function getWSStats() { /** Mount in Bun.serve() options */ export function getBunServeConfig() { return { - fetch(req: Request, server: any) { + fetch: async (req: Request, server: any) => { const url = new URL(req.url); if (url.pathname === "/betterbase/ws") { + let token: string | null = null; + + const authHeader = req.headers.get("Authorization"); + token = extractBearerToken(authHeader ?? ""); + + if (!token) { + token = url.searchParams.get("token"); + } + + if (!token) { + return new Response(JSON.stringify({ error: "Authentication required" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + + const payload = await verifyAdminToken(token); + if (!payload) { + return new Response(JSON.stringify({ error: "Invalid or expired token" }), { + status: 401, + headers: { "Content-Type": "application/json" }, + }); + } + const projectSlug = url.searchParams.get("project") ?? "default"; const upgraded = server.upgrade(req, { data: { projectSlug } }); if (upgraded) return undefined; diff --git a/03_test_suite.md b/specs/03_test_suite.md similarity index 100% rename from 03_test_suite.md rename to specs/03_test_suite.md diff --git a/BETTERBASE.md b/specs/BETTERBASE.md similarity index 100% rename from BETTERBASE.md rename to specs/BETTERBASE.md diff --git a/BetterBase_Competitive_Plan.md b/specs/BetterBase_Competitive_Plan.md similarity index 100% rename from BetterBase_Competitive_Plan.md rename to specs/BetterBase_Competitive_Plan.md diff --git a/BetterBase_Dashboard_Backend_Spec.md b/specs/BetterBase_Dashboard_Backend_Spec.md similarity index 100% rename from BetterBase_Dashboard_Backend_Spec.md rename to specs/BetterBase_Dashboard_Backend_Spec.md diff --git a/BetterBase_Dashboard_Frontend_Spec.md b/specs/BetterBase_Dashboard_Frontend_Spec.md similarity index 100% rename from BetterBase_Dashboard_Frontend_Spec.md rename to specs/BetterBase_Dashboard_Frontend_Spec.md diff --git a/BetterBase_IaC_Phase2_Spec.md b/specs/BetterBase_IaC_Phase2_Spec.md similarity index 100% rename from BetterBase_IaC_Phase2_Spec.md rename to specs/BetterBase_IaC_Phase2_Spec.md diff --git a/BetterBase_IaC_Phase3_Spec.md b/specs/BetterBase_IaC_Phase3_Spec.md similarity index 100% rename from BetterBase_IaC_Phase3_Spec.md rename to specs/BetterBase_IaC_Phase3_Spec.md diff --git a/BetterBase_InfraAsCode_Spec.md b/specs/BetterBase_InfraAsCode_Spec.md similarity index 100% rename from BetterBase_InfraAsCode_Spec.md rename to specs/BetterBase_InfraAsCode_Spec.md diff --git a/BetterBase_Inngest_Dashboard_Spec.md b/specs/BetterBase_Inngest_Dashboard_Spec.md similarity index 100% rename from BetterBase_Inngest_Dashboard_Spec.md rename to specs/BetterBase_Inngest_Dashboard_Spec.md diff --git a/BetterBase_Inngest_Spec.md b/specs/BetterBase_Inngest_Spec.md similarity index 100% rename from BetterBase_Inngest_Spec.md rename to specs/BetterBase_Inngest_Spec.md diff --git a/BetterBase_Observability_Spec.docx.md b/specs/BetterBase_Observability_Spec.docx.md similarity index 100% rename from BetterBase_Observability_Spec.docx.md rename to specs/BetterBase_Observability_Spec.docx.md diff --git a/BetterBase_SelfHosted_Spec.md b/specs/BetterBase_SelfHosted_Spec.md similarity index 100% rename from BetterBase_SelfHosted_Spec.md rename to specs/BetterBase_SelfHosted_Spec.md diff --git a/specs/CODEBASE_MAP.md b/specs/CODEBASE_MAP.md new file mode 100644 index 0000000..c981e62 --- /dev/null +++ b/specs/CODEBASE_MAP.md @@ -0,0 +1,2218 @@ +# BetterBase — Codebase Map + +> Last updated: 2026-03-30 + +## What is BetterBase? + +AI-native Backend-as-a-Service platform built with Bun. Define your backend in TypeScript using the Convex-inspired IaC layer, or use traditional Drizzle + Hono patterns. + +--- + +## Quick Start + +```bash +bun install -g @betterbase/cli +bb init my-app +cd my-app +bun install +bb dev +``` + +--- + +## Project Structure + +### IaC Pattern (Recommended) + +``` +my-app/ +├── betterbase/ +│ ├── schema.ts # defineSchema() + defineTable() +│ ├── queries/ # query() functions (auto-realtime) +│ ├── mutations/ # mutation() functions (transactions) +│ ├── actions/ # action() functions (side-effects) +│ └── cron.ts # scheduled functions +├── betterbase.config.ts # Optional config +└── package.json +``` + +### Original Pattern (Advanced) + +``` +my-app/ +├── src/ +│ ├── db/schema.ts # Drizzle schema +│ ├── routes/ # Hono routes +│ └── functions/ # Serverless functions +└── package.json +``` + +Both patterns work together. + +--- + +## Package Architecture + +| Package | Purpose | +|---------|---------| +| `@betterbase/cli` | CLI tool (`bb` command) | +| `@betterbase/client` | TypeScript SDK for frontend | +| `@betterbase/core` | Core backend engine | +| `@betterbase/shared` | Shared utilities | +| `@betterbase/server` | Self-hosted admin API | +| `apps/dashboard` | Admin dashboard (React) | + +--- + +## Core IaC Modules + +### validators.ts +`v.string()`, `v.number()`, `v.id()`, etc. — Zod-backed validators + +### schema.ts +`defineSchema()`, `defineTable()` — schema definition with index builders + +### functions.ts +`query()`, `mutation()`, `action()` — function primitives with context types + +### db-context.ts +`DatabaseReader`, `DatabaseWriter` — typed DB access layer + +### function-registry.ts +Scans `betterbase/` directory, registers functions + +--- + +## CLI Commands + +| Command | Description | +|---------|-------------| +| `bb init` | Create new project | +| `bb dev` | Start dev server | +| `bb iac sync` | Sync schema to DB | +| `bb iac analyze` | Query diagnostics | +| `bb migrate` | Run migrations | +| `bb generate` | Generate types | + +--- + +## IaC Modules (`packages/core/src/iac/`) + +| File | Purpose | +|------|---------| +| `validators.ts` | `v.string()`, `v.number()`, `v.id()`, etc. — Zod-backed | +| `schema.ts` | `defineSchema()`, `defineTable()` with index builders | +| `functions.ts` | `query()`, `mutation()`, `action()` primitives | +| `db-context.ts` | `DatabaseReader`, `DatabaseWriter` | +| `function-registry.ts` | Scans `betterbase/`, registers functions | +| `schema-serializer.ts` | Serialize schema to JSON | +| `schema-diff.ts` | Diff two schemas, detect changes | +| `generators/drizzle-schema-gen.ts` | Generate Drizzle schema | +| `generators/migration-gen.ts` | Generate SQL migrations | +| `generators/api-typegen.ts` | Generate TypeScript types | +| `errors.ts` | Error classes with suggestions | +| `cron.ts` | `cron()` scheduled functions | + +--- + +## Dashboard (`apps/dashboard/`) + +React admin dashboard for self-hosted management. + +### Pages + +| Page | Route | Description | +|------|-------|-------------| +| Overview | `/` | Metrics, charts, activity | +| Setup | `/setup` | Initial setup | +| Login | `/login` | Authentication | +| NotFound | `/*` | 404 page | +| Projects | `/projects` | List all projects | +| Project Detail | `/projects/:id` | Project settings | +| Project Functions | `/projects/:id/functions` | Serverless functions | +| Project IaC Functions | `/projects/:id/iac/functions` | IaC function management | +| Project IaC Schema | `/projects/:id/iac/schema` | IaC schema view | +| Project IaC Jobs | `/projects/:id/iac/jobs` | IaC job history | +| Project IaC Query | `/projects/:id/iac/query` | IaC query diagnostics | +| Project IaC Realtime | `/projects/:id/iac/realtime` | IaC realtime settings | +| Project Database | `/projects/:id/database` | Database management | +| Project Auth | `/projects/:id/auth` | Auth configuration | +| Project Env | `/projects/:id/env` | Environment variables | +| Project Webhooks | `/projects/:id/webhooks` | Project webhooks | +| Project Users | `/projects/:id/users` | Project user management | +| Project User | `/projects/:id/users/:userId` | User detail page | +| Project Realtime | `/projects/:id/realtime` | Realtime settings | +| Project Observability | `/projects/:id/observability` | Observability metrics | +| Storage | `/storage` | Storage buckets | +| Storage Bucket | `/storage/:bucketId` | Bucket details | +| Logs | `/logs` | Request logs | +| Function Invocations | `/functions` | Function invocation logs | +| Webhook Deliveries | `/webhook-deliveries` | Webhook delivery logs | +| Team | `/team` | Team management | +| Audit | `/audit` | Audit log | +| Observability | `/observability` | System observability | +| Settings | `/settings` | Instance settings | +| Settings SMTP | `/settings/smtp` | SMTP configuration | +| Settings API Keys | `/settings/api-keys` | API key management | +| Settings Notifications | `/settings/notifications` | Notification settings | +| Settings Inngest | `/settings/inngest` | Inngest dashboard | + +### UI Components + +| Component | Description | +|-----------|-------------| +| label | Label component | +| tooltip | Tooltip component | +| scroll-area | Scroll area wrapper | +| separator | Vertical/horizontal separator | +| popover | Popover component | +| switch | Toggle switch | +| textarea | Text area input | +| progress | Progress bar | +| skeleton | Loading skeleton | +| sheet | Slide-out panel | +| badge | Status badge | +| collapsible | Collapsible content | +| Avatar | User avatar | +| ConfirmDialog | Confirmation dialog | +| LiveLogStream | Live log streaming | +| CommandPalette | Command palette (Ctrl+K) | +| ErrorBoundary | Error boundary wrapper | + +### Hooks + +| Hook | Description | +|------|-------------| +| useLogStream | Live log streaming hook | +| useGlobalMetrics | Global metrics hook | +| useTheme | Theme management hook | + +### Libs + +| File | Description | +|------|-------------| +| query-keys.ts | TanStack Query keys | +| inngest-client.ts | Inngest client | +| api.ts | API client | +| utils.ts | Utility functions | + +### Layouts + +| Layout | Description | +|--------|-------------| +| AppLayout | Main application layout | + +### Tech Stack + +- React Router v7 +- TanStack Query v5 +- Tailwind CSS v4 +- shadcn/ui components +- Recharts for charts + +### Architecture Flow Diagram + +``` +┌────────────────────────────────────────────────────────────────────────────────┐ +│ CLIENT LAYER │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ Web SDK │ │ React Hooks │ │ Mobile │ │ GraphQL │ │ +│ │@betterbase │ │ @betterbase │ │ SDK │ │ Client │ │ +│ │ /client │ │ /client │ │ │ │ │ │ +│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ +└─────────┼──────────────────┼──────────────────┼──────────────────┼──────────┘ + │ │ │ │ + ▼ ▼ ▼ ▼ +┌────────────────────────────────────────────────────────────────────────────────┐ +│ API GATEWAY (Hono) │ +│ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ REST API │ │ GraphQL │ │ Auth │ │ Storage │ │ Realtime │ │ Webhooks │ │ +│ │/api/v1/* │ │ /graphql│ │/api/auth│ │/storage │ │/realtime │ │ │ │ +│ └────┬─────┘ └────┬─────┘ └────┬───┘ └────┬────┘ └────┬─────┘ └────┬─────┘ │ +└───────┼────────────┼────────────┼──────────┼────────────┼────────────┼────────┘ + │ │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ ▼ +┌────────────────────────────────────────────────────────────────────────────────┐ +│ CORE SERVICES LAYER │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ Query │ │ Auth │ │ Realtime │ │ Storage │ │ Function │ │ Webhook │ │ +│ │ Engine │ │ Service │ │ Service │ │ Service │ │ Runtime │ │ Dispatch │ │ +│ │(Drizzle) │ │(Better │ │(WebSocket│ │ (S3) │ │ (Bun) │ │ │ │ +│ │ │ │ Auth) │ │) │ │ │ │ │ │ │ │ +│ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘ │ +│ │ │ │ │ │ │ │ +│ └────────────┴────────────┴────────────┴────────────┴────────────┘ │ +│ │ │ +└─────────────────────────────────────┼────────────────────────────────────────────┘ + ▼ +┌────────────────────────────────────────────────────────────────────────────────┐ +│ DATA LAYER │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ SQLite │ │PostgreSQL│ │ MySQL │ │ Neon │ │ Turso │ │ Supabase │ │ +│ │ (dev) │ │ │ │ │ │(serverless│ │ (libSQL) │ │ │ │ +│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ +│ │ +│ ┌──────────────────────────────────────────────────────────────────────────┐ │ +│ │ STORAGE LAYER │ │ +│ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ +│ │ │ AWS S3 │ │Cloudflare│ │Backblaze │ │ MinIO │ │ Local │ │ │ +│ │ │ │ │ R2 │ │ B2 │ │ │ │ Disk │ │ │ +│ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │ │ +│ └──────────────────────────────────────────────────────────────────────────┘ │ +└────────────────────────────────────────────────────────────────────────────────┘ +``` + +### Self-Hosted Deployment Architecture + +``` +┌────────────────────────────────────────────────────────────────────────────────┐ +│ SELF-HOSTED DEPLOYMENT │ +├────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ External Clients │ │ +│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ +│ │ │ Web App │ │ CLI (bb) │ │ Mobile │ │ Dashboard │ │ │ +│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ +│ └─────────┼────────────────┼────────────────┼────────────────┼──────────┘ │ +│ │ │ │ │ │ +│ ▼ ▼ ▼ ▼ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ NGINX Reverse Proxy │ │ +│ │ (docker/nginx/nginx.conf) │ │ +│ └────────────────────────────────┬────────────────────────────────────────┘ │ +│ │ │ +│ ┌───────────────────────┼───────────────────────┐ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ +│ │ Dashboard │ │ Server │ │ Inngest │ │ +│ │ (React App) │ │ (@betterbase │ │ (Workflow │ │ +│ │ Behind nginx │ │ /server) │ │ Engine) │ │ +│ │ (not direct) │ │ Port: 3001 │ │ Port: 8288 │ │ +│ └─────────────────┘ └────────┬────────┘ └────────┬────────┘ │ +│ │ │ │ +│ └───────────┬───────────┘ │ +│ │ │ +│ ▼ │ +│ ┌─────────────────────┐ │ +│ │ PostgreSQL │ │ +│ │ (Database) │ │ +│ └─────────────────────┘ │ +└────────────────────────────────────────────────────────────────────────────────┘ +``` + +``` +betterbase/ +├── package.json # Root workspace config (name: "betterbase") +├── turbo.json # Turborepo task configuration +├── tsconfig.base.json # Shared TypeScript config (ES2022, strict) +├── bun.lock # Bun lockfile +├── CODEBASE_MAP.md # This file +├── README.md # Project documentation +├── .gitignore # Git ignore patterns +├── .npmignore # npm ignore patterns +│ +├── packages/ +│ ├── cli/ # @betterbase/cli - CLI tool (bb command) +│ │ ├── package.json +│ │ ├── tsconfig.json +│ │ ├── src/ +│ │ │ ├── index.ts # Main CLI entry point +│ │ │ ├── build.ts # Build script +│ │ │ ├── constants.ts # Shared constants +│ │ │ ├── commands/ # CLI commands (20+ files) +│ │ │ │ ├── auth.ts # bb auth setup - BetterAuth integration +│ │ │ │ ├── auth-providers.ts # bb auth add-provider - OAuth provider management +│ │ │ │ ├── dev.ts # bb dev - Development server with watch +│ │ │ │ ├── function.ts # bb function - Edge function management +│ │ │ │ ├── generate.ts # bb generate crud - CRUD route generation +│ │ │ │ ├── graphql.ts # bb graphql - GraphQL management +│ │ │ │ ├── init.ts # bb init - Project initialization +│ │ │ │ ├── login.ts # bb login - Cloud authentication +│ │ │ │ ├── migrate.ts # bb migrate - Database migrations +│ │ │ │ ├── migrate-utils.ts # Migration utilities +│ │ │ │ ├── rls.ts # bb rls - Row Level Security management +│ │ │ │ ├── rls-test.ts # bb rls test - RLS policy testing +│ │ │ │ ├── storage.ts # bb storage - Storage bucket management +│ │ │ │ ├── webhook.ts # bb webhook - Webhook management +│ │ │ │ ├── branch.ts # bb branch - Branch management +│ │ │ │ ├── iac/ # IaC commands (NEW in Phase 3) +│ │ │ │ │ ├── analyze.ts # bb iac analyze - Query diagnostics +│ │ │ │ │ ├── export.ts # bb iac export - Data export +│ │ │ │ │ ├── import.ts # bb iac import - Data import +│ │ │ │ │ ├── generate.ts # bb iac generate - Function code gen +│ │ │ │ │ └── sync.ts # bb iac sync - Schema sync +│ │ │ │ ├── migrate/ # Migration tools +│ │ │ │ │ └── from-convex.ts # bb migrate from-convex +│ │ │ │ └── dev/ # Dev mode utilities +│ │ │ │ ├── process-manager.ts # Server process management +│ │ │ │ ├── watcher.ts # File watcher for hot reload +│ │ │ │ ├── error-formatter.ts # Error formatting +│ │ │ │ └── query-log.ts # Query logging (NEW) +│ │ │ └── utils/ # CLI utilities (9 files) +│ │ │ │ ├── api-client.ts # API client for cloud operations +│ │ │ │ ├── context-generator.ts # Generates .betterbase-context.json +│ │ │ │ ├── credentials.ts # Credentials management +│ │ │ │ ├── logger.ts # Colored console logging +│ │ │ │ ├── prompts.ts # Interactive prompts (Inquirer) +│ │ │ │ ├── provider-prompts.ts # Database provider selection +│ │ │ │ ├── route-scanner.ts # Hono route scanning +│ │ │ │ ├── schema-scanner.ts # Drizzle schema scanning +│ │ │ │ └── scanner.ts # Schema scanner core +│ │ │ │ │ └── spinner.ts # Spinner utilities +│ │ └── test/ # CLI tests (30+ test files) +│ │ +│ ├── client/ # @betterbase/client - TypeScript SDK +│ │ ├── package.json +│ │ ├── tsconfig.json +│ │ ├── tsconfig.test.json +│ │ ├── README.md +│ │ ├── src/ # Client SDK source +│ │ │ ├── index.ts # Package exports +│ │ │ ├── auth.ts # Authentication client +│ │ │ ├── build.ts # Build configuration +│ │ │ ├── client.ts # Main client factory +│ │ │ ├── errors.ts # Client error classes +│ │ │ ├── query-builder.ts # Chainable query builder +│ │ │ ├── realtime.ts # Realtime subscription client +│ │ │ ├── storage.ts # Storage client +│ │ │ ├── types.ts # TypeScript definitions +│ │ │ └── iac/ # IaC client (Phase 3) +│ │ │ ├── index.ts # IaC exports +│ │ │ ├── hooks.ts # React hooks (useQuery, useMutation, useAction) +│ │ │ ├── vanilla.ts # Non-React client +│ │ │ ├── paginated-query.ts # Paginated query support +│ │ │ └── embeddings.ts # Embedding utilities +│ │ └── test/ # Client tests (10+ test files) +│ │ +│ ├── core/ # @betterbase/core - Core backend engine +│ │ ├── package.json +│ │ ├── README.md +│ │ ├── tsconfig.json +│ │ └── src/ +│ │ ├── index.ts # Core exports +│ │ ├── auto-rest.ts # Auto-REST: Automatic CRUD route generation +│ │ ├── config/ # Configuration modules +│ │ │ ├── index.ts # Config exports +│ │ │ ├── schema.ts # Project config schema (Zod) +│ │ │ └── drizzle-generator.ts # Drizzle config generator +│ │ ├── functions/ # Serverless functions +│ │ │ ├── index.ts # Functions exports +│ │ │ ├── bundler.ts # Function bundler (esbuild) +│ │ │ ├── deployer.ts # Function deployer +│ │ │ └── local-runtime.ts # Local functions runtime +│ │ ├── graphql/ # GraphQL server +│ │ │ ├── index.ts # GraphQL exports +│ │ │ ├── resolvers.ts # GraphQL resolvers +│ │ │ ├── schema-generator.ts # Schema from DB +│ │ │ ├── sdl-exporter.ts # SDL export +│ │ │ ├── server.ts # GraphQL HTTP server +│ │ │ └── realtime-bridge.ts # GraphQL subscriptions bridge +│ │ ├── middleware/ # Middleware +│ │ │ ├── index.ts # Middleware exports +│ │ │ ├── rls-session.ts # RLS session middleware +│ │ │ └── request-logger.ts # Request logging middleware +│ │ ├── migration/ # Database migrations +│ │ │ ├── index.ts # Migration exports +│ │ │ └── rls-migrator.ts # RLS policy migration +│ │ ├── providers/ # Database providers +│ │ │ ├── index.ts # Provider exports +│ │ │ ├── types.ts # Provider interfaces +│ │ │ ├── neon.ts # Neon serverless PostgreSQL +│ │ │ ├── planetscale.ts # PlanetScale MySQL +│ │ │ ├── postgres.ts # PostgreSQL +│ │ │ ├── supabase.ts # Supabase-compatible +│ │ │ └── turso.ts # Turso libSQL +│ │ ├── rls/ # Row Level Security +│ │ │ ├── index.ts # RLS exports +│ │ │ ├── types.ts # RLS type definitions +│ │ │ ├── scanner.ts # RLS policy scanner +│ │ │ ├── generator.ts # RLS policy generator +│ │ │ ├── evaluator.ts # RLS policy evaluator (SQLite) +│ │ │ └── auth-bridge.ts # Auth-RLS bridge +│ │ ├── storage/ # Storage adapter +│ │ │ ├── index.ts # Storage exports +│ │ │ ├── types.ts # Storage types +│ │ │ ├── s3-adapter.ts # S3-compatible adapter +│ │ │ ├── image-transformer.ts # Image transformations (Sharp) +│ │ │ └── policy-engine.ts # Storage policy engine +│ │ ├── webhooks/ # Webhook handling +│ │ │ ├── index.ts # Webhook exports +│ │ │ ├── types.ts # Webhook types +│ │ │ ├── dispatcher.ts # Event dispatcher +│ │ │ ├── integrator.ts # DB trigger integration +│ │ │ ├── signer.ts # Payload signing +│ │ │ ├── startup.ts # Server initialization +│ │ │ └── schema.sql # Webhook schema +│ │ ├── vector/ # Vector search (pgvector) +│ │ │ ├── index.ts # Vector exports +│ │ │ ├── types.ts # Vector column types +│ │ │ ├── embeddings.ts # Embedding providers (OpenAI, Cohere) +│ │ │ └── search.ts # Vector similarity search +│ │ ├── branching/ # Preview environments +│ │ │ ├── index.ts # Branching exports +│ │ │ ├── types.ts # Branch types +│ │ │ ├── database.ts # Database branching +│ │ │ └── storage.ts # Storage branching +│ │ ├── logger/ # Logging +│ │ │ ├── index.ts # Logger exports +│ │ │ └── file-transport.ts # File transport +│ │ └── realtime/ # Realtime subscriptions +│ │ ├── index.ts # Realtime exports +│ │ └── channel-manager.ts # Channel manager +│ │ +│ ├── shared/ # @betterbase/shared - Shared utilities +│ │ ├── package.json +│ │ ├── tsconfig.json +│ │ └── src/ +│ │ ├── index.ts # Main exports +│ │ ├── types.ts # Shared types +│ │ ├── errors.ts # Shared error classes +│ │ ├── constants.ts # Shared constants +│ │ └── utils.ts # Utility functions +│ │ +│ ├── server/ # @betterbase/server - Self-hosted server +│ │ ├── package.json +│ │ ├── tsconfig.json +│ │ ├── Dockerfile +│ │ ├── migrations/ # Database migrations +│ │ │ ├── 001_initial_schema.sql +│ │ │ ├── 002_admin_users.sql +│ │ │ ├── 003_projects.sql +│ │ │ ├── 004_logs.sql +│ │ │ └── 014_inngest_support.sql +│ │ └── src/ +│ │ ├── index.ts # Server entry point +│ │ ├── types.d.ts # TypeScript declarations +│ │ ├── lib/ +│ │ │ ├── db.ts # Database connection +│ │ │ ├── migrate.ts # Migration runner +│ │ │ ├── env.ts # Environment validation +│ │ │ ├── auth.ts # Auth utilities +│ │ │ ├── admin-middleware.ts # Admin auth middleware +│ │ │ ├── inngest.ts # Inngest client & functions +│ │ │ ├── webhook-dispatcher.ts # Webhook event dispatcher +│ │ │ ├── webhook-logger.ts # Webhook delivery logging +│ │ │ ├── function-logger.ts # Function invocation logging +│ │ │ └── audit.ts # Audit logging +│ │ └── routes/ +│ │ ├── admin/ # Admin API routes +│ │ │ ├── index.ts +│ │ │ ├── auth.ts +│ │ │ ├── projects.ts +│ │ │ ├── users.ts +│ │ │ ├── metrics.ts +│ │ │ ├── metrics-enhanced.ts # Enhanced metrics +│ │ │ ├── storage.ts +│ │ │ ├── webhooks.ts +│ │ │ ├── functions.ts +│ │ │ ├── logs.ts +│ │ │ ├── audit.ts +│ │ │ ├── roles.ts # Role management +│ │ │ ├── notifications.ts # Notification system +│ │ │ ├── smtp.ts # SMTP configuration +│ │ │ ├── api-keys.ts # API key management +│ │ │ ├── cli-sessions.ts # CLI session management +│ │ │ ├── inngest.ts # Inngest integration +│ │ │ ├── instance.ts # Instance settings +│ │ │ └── project-scoped/ # Project-specific routes +│ │ │ ├── index.ts +│ │ │ ├── database.ts # Database management +│ │ │ ├── functions.ts # Function management +│ │ │ ├── users.ts # Project users +│ │ │ ├── env.ts # Environment variables +│ │ │ ├── auth-config.ts # Auth configuration +│ │ │ ├── webhooks.ts # Project webhooks +│ │ │ ├── realtime.ts # Realtime settings +│ │ │ └── iac.ts # IaC management +│ │ ├── betterbase/ # BetterBase IaC API routes +│ │ │ ├── index.ts # Main IaC API handler +│ │ │ └── ws.ts # WebSocket handler +│ │ └── device/ # Device auth routes +│ │ └── index.ts +│ │ +├── apps/ +│ ├── dashboard/ # Admin dashboard for self-hosted +│ │ ├── Dockerfile +│ └── test-project/ # Example/test project +│ ├── betterbase.config.ts # Project configuration +│ ├── drizzle.config.ts # Drizzle configuration +│ ├── package.json +│ ├── tsconfig.json +│ ├── README.md +│ ├── src/ +│ │ ├── index.ts # App entry point (Hono server) +│ │ ├── auth/ +│ │ │ ├── index.ts # Auth module +│ │ │ └── types.ts # Auth types +│ │ ├── db/ +│ │ │ ├── index.ts # Database setup +│ │ │ ├── migrate.ts # Migration runner +│ │ │ ├── schema.ts # Database schema +│ │ │ └── policies/ # RLS policies +│ │ │ └── .gitkeep +│ │ ├── functions/ # Serverless functions +│ │ │ └── hello/ # Example function +│ │ │ └── index.ts +│ │ ├── lib/ +│ │ │ ├── env.ts # Environment vars +│ │ │ └── realtime.ts # Realtime events +│ │ ├── middleware/ +│ │ │ ├── auth.ts # Auth middleware +│ │ │ └── validation.ts # Validation middleware +│ │ └── routes/ +│ │ ├── index.ts # Routes registration +│ │ ├── health.ts # Health check +│ │ ├── storage.ts # Storage routes +│ │ ├── users.ts # User CRUD routes +│ │ ├── webhooks.ts # Webhook routes +│ │ └── graphql.d.ts # GraphQL types +│ └── test/ # Project tests +│ ├── crud.test.ts +│ └── health.test.ts +│ +├── templates/ +│ ├── base/ # Base project template +│ │ ├── src/ +│ │ │ ├── index.ts # Main entry point +│ │ │ ├── routes/ # API routes +│ │ │ ├── functions/ # Edge functions +│ │ │ ├── auth/ # Authentication +│ │ │ ├── middleware/ # Middleware +│ │ │ ├── lib/ # Utilities +│ │ │ └── db/ # Database (schema, migrations) +│ │ ├── test/ +│ │ ├── betterbase.config.ts +│ │ ├── drizzle.config.ts +│ │ ├── package.json +│ │ └── README.md +│ │ +│ └── auth/ # Auth project template +│ ├── src/ +│ │ ├── routes/ +│ │ │ ├── auth.ts # Auth routes +│ │ │ └── auth-example.ts +│ │ ├── auth/ +│ │ │ ├── index.ts +│ │ │ └── types.ts +│ │ ├── db/ +│ │ │ ├── schema.ts +│ │ │ ├── auth-schema.ts +│ │ │ └── index.ts +│ │ └── middleware/ +│ │ └── auth.ts +│ ├── README.md +│ └── (template files) +│ +├── cli-auth-page/ # Standalone auth page for CLI (not a template) +│ ├── .gitignore +│ └── index.html # Auth UI entry +│ +├── new-features-docs/ # Documentation for new features +│ └── README_START_HERE.md +│ +└── scripts/ # Build/release scripts +``` + +--- + +## Docker Deployment + +Betterbase includes production-ready Docker configuration for self-hosted deployment. + +### Docker Files + +| File | Purpose | +|------|---------| +| `Dockerfile` | Monorepo build (for developing Betterbase itself) | +| `Dockerfile.project` | Project template for deploying user projects | +| `docker-compose.yml` | Development environment with PostgreSQL | +| `docker-compose.dev.yml` | Inngest dev server for local development | +| `docker-compose.production.yml` | Production-ready configuration | +| `docker-compose.self-hosted.yml` | Self-hosted deployment with dashboard | +| `docker/nginx/nginx.conf` | Nginx reverse proxy configuration | +| `.dockerignore` | Optimizes Docker builds | +| `.env.example` | Environment variable template | + +### Quick Start + +```bash +# Development with Docker Compose +docker-compose up -d + +# Production deployment +docker-compose -f docker-compose.production.yml up -d +``` + +### Docker Features + +- **Multi-stage builds** for minimal image size +- **PostgreSQL** included in dev environment +- **Inngest** for durable workflows and background jobs +- **Health checks** for reliability +- **Non-root user** for security +- **Volume mounts** for hot-reload in development +- **External database support** - Neon, Supabase, RDS, etc. +- **S3-compatible storage** - R2, S3, B2, MinIO + +--- + +## Inngest Integration + +Betterbase uses [Inngest](https://www.inngest.com/) for durable workflows and background jobs. + +### Deployment Modes + +| Mode | Inngest Backend | Used By | +|------|----------------|---------| +| Cloud | `https://api.inngest.com` | BetterBase Cloud | +| Self-Hosted | `http://inngest:8288` | Docker deployment | +| Local Dev | `http://localhost:8288` | Development | + +### Inngest Functions + +| Function | Trigger | Description | +|----------|---------|-------------| +| `deliverWebhook` | Event | Retryable webhook delivery with auto-backoff | +| `evaluateNotificationRule` | Event | Email/webhook notifications on threshold breach | +| `exportProjectUsers` | Event | Background CSV export | +| `pollNotificationRules` | Cron `*/5 * * * *` | 5-minute metric polling | + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `INNGEST_BASE_URL` | Inngest backend URL | +| `INNGEST_SIGNING_KEY` | Verifies Inngest→Server callbacks | +| `INNGEST_EVENT_KEY` | Authenticates Server→Inngest events | + +--- + +## Root-Level Files + +### [`package.json`](package.json) +**Purpose:** Root workspace configuration for Turborepo monorepo. +- **Key Fields:** `name: "betterbase"`, workspaces: `["packages/*", "apps/*"]` +- **Scripts:** Build, test, and dev scripts using turbo +- **Dependencies:** `turbo@^2.3.0`, `bun` (package manager) + +### [`turbo.json`](turbo.json) +**Purpose:** Turborepo task configuration defining build pipelines. +- **Tasks:** `build`, `test`, `lint` with cache settings +- **Dependencies:** Build depends on ^build, test depends on ^test +- **Cache:** Remote caching enabled for CI/CD + +### [`tsconfig.base.json`](tsconfig.base.json) +**Purpose:** Shared TypeScript configuration for all packages. +- **Target:** ES2022 +- **Module:** NodeNext +- **Strict:** Enabled +- **Module Resolution:** NodeNext + +--- + +## Documentation (docs/) + +Comprehensive documentation for BetterBase. + +| Directory | Content | +|-----------|---------| +| `docs/getting-started/` | Installation, quick start, your first project | +| `docs/features/` | Authentication, database, storage, realtime, webhooks, functions, RLS, GraphQL | +| `docs/core/` | Core modules documentation (overview, config, providers, middleware, etc.) | +| `docs/client/` | Client SDK documentation | +| `docs/guides/` | Deployment, scaling, monitoring, production checklist | +| `docs/iac/` | Infrastructure as Code documentation (schema, functions, client hooks, etc.) | +| `docs/api-reference/` | REST API, GraphQL API, CLI commands, Client SDK reference | +| `docs/templates/` | Template documentation | +| `docs/examples/` | Example projects (ecommerce, blog, chat-app, todo-app) | + +--- + +## packages/core + +`@betterbase/core` - Core backend engine with all server-side functionality. + +### Core Modules + +#### [`src/config/index.ts`](packages/core/src/config/index.ts) +**Purpose:** Configuration module exports. + +#### [`src/config/schema.ts`](packages/core/src/config/schema.ts) +**Purpose:** Project configuration schema validation using Zod. +- **Exports:** `ProviderTypeSchema`, `BetterBaseConfigSchema`, `defineConfig`, `validateConfig`, `parseConfig`, `assertConfig` +- **Key Types:** `ProviderType`, `BetterBaseConfig` +- **Validation Features:** + - Validates project configuration structure + - Provider-specific validation (e.g., Turso requires url and authToken) + - Storage configuration validation + - Webhook configuration validation + - GraphQL configuration validation + +#### [`src/config/drizzle-generator.ts`](packages/core/src/config/drizzle-generator.ts) +**Purpose:** Drizzle configuration generator based on provider type. +- **Exports:** `generateDrizzleConfig`, `getDialectForProvider`, `getDriverForProvider`, `getRequiredEnvVars` +- **Key Types:** `DrizzleDriver`, `DrizzleDialect`, `DrizzleConfigOutput`, `DbCredentials` +- **Supported Providers:** Neon, Turso, PlanetScale, Supabase, PostgreSQL, managed + +### functions/ + +#### [`functions/index.ts`](packages/core/src/functions/index.ts) +**Purpose:** Serverless functions module exports. + +#### [`functions/bundler.ts`](packages/core/src/functions/bundler.ts) +**Purpose:** Bundles serverless functions using Bun's build API for edge compatibility. +- **Exports:** `bundleFunction`, `readFunctionConfig`, `listFunctions`, `isFunctionBuilt` +- **Key Types:** `BundleResult`, `FunctionConfig`, `FunctionInfo` +- **Features:** + - Bundles TypeScript functions into single JavaScript file + - Supports Cloudflare Workers and Vercel Edge runtime + - Handles function configuration from config.ts + - Lists all functions in project + - Checks if function has been built + +#### [`functions/deployer.ts`](packages/core/src/functions/deployer.ts) +**Purpose:** Deploys serverless functions to cloud providers. +- **Exports:** `deployToCloudflare`, `deployToVercel`, `syncEnvToCloudflare`, `syncEnvToVercel`, `getCloudflareLogs`, `getVercelLogs` +- **Key Types:** `DeployResult` +- **Features:** + - Deploys to Cloudflare Workers using Wrangler CLI + - Deploys to Vercel Edge using Vercel CLI + - Syncs environment variables + - Retrieves function logs + +### graphql/ + +#### [`graphql/index.ts`](packages/core/src/graphql/index.ts) +**Purpose:** GraphQL module exports. + +#### [`graphql/resolvers.ts`](packages/core/src/graphql/resolvers.ts) +**Purpose:** GraphQL resolver generator that calls Drizzle ORM directly. +- **Exports:** `generateResolvers`, `GraphQLContext`, `GraphQLResolver`, `Resolvers`, `ResolverGenerationConfig` +- **Key Features:** + - Auto-generates resolvers from Drizzle schema + - Supports queries, mutations, and subscriptions (placeholder) + - Respect auth context from BetterAuth + - Custom hooks for before/after mutations + - Error handling + +#### [`graphql/schema-generator.ts`](packages/core/src/graphql/schema-generator.ts) +**Purpose:** Generates GraphQL schema from Drizzle ORM schema. +- **Exports:** `generateGraphQLSchema`, `GraphQLGenerationConfig` +- **Key Features:** + - Auto-generates GraphQL types from Drizzle schema + - Creates input types for mutations + - Generates query types for tables + - Supports custom type mappings + - Handles relationships and pagination + +#### [`graphql/sdl-exporter.ts`](packages/core/src/graphql/sdl-exporter.ts) +**Purpose:** Exports GraphQL schema as SDL (Schema Definition Language) string. +- **Exports:** `exportSDL`, `exportTypeSDL`, `saveSDL` +- **Key Features:** + - Exports complete GraphQL schema as SDL + - Exports individual types as SDL + - Saves SDL to file + - Customizable output options (descriptions, sorting) + +#### [`graphql/server.ts`](packages/core/src/graphql/server.ts) +**Purpose:** GraphQL HTTP server using graphql-yoga that integrates with Hono. +- **Exports:** `createGraphQLServer`, `startGraphQLServer`, `GraphQLConfig` +- **Key Features:** + - Creates Hono-compatible GraphQL server + - Supports authentication + - GraphQL Playground in development + - Subscriptions support + - Health check endpoint + +### middleware/ + +#### [`middleware/index.ts`](packages/core/src/middleware/index.ts) +**Purpose:** Middleware module exports. + +#### [`middleware/rls-session.ts`](packages/core/src/middleware/rls-session.ts) +**Purpose:** RLS session middleware for Hono. +- **Exports:** `rlsSession`, `requireRLS`, `clearRLS`, `getRLSUserId`, `isRLSSessionSet` +- **Key Constants:** `RLS_USER_ID_KEY`, `RLS_SESSION_SET_KEY` +- **Key Types:** `RLSCContext` +- **Features:** + - Reads authenticated user from BetterAuth session + - Makes user ID available for RLS policies + - Idempotent operations (safe to call multiple times) + - Requires RLS to be set for protected routes + - Clears RLS context (e.g., on logout) + +### migration/ + +#### [`migration/index.ts`](packages/core/src/migration/index.ts) +**Purpose:** Migration module exports. + +#### [`migration/rls-migrator.ts`](packages/core/src/migration/rls-migrator.ts) +**Purpose:** Applies RLS policies to the database. +- **Exports:** `applyAuthFunction`, `dropAuthFunctionSQL`, `applyPolicies`, `dropPolicies`, `dropTableRLS`, `applyRLSMigration`, `getAppliedPolicies` +- **Features:** + - Applies auth.uid() function to database + - Creates RLS policies from policy definitions + - Idempotent operations (safe to run multiple times) + - Drops RLS policies from database + - Disables RLS on tables + - Gets information about applied policies + +### providers/ + +#### [`providers/index.ts`](packages/core/src/providers/index.ts) +**Purpose:** Database providers module exports. +- **Exports:** `resolveProvider`, `resolveProviderByType`, `getSupportedProviders`, `providerSupportsRLS`, `getProviderDialect`, `ManagedProviderNotSupportedError` + +#### [`providers/types.ts`](packages/core/src/providers/types.ts) +**Purpose:** Provider interface definitions. +- **Exports:** All provider types and interfaces +- **Key Types:** + - `DatabaseDialect`: "postgres", "mysql", "sqlite" + - `ProviderConfig`: Configuration for connecting to a database provider + - `DatabaseConnection`: Database connection wrapper + - `DrizzleMigrationDriver`: Migration driver interface + - `ProviderAdapter`: Provider adapter interface + - `onchange()`: CDC (Change Data Capture) callback for database changes +- **Provider-Specific Types:** + - `NeonProviderConfig`, `NeonDatabaseConnection`, `NeonMigrationDriver` + - `TursoProviderConfig`, `TursoDatabaseConnection`, `TursoMigrationDriver` + - `PlanetScaleProviderConfig`, `PlanetScaleDatabaseConnection`, `PlanetScaleMigrationDriver` + - `SupabaseProviderConfig`, `SupabaseDatabaseConnection`, `SupabaseMigrationDriver` + - `PostgresProviderConfig`, `PostgresDatabaseConnection`, `PostgresMigrationDriver` + - `ManagedProviderConfig` + +#### [`providers/neon.ts`](packages/core/src/providers/neon.ts) +**Purpose:** Neon database provider implementation. + +#### [`providers/planetscale.ts`](packages/core/src/providers/planetscale.ts) +**Purpose:** PlanetScale database provider implementation. + +#### [`providers/postgres.ts`](packages/core/src/providers/postgres.ts) +**Purpose:** PostgreSQL database provider implementation. + +#### [`providers/supabase.ts`](packages/core/src/providers/supabase.ts) +**Purpose:** Supabase database provider implementation. + +#### [`providers/turso.ts`](packages/core/src/providers/turso.ts) +**Purpose:** Turso database provider implementation. + +### rls/ + +#### [`rls/index.ts`](packages/core/src/rls/index.ts) +**Purpose:** RLS module exports. + +#### [`rls/auth-bridge.ts`](packages/core/src/rls/auth-bridge.ts) +**Purpose:** Creates the auth.uid() PostgreSQL function for RLS policies. +- **Exports:** `generateAuthFunction`, `generateAuthFunctionWithSetting`, `dropAuthFunction`, `setCurrentUserId`, `clearCurrentUserId`, `generateIsAuthenticatedCheck`, `dropIsAuthenticatedCheck`, `generateAllAuthFunctions`, `dropAllAuthFunctions` +- **Key Features:** + - Generates SQL to create auth.uid() function + - Generates SQL to set/clear current user ID + - Generates is_authenticated() helper function + - Handles SQL injection protection + - Generates all auth bridge functions at once + +#### [`rls/generator.ts`](packages/core/src/rls/generator.ts) +**Purpose:** RLS Policy SQL Generator. +- **Exports:** `policyToSQL`, `dropPolicySQL`, `dropPolicyByName`, `disableRLS`, `hasPolicyConditions`, `policiesToSQL`, `dropPoliciesSQL` +- **Key Types:** `PolicyOperation` +- **Features:** + - Generates SQL to create RLS policies + - Generates SQL to drop RLS policies + - Handles policy operations (select, insert, update, delete) + - Checks if policy has conditions + - Converts policies to SQL statements + +#### [`rls/scanner.ts`](packages/core/src/rls/scanner.ts) +**Purpose:** Scans a project for policy definition files and loads them. +- **Exports:** `scanPolicies`, `scanPoliciesStrict`, `listPolicyFiles`, `getPolicyFileInfo`, `PolicyScanError` +- **Key Types:** `ScanResult`, `PolicyFileInfo` +- **Features:** + - Scans project for policy files (*.policy.ts) + - Loads policy definitions + - Handles errors gracefully + - Lists policy files without loading them + - Extracts metadata from policy files + +#### [`rls/types.ts`](packages/core/src/rls/types.ts) +**Purpose:** RLS (Row Level Security) Policy Definition Types. +- **Exports:** `definePolicy`, `isPolicyDefinition`, `mergePolicies` +- **Key Types:** `PolicyDefinition`, `PolicyConfig` +- **Features:** + - Helper function to create policy definitions + - Type guard to check if value is a valid PolicyDefinition + - Merges multiple policy configs for the same table + +#### [`rls/evaluator.ts`](packages/core/src/rls/evaluator.ts) +**Purpose:** RLS Policy Evaluator for enforcing row-level security. +- **Exports:** `evaluatePolicy`, `applyRLSSelect`, `applyRLSInsert`, `applyRLSUpdate`, `applyRLSDelete` +- **Key Features:** + - Evaluates RLS policies for database operations + - Supports SELECT, INSERT, UPDATE, DELETE operations + - SQLite-compatible policy evaluation + - `evaluatePolicy()` function for evaluating policy expressions + - Applies RLS policies to Drizzle queries + +### storage/ + +#### [`storage/index.ts`](packages/core/src/storage/index.ts) +**Purpose:** Storage Module - Fluent Builder API. +- **Exports:** `createStorage`, `resolveStorageAdapter`, `Storage` +- **Key Types:** `StorageFactory`, `BucketClient`, `StorageConfig`, `UploadOptions`, `SignedUrlOptions`, `UploadResult`, `StorageObject` +- **Features:** + - Supabase-compatible storage API + - Fluent `.from(bucket)` API + - Resolves storage adapter based on provider + - Handles async operations with { data, error } pattern + +#### [`storage/s3-adapter.ts`](packages/core/src/storage/s3-adapter.ts) +**Purpose:** S3-Compatible Storage Adapter Implementation. +- **Exports:** `S3StorageAdapter`, `createS3Adapter` +- **Key Features:** + - Implements StorageAdapter interface for S3-compatible services + - Supports AWS S3, Cloudflare R2, Backblaze B2, MinIO + - Handles upload, download, delete, list, signed URL operations + - Uses AWS SDK v3 + - Converts ReadableStream to Buffer for Bun runtime + +#### [`storage/types.ts`](packages/core/src/storage/types.ts) +**Purpose:** Storage Types for S3-Compatible Storage Adapter. +- **Key Types:** + - `StorageProvider`: "s3", "r2", "backblaze", "minio", "managed" + - `StorageConfig`: Union of all storage provider config types + - `UploadOptions`: File upload options (contentType, metadata, isPublic) + - `SignedUrlOptions`: Signed URL options (expiresIn) + - `UploadResult`: Result of successful upload + - `StorageObject`: Represents a storage object + - `StorageAdapter`: Core storage adapter interface + - `AllowedMimeTypes`: Array of allowed MIME types for uploads + - `BucketConfig`: Bucket configuration with size limits and allowed types + +#### [`storage/policy-engine.ts`](packages/core/src/storage/policy-engine.ts) +**Purpose:** Storage Policy Engine for evaluating access policies. +- **Exports:** `evaluateStoragePolicy`, `checkStorageAccess`, `StoragePolicy` +- **Key Features:** + - Evaluates storage access policies + - Supports path-based access control + - Integrates with RLS user context + - `evaluateStoragePolicy()` function for policy evaluation + +#### [`storage/image-transformer.ts`](packages/core/src/storage/image-transformer.ts) +**Purpose:** On-the-fly image transformations using Sharp. +- **Exports:** `ImageTransformer`, `createImageTransformer`, `TransformOptions` +- **Key Features:** + - Resize images (width, height, fit modes) + - Crop images to specific dimensions + - Format conversion (JPEG, PNG, WebP, AVIF) + - Quality adjustment + - Auto-optimization + - Lazy transformation (on-demand) + +### vector/ + +Vector Search module for pgvector support in PostgreSQL. + +#### [`vector/types.ts`](packages/core/src/vector/types.ts) +**Purpose:** Vector Search Type Definitions. +- **Key Types:** + - `EmbeddingProvider`: "openai" | "cohere" | "huggingface" | "custom" + - `SimilarityMetric`: "cosine" | "euclidean" | "inner_product" + - `EmbeddingConfig`: Configuration for embedding generation + - `EmbeddingInput`: Input for generating an embedding + - `EmbeddingResult`: Generated embedding result + - `SearchOptions`: Options for vector search + - `VectorSearchResult`: Search result with similarity score + +#### [`vector/embeddings.ts`](packages/core/src/vector/embeddings.ts) +**Purpose:** Embedding Generation Providers. +- **Exports:** `generateEmbedding`, `generateEmbeddings`, `normalizeVector`, `computeCosineSimilarity`, `createEmbeddingConfig`, `EmbeddingProviderBase`, `OpenAIEmbeddingProvider`, `CohereEmbeddingProvider`, `createEmbeddingProvider`, `DEFAULT_EMBEDDING_CONFIGS`, `validateEmbeddingDimensions` +- **Key Features:** + - OpenAI embeddings provider (text-embedding-3-small, text-embedding-3-large, text-embedding-ada-002) + - Cohere embeddings provider (embed-english-v3.0, embed-multilingual-v3.0) + - Vector normalization utilities + - Cosine similarity computation + - Configurable embedding dimensions + +#### [`vector/search.ts`](packages/core/src/vector/search.ts) +**Purpose:** Vector Similarity Search Functions. +- **Exports:** `VECTOR_OPERATORS`, `vectorDistance`, `cosineDistance`, `euclideanDistance`, `innerProductDistance`, `vectorSearch`, `createVectorIndex` +- **Key Features:** + - pgvector operator mappings for PostgreSQL + - Cosine distance calculation + - Euclidean distance calculation + - Inner product calculation + - Vector search with filtering and pagination + - Drizzle ORM integration for type-safe queries + +#### [`vector/index.ts`](packages/core/src/vector/index.ts) +**Purpose:** Vector Module - Main entry point. +- **Exports:** All types and functions from the vector module +- **Key Features:** + - Unified API for embedding generation and vector search + - Support for multiple embedding providers + - Type-safe vector operations with Drizzle ORM + +### branching/ + +Preview Environments module for creating isolated development branches. + +#### [`branching/types.ts`](packages/core/src/branching/types.ts) +**Purpose:** Branching/Preview Environment Types. +- **Key Types:** + - `BranchStatus`: Enum (ACTIVE, SLEEPING, DELETED) + - `BranchConfig`: Configuration for a preview environment + - `PreviewEnvironment`: Complete preview environment definition + - `CreateBranchOptions`: Options for creating a new branch + - `BranchingConfig`: Global branching configuration + - `BranchOperationResult`: Result of branch operations + - `BranchListResult`: List of branches with pagination + +#### [`branching/database.ts`](packages/core/src/branching/database.ts) +**Purpose:** Database Branching for Preview Environments. +- **Exports:** `DatabaseBranching`, `createDatabaseBranching`, `buildBranchConfig` +- **Key Features:** + - Creates isolated database copies for preview environments + - Supports PostgreSQL database cloning + - Manages connection strings for branch databases + - Handles database cleanup on branch deletion + +#### [`branching/storage.ts`](packages/core/src/branching/storage.ts) +**Purpose:** Storage Branching for Preview Environments. +- **Exports:** `StorageBranching`, `createStorageBranching` +- **Key Features:** + - Creates isolated storage buckets for preview environments + - Supports S3-compatible storage backends + - Manages storage namespace per branch + - Handles storage cleanup on branch deletion + +#### [`branching/index.ts`](packages/core/src/branching/index.ts) +**Purpose:** Branching Module - Main Orchestration. +- **Exports:** `BranchManager`, `createBranchManager`, `getAllBranches`, `clearAllBranches` +- **Key Features:** + - Orchestrates database and storage branching together + - Creates and manages preview environments + - Handles branch sleep/wake cycles + - Provides unified API for branch operations + +### auto-rest.ts + +#### [`auto-rest.ts`](packages/core/src/auto-rest.ts) +**Purpose:** Automatic CRUD Route Generation from Drizzle Schema. +- **Exports:** `mountAutoRest`, `AutoRestOptions`, `DrizzleTable`, `DrizzleDB` +- **Key Features:** + - Runtime route registration for all tables in schema + - Auto-generates full CRUD operations + - Configurable base path (default: /api) + - Supports table exclusion + - RLS enforcement option + - Generated Routes: + - `GET /api/:table` - List all rows (paginated) + - `GET /api/:table/:id` - Get single row by ID + - `POST /api/:table` - Insert new row + - `PATCH /api/:table/:id` - Update existing row + - `DELETE /api/:table/:id` - Delete row + +### iac/ (NEW - Phase 3) + +Infrastructure as Code module - Convex-inspired database and functions. + +#### [`iac/index.ts`](packages/core/src/iac/index.ts) +**Purpose:** IaC module exports. +- **Exports:** `query`, `mutation`, `action`, `defineSchema`, `defineTable`, `v`, `cron` + +#### [`iac/schema.ts`](packages/core/src/iac/schema.ts) +**Purpose:** Schema definition with `defineSchema` and `defineTable`. +- **Exports:** `defineSchema`, `defineTable`, `SchemaDefinition`, `TableDefinition` +- **Key Features:** + - Define tables with fields and indexes + - Support for full-text and vector fields + - Index definitions for query optimization + +#### [`iac/functions.ts`](packages/core/src/iac/functions.ts) +**Purpose:** Function registration (query, mutation, action). +- **Exports:** `query`, `mutation`, `action`, `QueryRegistration`, `MutationRegistration`, `ActionRegistration` +- **Key Features:** + - Optimistic updates support (`optimistic` field) + - Argument validation with v.* validators + - Handler functions with full ctx access + +#### [`iac/validators.ts`](packages/core/src/iac/validators.ts) +**Purpose:** Validators for IaC function arguments. +- **Exports:** `v` object with `string()`, `number()`, `boolean()`, `id()`, `optional()`, `array()`, `object()`, `fullText()`, `vector()` +- **Key Types:** `VString`, `VNumber`, `VBoolean`, `VAny` +- **Key Features:** + - Type-safe argument validation + - `fullText()` for PostgreSQL FTS fields + - `vector(dimensions)` for pgvector fields + +#### [`iac/db-context.ts`](packages/core/src/iac/db-context.ts) +**Purpose:** Database context for IaC functions. +- **Exports:** `DatabaseReader`, `DatabaseWriter`, `QueryBuilder` +- **Key Methods:** + - `get(table, id)` - Get single document + - `query(table)` - Create query builder + - `insert(table, doc)` - Insert document + - `patch(table, id, doc)` - Update document + - `delete(table, id)` - Delete document + - `execute(sql, params)` - Raw SQL execution (NEW) + - `search(table, query)` - Full-text search (NEW) + - `similarity(table, embedding, options)` - Vector search (NEW) + - `analyze(query)` - Query diagnostics (NEW) + +#### [`iac/cron.ts`](packages/core/src/iac/cron.ts) +**Purpose:** Cron job scheduling for scheduled tasks. +- **Exports:** `cron`, `getCronJobs`, `CronJob` +- **Key Features:** + - Cron expression scheduling + - Registered jobs run on schedule + +#### [`iac/errors.ts`](packages/core/src/iac/errors.ts) (NEW) +**Purpose:** Improved error classes with suggestions. +- **Exports:** `IaCError`, `ValidationError`, `DatabaseError`, `AuthError`, `NotFoundError`, `formatError` +- **Key Features:** + - Error codes and suggestions + - Auto-suggestions for common errors + - Links to documentation + +#### [`iac/schema-serializer.ts`](packages/core/src/iac/schema-serializer.ts) +**Purpose:** Serializes IaC schema to Drizzle schema. +- **Exports:** `serializeSchema`, `serializeTable` + +#### [`iac/schema-diff.ts`](packages/core/src/iac/schema-diff.ts) +**Purpose:** Computes schema diffs for migrations. +- **Exports:** `diffSchema`, `SchemaDiff` + +#### [`iac/function-registry.ts`](packages/core/src/iac/function-registry.ts) +**Purpose:** Registry for all IaC functions. +- **Exports:** `registerFunction`, `lookupFunction`, `listFunctions` + +#### [`iac/storage/`](packages/core/src/iac/storage/) +**Purpose:** IaC storage context for file operations. + +#### [`iac/storage/storage-ctx.ts`](packages/core/src/iac/storage/storage-ctx.ts) +**Purpose:** Storage operations for IaC functions. +- **Exports:** `StorageContext`, `createStorageContext` +- **Key Features:** + - File upload and download + - Signed URL generation + - Storage policy enforcement + +#### [`iac/realtime/`](packages/core/src/iac/realtime/) +**Purpose:** IaC realtime subscription infrastructure. + +#### [`iac/realtime/table-dep-inferrer.ts`](packages/core/src/iac/realtime/table-dep-inferrer.ts) +**Purpose:** Infers table dependencies from queries for cache invalidation. +- **Exports:** `inferTableDependencies`, `TableDependencies` + +#### [`iac/realtime/invalidation-manager.ts`](packages/core/src/iac/realtime/invalidation-manager.ts) +**Purpose:** Manages query result cache invalidation. +- **Exports:** `InvalidationManager`, `createInvalidationManager` +- **Key Features:** + - Tracks query dependencies + - Invalidates cached results on data changes + +#### [`iac/realtime/subscription-tracker.ts`](packages/core/src/iac/realtime/subscription-tracker.ts) +**Purpose:** Tracks active subscriptions for realtime updates. +- **Exports:** `SubscriptionTracker`, `createSubscriptionTracker` +- **Key Features:** + - Manages subscription lifecycle + - Broadcasts updates to subscribers + +### webhooks/ + +#### [`webhooks/index.ts`](packages/core/src/webhooks/index.ts) +**Purpose:** Webhook module exports. + +#### [`webhooks/dispatcher.ts`](packages/core/src/webhooks/dispatcher.ts) +**Purpose:** WebhookDispatcher handles sending webhook payloads to configured endpoints. +- **Exports:** `WebhookDispatcher` +- **Key Types:** `RetryConfig`, `WebhookDeliveryLog` +- **Features:** + - Handles webhook dispatch with retry logic + - Tests webhooks by sending synthetic payload + - Tracks delivery logs + - Fire-and-forget pattern + - Retry with exponential backoff + +#### [`webhooks/integrator.ts`](packages/core/src/webhooks/integrator.ts) +**Purpose:** Connects WebhookDispatcher to realtime event emitter. +- **Exports:** `connectToRealtime` +- **Features:** + - Listens for database change events + - Bridges Phase 6 (Realtime WebSockets) with Phase 13 (Webhooks) + - Handles 'db:change', 'db:insert', 'db:update', 'db:delete' events + +#### [`webhooks/signer.ts`](packages/core/src/webhooks/signer.ts) +**Purpose:** Signs and verifies webhook payloads using HMAC-SHA256. +- **Exports:** `signPayload`, `verifySignature` +- **Features:** + - Signs payload with secret using HMAC-SHA256 + - Verifies signatures using timing-safe comparison + - Prevents timing attacks + - Handles both string and object payloads + +#### [`webhooks/startup.ts`](packages/core/src/webhooks/startup.ts) +**Purpose:** Initializes webhooks from configuration during server startup. +- **Exports:** `initializeWebhooks` +- **Key Features:** + - Loads webhooks from BetterBase config + - Resolves environment variable references + - Creates webhook dispatcher + - Connects to realtime emitter + - Handles missing environment variables + +#### [`webhooks/types.ts`](packages/core/src/webhooks/types.ts) +**Purpose:** Webhook configuration and payload types. +- **Key Types:** + - `WebhookConfig`: Webhook configuration (id, table, events, url, secret, enabled) + - `WebhookPayload`: Payload sent to webhook endpoint (id, webhook_id, table, type, record, old_record, timestamp) + +### logger/ + +Logging module for application-wide logging capabilities. + +#### [`logger/index.ts`](packages/core/src/logger/index.ts) +**Purpose:** Logger module exports. +- **Exports:** `BetterBaseLogger`, `createLogger`, `logLevel`, `LogEntry` +- **Key Features:** + - Configurable log levels (debug, info, warn, error) + - Structured logging with metadata + - File transport support + - Console output with colors + +#### [`logger/file-transport.ts`](packages/core/src/logger/file-transport.ts) +**Purpose:** File-based logging transport. +- **Exports:** `FileTransport`, `createFileTransport` +- **Key Features:** + - Rotating log files + - Configurable file paths + - Log rotation by size or time + +### realtime/ + +Realtime subscriptions module for WebSocket-based live data updates. + +#### [`realtime/index.ts`](packages/core/src/realtime/index.ts) +**Purpose:** Realtime module exports. +- **Exports:** `RealtimeManager`, `createRealtimeManager`, `Channel`, `Subscription` + +#### [`realtime/channel-manager.ts`](packages/core/src/realtime/channel-manager.ts) +**Purpose:** Channel manager for managing WebSocket subscriptions. +- **Exports:** `ChannelManager`, `createChannelManager` +- **Key Features:** + - Subscribe to database changes (INSERT, UPDATE, DELETE) + - Filter by table, schema, or specific records + - Automatic reconnection with exponential backoff + - Presence detection for collaborative features + +--- + +## packages/client + +`@betterbase/client` - TypeScript SDK for BetterBase backends (like `@supabase/supabase-js`). + +### Client Modules + +#### [`src/auth.ts`](packages/client/src/auth.ts) +**Purpose:** Authentication client for BetterAuth integration. +- **Exports:** `AuthClient`, `authClient`, `createAuthClientInstance` +- **Key Types:** `User`, `Session`, `StorageAdapter` +- **Features:** + - Wraps BetterAuth client + - Handles sign up, sign in, sign out, get session + - Manages session token in localStorage + - On auth state change callback + - Fallback storage adapter + - **New Authentication Methods:** + - `sendMagicLink(email)` - Send magic link for passwordless login + - `verifyMagicLink(email, code)` - Verify magic link code + - `sendOtp(email)` - Send one-time password + - `verifyOtp(email, code)` - Verify OTP code + - `mfa.enable()` - Enable multi-factor authentication + - `mfa.verify(code)` - Verify MFA code + - `mfa.disable()` - Disable MFA + - `mfa.challenge()` - Challenge MFA + - `sendPhoneVerification(phone)` - Send phone verification SMS + - `verifyPhone(phone, code)` - Verify phone number + +#### [`src/client.ts`](packages/client/src/client.ts) +**Purpose:** Main BetterBase client constructor. +- **Exports:** `createClient`, `BetterBaseClient` +- **Key Types:** `BetterBaseConfig` +- **Features:** + - Configuration validation with Zod + - Initializes auth, realtime, and storage clients + - Manages authentication state + - Provides fetch method with authenticated headers + - Query builder support + +#### [`src/query-builder.ts`](packages/client/src/query-builder.ts) +**Purpose:** Chainable query builder for database operations. +- **Exports:** `QueryBuilder` +- **Key Types:** `QueryBuilderOptions`, `QueryOptions` +- **Methods:** + - `select(fields)`: Select fields to retrieve + - `eq(column, value)`: Add equality filter + - `in(column, values)`: Add IN filter + - `limit(count)`: Limit number of results + - `offset(count)`: Offset results + - `order(column, direction)`: Sort results + - `execute()`: Execute query + - `single(id)`: Get single record by ID + - `insert(data)`: Insert new record + - `update(id, data)`: Update record + - `delete(id)`: Delete record + +#### [`src/realtime.ts`](packages/client/src/realtime.ts) +**Purpose:** Real-time subscription client for database changes. +- **Exports:** `RealtimeClient` +- **Key Types:** `RealtimeCallback`, `RealtimeSubscription`, `RealtimeEvent` +- **Features:** + - WebSocket-based realtime updates + - Subscription management + - Reconnect logic with exponential backoff + - Event filtering + - Supports INSERT, UPDATE, DELETE, and * (all) events + +#### [`src/storage.ts`](packages/client/src/storage.ts) +**Purpose:** Storage client for file operations. +- **Exports:** `Storage`, `StorageBucketClient` +- **Key Types:** `UploadOptions`, `SignedUrlOptions`, `StorageFile`, `UploadResult`, `PublicUrlResult`, `SignedUrlResult`, `RemoveResult` +- **Features:** + - Supabase-compatible storage API + - Fluent `.from(bucket)` API + - Upload, download, remove, list operations + - Public URL and signed URL generation + - Handles File, Blob, and ArrayBuffer inputs + - Error handling with { data, error } pattern + +#### [`src/types.ts`](packages/client/src/types.ts) +**Purpose:** TypeScript type definitions for client. +- **Exports:** All client types and interfaces + +#### [`src/errors.ts`](packages/client/src/errors.ts) +**Purpose:** Client-side error classes. +- **Exports:** Custom error classes (AuthError, NetworkError, ValidationError, etc.) + +#### [`src/index.ts`](packages/client/src/index.ts) +**Purpose:** Client package entry point. +- **Exports:** All public APIs from the client package + +#### [`src/build.ts`](packages/client/src/build.ts) +**Purpose:** Build configuration for client package. + +### IaC Client Modules (NEW - Phase 3) + +#### [`src/iac/provider.tsx`](packages/client/src/iac/provider.tsx) +**Purpose:** React context provider for IaC functions. +- **Exports:** `BetterBaseReactProvider`, `useIaC` +- **Key Features:** + - Wraps React app with BetterBase context + - `useIaC()` hook returns query/mutation/action functions + - Configurable base URL and auth token + - Compatible with TanStack Query + +#### [`src/iac/hooks.ts`](packages/client/src/iac/hooks.ts) +**Purpose:** React hooks for IaC functions (query, mutation). +- **Exports:** `useQuery`, `useMutation`, `useAction` +- **Key Features:** + - `useQuery(path, args)` - Subscribe to query results + - `useMutation(path)` - Execute mutations with optimistic updates + - `useAction(path)` - Execute one-off actions + - Optimistic updates support (`optimisticData` return) + +#### [`src/iac/vanilla.ts`](packages/client/src/iac/vanilla.ts) +**Purpose:** Non-React IaC client for vanilla JS/other frameworks. +- **Exports:** `createIaCClient`, `IaCClient` +- **Key Methods:** + - `query(path, args)` - Execute query + - `mutation(path, args, options)` - Execute mutation + - `action(path, args)` - Execute action + +#### [`src/iac/embeddings.ts`](packages/client/src/iac/embeddings.ts) +**Purpose:** Embedding generation utilities for vector search. +- **Exports:** `generateEmbedding`, `createEmbeddingProvider` +- **Key Features:** + - OpenAI embeddings support + - Cohere embeddings support + - Text-to-vector conversion + +#### [`src/iac/paginated-query.ts`](packages/client/src/iac/paginated-query.ts) +**Purpose:** Paginated query support for IaC functions. +- **Exports:** `PaginatedQuery`, `createPaginatedQuery` +- **Key Features:** + - Cursor-based pagination + - Limit and offset support + - Total count estimation + +--- + +## packages/cli + +Canonical `@betterbase/cli` implementation - the `bb` command-line tool. + +### CLI Commands + +#### [`commands/init.ts`](packages/cli/src/commands/init.ts) +**Purpose:** `bb init` command - scaffolds new BetterBase projects. +- **Exports:** `runInitCommand(options)` - main command function, `InitCommandOptions` - type +- **Key Functions:** `installDependencies()`, `initializeGitRepository()`, `buildPackageJson()`, `buildDrizzleConfig()`, `buildSchema()`, `buildMigrateScript()`, `buildDbIndex()`, `buildAuthMiddleware()`, `buildReadme()`, `buildRoutesIndex()`, `writeProjectFiles()` +- **Internal Deps:** `../utils/logger`, `../utils/prompts` +- **Usage Patterns:** Typically called by developers starting a new project. Uses interactive prompts to gather project name, database mode, and options. Creates a complete project structure with sensible defaults. +- **Implementation Details:** Uses Inquirer for interactive prompts, writes files synchronously using fs module. Supports three database modes: local (SQLite), neon (PostgreSQL), turso (LibSQL). Generates Zod-validated config. Implements file templating with template literals for code generation. +- **External Deps:** `inquirer`, `zod`, `chalk` +- **Cross-Ref:** [`packages/cli/src/utils/prompts.ts`](packages/cli/src/utils/prompts.ts), [`apps/test-project/`](apps/test-project/) + +#### [`commands/dev.ts`](packages/cli/src/commands/dev.ts) +**Purpose:** `bb dev` command - watches schema/routes and regenerates context. +- **Exports:** `runDevCommand(projectRoot)` - returns cleanup function +- **Internal Deps:** `../utils/context-generator`, `../utils/logger` +- **Usage Patterns:** Runs during development to continuously regenerate `.betterbase-context.json` as files change. +- **Implementation Details:** Sets up file watchers on schema and routes directories, triggers context regeneration on changes. Returns cleanup function to stop watchers. +- **External Deps:** `bun`, `chalk` +- **Cross-Ref:** [`packages/cli/src/utils/context-generator.ts`](packages/cli/src/utils/context-generator.ts) + +#### [`commands/migrate.ts`](packages/cli/src/commands/migrate.ts) +**Purpose:** `bb migrate` commands - generates and applies migrations with safety checks. +- **Exports:** `runMigrateCommand(options)` - main function, `MigrateCommandOptions` - type, `MigrationChange` - interface, `MigrationChangeType` - type +- **Key Functions:** `runDrizzleKit()`, `listSqlFiles()`, `analyzeMigration()`, `displayDiff()`, `confirmDestructive()`, `backupDatabase()`, `restoreBackup()`, `splitStatements()`, `collectChangesFromGenerate()` +- **Internal Deps:** `../constants`, `../utils/logger`, `../utils/prompts` +- **Usage Patterns:** Called during database schema changes. Generates migration files, optionally previews changes, applies with safety checks. +- **Implementation Details:** Wraps DrizzleKit for migration generation. Implements visual diff display with color-coded changes. Prompts for confirmation on destructive operations. Creates automatic backups before dangerous migrations. Parses SQL files to extract migration metadata. +- **External Deps:** `drizzle-orm`, `drizzle-kit`, `inquirer`, `chalk`, `zod` + +#### [`commands/auth.ts`](packages/cli/src/commands/auth.ts) +**Purpose:** `bb auth setup` command - scaffolds BetterAuth integration. +- **Exports:** `runAuthSetupCommand(projectRoot)` - main function +- **Key Constants:** `AUTH_SCHEMA_BLOCK` - sessions/accounts tables SQL, `AUTH_ROUTE_FILE` - auth routes template, `AUTH_MIDDLEWARE_FILE` - requireAuth/optionalAuth middleware +- **Key Functions:** `appendIfMissing()`, `ensurePasswordHashColumn()`, `ensureAuthInConfig()`, `ensureEnvVar()`, `ensureRoutesIndexHook()` +- **Internal Deps:** `../utils/logger` +- **Usage Patterns:** Run after project initialization to add authentication. Modifies existing files to integrate BetterAuth. +- **Implementation Details:** Injects SQL schema blocks into existing schema file, adds auth routes to routes index, creates auth middleware. Uses file patching rather than full file generation for integration. +- **External Deps:** `better-auth`, `chalk` + +#### [`commands/generate.ts`](packages/cli/src/commands/generate.ts) +**Purpose:** `bb generate crud` command - generates CRUD routes for a table. +- **Exports:** `runGenerateCrudCommand(projectRoot, tableName)` - main function +- **Key Functions:** `toSingular()`, `schemaTypeToZod()`, `buildSchemaShape()`, `buildFilterableColumns()`, `buildFilterCoercers()`, `generateRouteFile()`, `updateMainRouter()`, `ensureRealtimeUtility()`, `ensureZodValidatorInstalled()` +- **Internal Deps:** `../utils/schema-scanner`, `../utils/logger` +- **Usage Patterns:** Called after creating a database table to auto-generate REST API routes. Saves developers from writing boilerplate CRUD code. +- **Implementation Details:** Scans Drizzle schema to understand table structure. Maps Drizzle column types to Zod schemas. Generates Hono routes with type-safe handlers. Updates route index to register new endpoints. +- **External Deps:** `zod`, `hono`, `drizzle-orm`, `chalk` +- **Cross-Ref:** [`packages/cli/src/utils/scanner.ts`](packages/cli/src/utils/scanner.ts) + +#### [`commands/function.ts`](packages/cli/src/commands/function.ts) +**Purpose:** `bb function` command - manages serverless functions. +- **Exports:** Function management commands (create, deploy, list, invoke) +- **Key Functions:** Function deployment and bundling +- **Internal Deps:** `../utils/logger`, `../utils/prompts` +- **Usage Patterns:** Deploy and manage serverless functions. +- **Implementation Details:** Handles function bundling, deployment to edge, and invocation. +- **External Deps:** `chalk`, `inquirer` + +#### [`commands/graphql.ts`](packages/cli/src/commands/graphql.ts) +**Purpose:** `bb graphql` command - GraphQL schema management. +- **Exports:** GraphQL schema generation and introspection commands +- **Key Functions:** Schema generation, SDL export +- **Internal Deps:** `../utils/logger`, `../utils/prompts` +- **Usage Patterns:** Generate GraphQL schema from database, export SDL. +- **Implementation Details:** Uses Drizzle introspection to generate GraphQL types. +- **External Deps:** `chalk`, `inquirer` + +#### [`commands/login.ts`](packages/cli/src/commands/login.ts) +**Purpose:** `bb login` command - authenticate with BetterBase cloud. +- **Exports:** `runLoginCommand(options)` - main function +- **Internal Deps:** `../utils/logger` +- **Usage Patterns:** Authenticate to BetterBase to access cloud features. +- **Implementation Details:** Handles OAuth flow or API key authentication. +- **External Deps:** `chalk` + +#### [`commands/rls.ts`](packages/cli/src/commands/rls.ts) +**Purpose:** `bb rls` command - Row Level Security management. +- **Exports:** RLS policy management commands +- **Key Functions:** Policy creation, enable/disable RLS +- **Internal Deps:** `../utils/logger` +- **Usage Patterns:** Manage RLS policies for tables. +- **Implementation Details:** Generates RLS policies based on table structure. +- **External Deps:** `chalk`, `drizzle-orm` + +#### [`commands/storage.ts`](packages/cli/src/commands/storage.ts) +**Purpose:** `bb storage` command - storage bucket management. +- **Exports:** Storage bucket management commands +- **Key Functions:** Bucket CRUD operations, policy management +- **Internal Deps:** `../utils/logger`, `../utils/prompts` +- **Usage Patterns:** Manage storage buckets and files. +- **Implementation Details:** Integrates with S3-compatible storage. +- **External Deps:** `chalk`, `inquirer` + +#### [`commands/webhook.ts`](packages/cli/src/commands/webhook.ts) +**Purpose:** `bb webhook` command - webhook management. +- **Exports:** Webhook lifecycle management commands +- **Key Functions:** Webhook creation, testing, logging +- **Internal Deps:** `../utils/logger` +- **Usage Patterns:** Register and manage webhooks for database events. +- **Implementation Details:** Handles webhook registration and event dispatch. +- **External Deps:** `chalk` + +#### [`commands/branch.ts`](packages/cli/src/commands/branch.ts) +**Purpose:** `bb branch` command - Preview Environment management. +- **Exports:** `runBranchCreateCommand`, `runBranchDeleteCommand`, `runBranchListCommand`, `runBranchStatusCommand`, `runBranchWakeCommand`, `runBranchSleepCommand` +- **Key Functions:** + - `runBranchCreateCommand` - Creates a new preview environment + - `runBranchDeleteCommand` - Deletes a preview environment + - `runBranchListCommand` - Lists all preview environments + - `runBranchStatusCommand` - Checks branch status + - `runBranchWakeCommand` - Wakes a sleeping preview + - `runBranchSleepCommand` - Puts a preview to sleep +- **Key Features:** + - `bb branch create ` - Create preview environment + - `bb branch delete ` - Delete preview environment + - `bb branch list` - List all preview environments + - `bb branch status ` - Check branch status + - `bb branch wake ` - Wake sleeping preview + - `bb branch sleep ` - Sleep preview +- **Internal Deps:** `../utils/logger`, `@betterbase/shared`, `@betterbase/core/branching` +- **Usage Patterns:** Manage preview environments for development branches. +- **External Deps:** `chalk` + +#### [`commands/auth-providers.ts`](packages/cli/src/commands/auth-providers.ts) +**Purpose:** `bb auth add-provider` command - OAuth provider management. +- **Exports:** `runAuthProviderCommand(options)` - main function +- **Key Functions:** `addProvider()`, `removeProvider()`, `listProviders()` +- **Supported Providers:** google, github, discord, apple, microsoft, twitter, facebook +- **Internal Deps:** `../utils/logger`, `../utils/prompts` +- **Usage Patterns:** Add or remove OAuth authentication providers. +- **Implementation Details:** Updates BetterAuth configuration with provider credentials. +- **External Deps:** `chalk`, `inquirer` + +#### [`commands/migrate-utils.ts`](packages/cli/src/commands/migrate-utils.ts) +**Purpose:** Migration utilities for the CLI. +- **Exports:** `analyzeMigration()`, `splitStatements()`, `collectChangesFromGenerate()` +- **Key Functions:** Migration analysis and SQL parsing +- **Internal Deps:** `../utils/logger`, `zod` +- **Usage Patterns:** Used by migrate command for migration management. +- **Implementation Details:** Parses SQL files, analyzes changes, supports rollback planning. +- **External Deps:** `zod`, `chalk` + +#### [`commands/rls-test.ts`](packages/cli/src/commands/rls-test.ts) +**Purpose:** `bb rls test` command - Test RLS policies for a table. +- **Exports:** `runRLSTestCommand(projectRoot, tableName)` - main function +- **Key Functions:** `testRLSPolicies()`, `verifyPolicy()`, `simulateQuery()` +- **Internal Deps:** `../utils/logger`, `../utils/schema-scanner` +- **Usage Patterns:** Test RLS policies before deploying to production. +- **Implementation Details:** Simulates queries as different users to verify RLS policy correctness. +- **External Deps:** `chalk`, `drizzle-orm` + +### CLI Utilities + +#### [`utils/logger.ts`](packages/cli/src/utils/logger.ts) +**Purpose:** Colored console logging utilities. +- **Exports:** `info(message)`, `warn(message)`, `error(message)`, `success(message)` +- **Internal Deps:** `chalk` +- **Usage Patterns:** Used throughout CLI commands for consistent, colored output. +- **Implementation Details:** Thin wrapper around Chalk with pre-configured color schemes. Info = cyan, Warn = yellow, Error = red, Success = green. +- **External Deps:** `chalk` + +#### [`utils/prompts.ts`](packages/cli/src/utils/prompts.ts) +**Purpose:** Interactive prompt utilities wrapping Inquirer. +- **Exports:** `text(options)`, `confirm(options)`, `select(options)` +- **Internal Deps:** `inquirer`, `zod` +- **Usage Patterns:** Used by CLI commands that need user input during execution. +- **Implementation Details:** Wraps Inquirer with Zod validation on input. Provides typed promise-based API. +- **External Deps:** `inquirer`, `zod` + +#### [`utils/context-generator.ts`](packages/cli/src/utils/context-generator.ts) +**Purpose:** Generates `.betterbase-context.json` for AI agents. +- **Exports:** `ContextGenerator` - class, `BetterBaseContext` - interface +- **Class Methods:** `generate(projectRoot)` - main method, `generateAIPrompt()` - creates AI-readable prompt +- **Internal Deps:** `./route-scanner`, `./schema-scanner`, `./logger` +- **Usage Patterns:** Called during `bb dev` or `bb generate` to create context file. Used by AI assistants to understand the project structure. +- **Implementation Details:** Scans schema and routes, aggregates metadata, outputs JSON file with tables, routes, and AI-readable prompt. The AI prompt helps contextualize the project for LLM-based development assistance. +- **External Deps:** `typescript`, `zod`, `chalk` +- **Cross-Ref:** [`packages/cli/src/utils/route-scanner.ts`](packages/cli/src/utils/route-scanner.ts), [`packages/cli/src/utils/scanner.ts`](packages/cli/src/utils/scanner.ts) + +#### [`utils/route-scanner.ts`](packages/cli/src/utils/route-scanner.ts) +**Purpose:** Scans Hono routes directory and extracts endpoint metadata. +- **Exports:** `RouteScanner` - class, `RouteInfo` - interface +- **Class Methods:** `scan(routesDir)` - main method, `scanFile()` - parses single file, `findSchemaUsage()` - detects Zod schemas +- **Internal Deps:** `typescript` (TS AST parser) +- **Usage Patterns:** Used by context generator to discover all API endpoints in the project. +- **Implementation Details:** Uses TypeScript compiler API to parse route files. Extracts HTTP method, path, auth requirements, and Zod schemas. Handles Hono's chainable API pattern detection. +- **External Deps:** `typescript` + +#### [`utils/scanner.ts`](packages/cli/src/utils/scanner.ts) +**Purpose:** Scans Drizzle schema files and extracts table metadata. +- **Exports:** `SchemaScanner` - class, `ColumnInfo` - type, `TableInfo` - type, `ColumnInfoSchema`, `TableInfoSchema`, `TablesRecordSchema` - Zod schemas +- **Class Methods:** `scan()` - main method, `parseTable()`, `parseColumn()`, `parseIndexes()` +- **Internal Deps:** `typescript`, `zod`, `./logger` +- **Usage Patterns:** Used by generate command and context generator to understand database schema. +- **Implementation Details:** Parses TypeScript schema files using TypeScript compiler API. Extracts table names, column definitions, relations, indexes. Returns typed metadata for code generation. +- **External Deps:** `typescript`, `zod` + +#### [`utils/schema-scanner.ts`](packages/cli/src/utils/schema-scanner.ts) +**Purpose:** Re-exports from scanner.ts for cleaner imports. +- **Exports:** `SchemaScanner` - class (re-export), `ColumnInfo` - type (re-export), `TableInfo` - type (re-export) +- **Usage Patterns:** Import point for schema scanning functionality. +- **External Deps:** None (re-exports) + +#### [`utils/provider-prompts.ts`](packages/cli/src/utils/provider-prompts.ts) +**Purpose:** Database provider selection prompts. +- **Exports:** Provider selection utilities +- **Usage Patterns:** Used by init command to select database provider. +- **Implementation Details:** Provides interactive selection for database providers (PostgreSQL, MySQL, SQLite). +- **External Deps:** `inquirer` + +### CLI Tests + +#### [`test/smoke.test.ts`](packages/cli/test/smoke.test.ts) +**Purpose:** Basic CLI tests verifying command registration. +- **Tests:** Program name, init argument, generate crud, auth setup, dev, migrate commands +- **Usage Patterns:** Smoke tests run in CI to verify CLI is functional after changes. + +#### [`test/scanner.test.ts`](packages/cli/test/scanner.test.ts) +**Purpose:** Tests for SchemaScanner. +- **Tests:** Extracts tables, columns, relations, indexes from Drizzle schema +- **Usage Patterns:** Unit tests for scanner module. + +#### [`test/context-generator.test.ts`](packages/cli/test/context-generator.test.ts) +**Purpose:** Tests for ContextGenerator. +- **Tests:** Creates context from schema/routes, handles missing routes, empty schema, missing schema +- **Usage Patterns:** Unit tests for context generation. + +#### [`test/route-scanner.test.ts`](packages/cli/test/route-scanner.test.ts) +**Purpose:** Tests for RouteScanner. +- **Tests:** Extracts Hono routes with auth detection and schema usage +- **Usage Patterns:** Unit tests for route scanning. + +--- + +## packages/shared + +`@betterbase/shared` - Shared utilities and types used across all packages. + +### Shared Modules + +#### [`src/types.ts`](packages/shared/src/types.ts) +**Purpose:** Common TypeScript type definitions. +- **Exports:** `BetterBaseResponse`, `ProviderType`, `DatabaseCredentials`, `StorageConfig`, etc. +- **Key Types:** + - `BetterBaseResponse`: Standard response wrapper { data, error } + - `ProviderType`: "postgres" | "mysql" | "sqlite" | "neon" | "turso" | "planetscale" | "supabase" + - `DatabaseCredentials`: Connection configuration interface + - `StorageConfig`: Storage provider configuration + +#### [`src/errors.ts`](packages/shared/src/errors.ts) +**Purpose:** Shared error classes across all packages. +- **Exports:** `BetterBaseError`, `ValidationError`, `DatabaseError`, `AuthError`, `StorageError` +- **Key Features:** All errors extend base Error class with code, status, and details + +#### [`src/constants.ts`](packages/shared/src/constants.ts) +**Purpose:** Shared constants used across the platform. +- **Exports:** `VERSION`, `DEFAULT_PORT`, `DEFAULT_POOL_SIZE`, `DEFAULT_TIMEOUT`, etc. + +#### [`src/utils.ts`](packages/shared/src/utils.ts) +**Purpose:** Utility functions for validation, formatting, etc. +- **Exports:** `validateEmail()`, `formatDate()`, `slugify()`, `generateId()`, etc. + +--- + +## Templates + +BetterBase provides project templates for quick project initialization. + +### templates/base + +The base template with essential project structure. + +- **Path:** `templates/base/` +- **Purpose:** Starting point for new BetterBase projects +- **Includes:** + - Basic Hono server setup + - Database schema with users example + - Authentication middleware + - Storage routes + - Health check endpoint + +### templates/auth + +The authentication template with full BetterAuth integration. + +- **Path:** `templates/auth/` +- **Purpose:** Projects requiring authentication out of the box +- **Includes:** + - Pre-configured BetterAuth setup (`src/auth/index.ts`, `src/auth/types.ts`) + - Email/password authentication + - Social OAuth providers (configurable) + - Session management + - Auth middleware (`src/middleware/auth.ts`) + - Auth routes (`src/routes/auth.ts`, `src/routes/auth-example.ts`) + - Auth schema (`src/db/auth-schema.ts`, `src/db/schema.ts`) + - Drizzle adapter integration (`src/db/index.ts`) + - `betterbase.config.ts`, `drizzle.config.ts`, `package.json` + +### templates/iac + +The IaC project template with Convex-inspired patterns. + +- **Path:** `templates/iac/` +- **Purpose:** Projects using the Infrastructure as Code approach +- **Includes:** + - `betterbase/schema.ts` - Schema definition + - `betterbase/queries/` - Query functions + - `betterbase/mutations/` - Mutation functions + - `betterbase/actions/` - Action functions + - `betterbase/cron.ts` - Scheduled functions + - `betterbase.config.ts` - Project configuration + +--- + +## Usage Examples + +### Client SDK + +```typescript +import { createClient } from '@betterbase/client'; + +const client = createClient({ + url: 'http://localhost:3000', + key: 'your-api-key', +}); + +// Authenticate user +const { data, error } = await client.auth.signIn('user@example.com', 'password123'); + +if (error) { + console.error('Sign in failed:', error); +} else { + console.log('User signed in:', data?.user); +} + +// Query data +const users = await client.from('users').select('*').execute(); +console.log('Users:', users); + +// Upload file +const file = new File(['hello world'], 'test.txt', { type: 'text/plain' }); +const uploadResult = await client.storage.from('bucket').upload('test.txt', file); +console.log('Upload result:', uploadResult); + +// Subscribe to realtime updates +const subscription = client.realtime.from('posts').on('INSERT', (payload) => { + console.log('New post:', payload); +}).subscribe(); + +// Cleanup subscription +subscription.unsubscribe(); +``` + +### Server-Side with Hono + +```typescript +import { Hono } from 'hono'; +import { eq } from 'drizzle-orm'; +import { auth } from './auth'; +import { db } from './db'; +import { users } from './db/schema'; + +const app = new Hono(); + +// Protected route +app.get('/api/protected', async (c) => { + const session = await auth.api.getSession({ headers: c.req.raw.headers }); + if (!session) { + return c.json({ error: 'Unauthorized' }, 401); + } + + const userPosts = await db.select().from(users) + .where(eq(users.id, session.user.id)); + + return c.json(userPosts); +}); + +export default app; +``` + +### RLS Policy Definition + +```typescript +// src/db/policies/users.policy.ts +import { definePolicy } from '@betterbase/core/rls'; + +export default definePolicy('users', { + select: 'auth.uid() = id', + update: 'auth.uid() = id', + delete: 'auth.uid() = id', + insert: 'auth.uid() = id', +}); +``` + +--- + +## Architecture Decisions + +### Authentication +- **Choice:** BetterAuth for password and social authentication +- **Rationale:** Lightweight, extensible, and compatible with Drizzle ORM + +### Database +- **Choice:** Drizzle ORM for database abstraction +- **Rationale:** Type-safe, composable, and supports multiple dialects + +### Storage +- **Choice:** S3-compatible storage with AWS SDK v3 +- **Rationale:** Wide support, compatibility with multiple providers + +### Realtime +- **Choice:** WebSocket-based realtime using Bun's WebSocket API +- **Rationale:** Fast, lightweight, and built into Bun runtime + +### GraphQL +- **Choice:** graphql-yoga for server, GraphQL.js for schema +- **Rationale:** Simple setup, good integration with Hono + +### Validation +- **Choice:** Zod for schema validation +- **Rationale:** Type-safe, easy to use, and integrates well with TypeScript + +### CLI +- **Choice:** Commander.js for CLI framework +- **Rationale:** Mature, lightweight, and well-documented + +--- + +## Development Workflow + +### Creating a New Project + +```bash +# Initialize new project +bb init + +# Answer prompts for project name, database provider, etc. + +# Navigate to project directory +cd my-project + +# Install dependencies +bun install + +# Start development server +bun run dev +``` + +### Adding Authentication + +```bash +# Add BetterAuth integration +bb auth setup + +# Run database migrations +bun run db:push + +# Set auth secret in .env +echo "AUTH_SECRET=your-secret-key" >> .env +``` + +### Generating CRUD Routes + +```bash +# Generate CRUD routes for a table +bb generate crud posts + +# Run GraphQL schema generation +bb graphql generate +``` + +### Migrating Database + +```bash +# Generate and apply migrations +bb migrate + +# Preview migration without applying +bb migrate preview + +# Apply migrations to production +bb migrate production +``` + +--- + +## API Reference + +### Client SDK + +#### `createClient(config)` +```typescript +const client = createClient({ + url: string; + key?: string; + schema?: string; + fetch?: typeof fetch; + storage?: StorageAdapter; +}); +``` + +#### `client.auth` +```typescript +client.auth.signUp(email: string, password: string, name: string): Promise>; +client.auth.signIn(email: string, password: string): Promise>; +client.auth.signOut(): Promise>; +client.auth.getSession(): Promise>; +client.auth.getToken(): string | null; +client.auth.setToken(token: string | null): void; +``` + +#### `client.from(table)` +```typescript +const query = client.from('users'); +query.select(fields?: string): this; +query.eq(column: string, value: unknown): this; +query.in(column: string, values: unknown[]): this; +query.limit(count: number): this; +query.offset(count: number): this; +query.order(column: string, direction?: 'asc' | 'desc'): this; +query.execute(): Promise>; +query.single(id: string): Promise>; +query.insert(data: Partial): Promise>; +query.update(id: string, data: Partial): Promise>; +query.delete(id: string): Promise>; +``` + +#### `client.realtime` +```typescript +client.realtime.from(table: string): { + on: ( + event: RealtimeEvent, + callback: RealtimeCallback, + ) => { + subscribe: (filter?: Record) => RealtimeSubscription; + }; +}; +``` + +#### `client.storage` +```typescript +client.storage.from(bucket: string): StorageBucketClient; +``` + +#### `StorageBucketClient` +```typescript +bucket.upload(path: string, file: File | Blob | ArrayBuffer, options?: UploadOptions): Promise>; +bucket.download(path: string): Promise>; +bucket.getPublicUrl(path: string): Promise>; +bucket.createSignedUrl(path: string, options?: SignedUrlOptions): Promise>; +bucket.remove(paths: string[]): Promise>; +bucket.list(prefix?: string): Promise>; +``` + +--- + +## Server-Side API + +### Hono App + +```typescript +import { Hono } from 'hono'; + +const app = new Hono(); + +// Health check +app.get('/health', (c) => { + return c.json({ status: 'ok' }); +}); + +// Protected route +app.get('/protected', requireAuth, (c) => { + const user = c.get('user'); + return c.json({ user }); +}); + +// Error handler +app.onError((err, c) => { + return c.json({ error: err.message }, 500); +}); + +export default app; +``` + +### Auth Middleware + +```typescript +import { requireAuth, optionalAuth } from './middleware/auth'; + +// Example 1: Require authentication for all routes +app.use('*', requireAuth); + +// Example 2: Optional authentication (mutually exclusive - use one or the other) +// app.use('*', optionalAuth); + +// Get user from context +const user = c.get('user'); +``` + +> **Note:** `requireAuth` and `optionalAuth` are mutually exclusive choices for route protection. Use `app.use('*', requireAuth)` for mandatory authentication, or `app.use('*', optionalAuth)` for optional authentication. + +### Realtime Broadcast + +```typescript +import { realtime } from './lib/realtime'; + +// Broadcast event +realtime.broadcast('posts', 'INSERT', { + id: '1', + title: 'New Post', + content: 'Hello World', + createdAt: new Date(), +}); +``` + +--- + +## Configuration + +### Project Configuration (`betterbase.config.ts`) + +```typescript +import { defineConfig } from '@betterbase/core'; + +export default defineConfig({ + project: { + name: 'my-project', + }, + provider: { + type: 'neon', + connectionString: process.env.DATABASE_URL, + }, + storage: { + provider: 's3', + bucket: 'my-bucket', + region: 'us-east-1', + accessKeyId: process.env.STORAGE_ACCESS_KEY_ID, + secretAccessKey: process.env.STORAGE_SECRET_ACCESS_KEY, + }, + webhooks: [ + { + id: 'new-post', + table: 'posts', + events: ['INSERT'], + url: process.env.WEBHOOK_URL, + secret: process.env.WEBHOOK_SECRET, + enabled: true, + }, + ], + graphql: { + enabled: true, + }, +}); +``` + +### Environment Variables + +```bash +# Database +DATABASE_URL="postgres://user:password@localhost:5432/mydb" +TURSO_URL="https://mydb.turso.io" +TURSO_AUTH_TOKEN="my-turso-token" + +# Auth +AUTH_SECRET="your-auth-secret" +AUTH_URL="http://localhost:3000" + +# Storage +STORAGE_PROVIDER="s3" +STORAGE_BUCKET="my-bucket" +STORAGE_REGION="us-east-1" +STORAGE_ACCESS_KEY_ID="my-access-key" +STORAGE_SECRET_ACCESS_KEY="my-secret-key" + +# Webhooks +WEBHOOK_URL="https://example.com/webhook" +WEBHOOK_SECRET="my-webhook-secret" + +# Server +PORT=3000 +NODE_ENV="development" +``` + +--- + +## Testing + +### Running Tests + +```bash +# Run all tests +bun test + +# Run tests in watch mode +bun test --watch + +# Run specific test file +bun test packages/cli/test/smoke.test.ts +``` + +### Test Structure + +Tests are located in the `test/` directory of each package. The test files follow the pattern `*.test.ts`. + +--- + +## Contributing + +### Development Setup + +```bash +# Clone repository +git clone +cd betterbase + +# Install dependencies +bun install + +# Build packages +bun run build + +# Run tests +bun test +``` + +### Development Workflow + +1. Create a new branch +2. Make changes to the codebase +3. Run tests +4. Commit changes +5. Push to remote repository +6. Create a pull request + +--- + +## Changelog + +All notable changes to BetterBase will be documented in this file. + +### Recent Updates (2026-03-30) + +#### Documentation Updates +- **CODEBASE_MAP.md**: Updated with new IaC error classes, generators, docs directory, and IaC template + +#### New Modules Added +- **IaC Errors**: Added `iac/errors.ts` with error classes (IaCError, ValidationError, DatabaseError, AuthError, NotFoundError) and formatError utility +- **IaC Generators**: Added code generators (drizzle-schema-gen.ts, migration-gen.ts, api-typegen.ts) +- **Documentation**: Added docs/ directory with comprehensive documentation structure +- **IaC Template**: Added `templates/iac/` for Convex-inspired IaC pattern projects + +#### Package Updates +- **packages/cli**: 30+ test files, 9 utility modules +- **packages/client**: IaC client with hooks, vanilla client, paginated queries, embeddings +- **packages/server**: Enhanced admin routes with roles, notifications, SMTP, API keys, CLI sessions, Inngest integration +- **apps/test-project**: Example project demonstrating all features + +--- + +## License + +BetterBase is released under the MIT license. + diff --git a/CONTRIBUTING.md b/specs/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to specs/CONTRIBUTING.md diff --git a/specs/CodeRabbit_Full_Codebase_review.md b/specs/CodeRabbit_Full_Codebase_review.md new file mode 100644 index 0000000..b98a89e --- /dev/null +++ b/specs/CodeRabbit_Full_Codebase_review.md @@ -0,0 +1,894 @@ +## chore: trigger CodeRabbit review - dashboard pages (4/7) #13 + +```text +Verify each finding against the current code and only fix it if needed. + +Inline comments: +In `@apps/dashboard/src/pages/projects/ProjectAuthPage.tsx`: +- Line 129: Remove the trailing whitespace at the end of the line inside the +ProjectAuthPage component (file ProjectAuthPage.tsx) — open the ProjectAuthPage +function/component and delete any extra space characters at the end of line 129 +(or run your editor/formatter to trim trailing whitespace) so the file has no +trailing whitespace on that line. + +In `@apps/dashboard/src/pages/projects/ProjectEnvPage.tsx`: +- Line 170: The file ProjectEnvPage.tsx contains trailing whitespace on the line +within the ProjectEnvPage component; remove the extra space character(s) at the +end of that line (ensure no trailing spaces remain in the JSX/TSX file) so the +file has no trailing whitespace. + +In `@apps/dashboard/src/pages/projects/ProjectIaCQueryPage.tsx`: +- Line 170: The file ProjectIaCQueryPage.tsx contains trailing whitespace at +EOF; remove the extra blank spaces/newline characters at the end of the file so +the file ends cleanly with no trailing whitespace, then re-run your +formatter/linter (e.g., Prettier/ESLint or your editor's trim trailing +whitespace setting) to ensure this is not reintroduced; verify by opening +ProjectIaCQueryPage.tsx and deleting the trailing spaces or saving with "trim +trailing whitespace" enabled. + +In `@apps/dashboard/src/pages/settings/SettingsPage.tsx`: +- Line 316: Remove the trailing whitespace at the end of the file by editing +SettingsPage.tsx: locate the final closing brace of the SettingsPage component +(the lone "}" shown in the diff) or the file end and delete any trailing +spaces/newline characters after it so the file ends cleanly with the closing +brace (or a single newline) and no extra whitespace. + +--- + +Outside diff comments: +In `@apps/dashboard/src/pages/projects/ProjectAuthPage.tsx`: +- Around line 25-31: updateMutation currently doesn't invalidate the cached +project auth query so the UI can remain stale; import and call useQueryClient(), +get queryClient in the component, and in updateMutation's onSuccess handler call +queryClient.invalidateQueries(QK.projectAuthConfig(projectId)) (keep the +existing toast.success call) so the cache for QK.projectAuthConfig(projectId) is +refreshed after the mutation. +- Around line 18-121: Replace the local useState config form in ProjectAuthPage +with React Hook Form + Zod: remove const [config, setConfig] and instead create +a zod schema for the auth config (fields like google_enabled, github_enabled, +discord_enabled, twitter_enabled, password_enabled, magic_link_enabled, +session_days), use useForm({ resolver: zodResolver(schema), defaultValues: +authConfig }) and populate defaults from data?.config; replace direct +value/checked props on Switch and Input with react-hook-form bindings—use +Controller for controlled components (Switch, numeric Input) and register for +simple fields; wire form submission/update to call updateMutation.mutate(values) +(or call updateMutation.mutate on controlled value changes via handleSubmit) and +remove setConfig calls; ensure validation/parsing of session_days via the schema +and show validation errors similarly to SettingsPage.tsx. + +In `@apps/dashboard/src/pages/projects/ProjectEnvPage.tsx`: +- Around line 28-29: The add-variable form in ProjectEnvPage currently uses +local state variables newKey/newValue (setNewKey/setNewValue) and manual +handlers; replace this with React Hook Form and a Zod resolver: create a Zod +schema for the variable shape (e.g., key pattern and required value), initialize +useForm({ resolver: zodResolver(schema) }) inside the ProjectEnvPage component, +remove useState declarations for newKey/newValue and their setters, register the +inputs with form.register("key") and form.register("value"), replace the submit +handler to use handleSubmit and read data from the form values, and surface +validation errors via formState.errors so the UI shows consistent validation +messages. Ensure the same submit logic (the function currently called to add the +variable) is invoked with the validated form data and reset the form via reset() +after successful submission. + +In `@apps/dashboard/src/pages/projects/ProjectIaCQueryPage.tsx`: +- Around line 28-42: The mutation queryMutation currently allows non-SELECT SQL +and may mutate project data without causing cache refresh; update the onSuccess +handler to invalidate relevant cached queries (e.g., call the react-query client +to invalidate QK.project(projectId) and any other project-related keys) so UI +reflects changes, or alternatively enforce SELECT-only SQL at the API layer; +locate useMutation's onSuccess in ProjectIaCQueryPage and add the +invalidateQueries call(s) for QK.project(projectId) (and related keys) to ensure +cached project data is refreshed after a successful mutation. + +In `@apps/dashboard/src/pages/settings/SettingsPage.tsx`: +- Around line 273-312: Replace the custom modal markup that is conditionally +rendered when showResetDialog is true with the shared ConfirmDialog component: +use ConfirmDialog (instead of the wrapper div and inner content) wired to +open={showResetDialog} and onOpenChange={setShowResetDialog}, pass a +destructive/variant prop, the title "Factory Reset", the descriptive body text, +and set the confirmation input by connecting ConfirmDialog's confirm value to +resetConfirm and its onConfirmChange to setResetConfirm; wire the confirm action +to call resetMutation.mutate and set the confirm button disabled state to match +the existing logic (resetConfirm !== instanceData?.instance?.name || +resetMutation.isPending) and show the pending label when +resetMutation.isPending. Ensure you pass requireTyping (or add support for it to +ConfirmDialog) so the dialog requires typing instanceData?.instance?.name to +enable the destructive action. + +``` + + +--------- + +## chore: trigger CodeRabbit review - config files & Dockerfiles (7/7)#10 + + +```txt + +
+🧩 Analysis chain + +`@Helal-maker` — triggering the review now. While that runs, let me pull the PR diff to analyze the files and draft the agent prompt. + + + + + + +--- + +🏁 Script executed: +``` +#!/bin/bash +# Get PR details, files changed, and diff +gh pr view 10 --repo Helal-maker/Betterbase --json title,body,files,additions,deletions,changedFiles +echo "---FILES---" +gh pr diff 10 --repo Helal-maker/Betterbase +``` + +Length of output: 7093 + +--- + +The diff is pure whitespace — need to read the actual file content to find real issues. + +--- + +🏁 Script executed: +``` +#!/bin/bash +echo "=== .coderabbit.yaml ===" && cat .coderabbit.yaml +echo "=== .github/workflows/ci.yml ===" && cat .github/workflows/ci.yml +echo "=== Dockerfile ===" && cat Dockerfile +echo "=== Dockerfile.project ===" && cat Dockerfile.project +echo "=== apps/dashboard/Dockerfile ===" && cat apps/dashboard/Dockerfile +echo "=== apps/dashboard/src/vite-env.d.ts ===" && cat apps/dashboard/src/vite-env.d.ts +echo "=== docker-compose.dev.yml ===" && cat docker-compose.dev.yml +echo "=== docker-compose.production.yml ===" && cat docker-compose.production.yml +echo "=== docker-compose.self-hosted.yml ===" && cat docker-compose.self-hosted.yml +echo "=== docker-compose.yml ===" && cat docker-compose.yml +echo "=== packages/cli/src/build.ts ===" && cat packages/cli/src/build.ts +echo "=== packages/cli/src/constants.ts ===" && cat packages/cli/src/constants.ts +echo "=== packages/cli/src/index.ts ===" && cat packages/cli/src/index.ts +echo "=== packages/client/src/iac/provider.tsx ===" && cat packages/client/src/iac/provider.tsx +echo "=== packages/server/Dockerfile ===" && cat packages/server/Dockerfile +``` + +Length of output: 74631 + +--- + + + +
+ +`@Helal-maker` — review triggered. Here is the full analysis and agent prompt based on the actual bugs found in the codebase. + +--- + +## Issues Found + +### 🔴 Critical + +**`packages/cli/src/index.ts` — `init` command action arguments are swapped** + +Commander.js passes positional arguments _before_ the options object. The current handler: +```ts +.action(async (options: { iac?: boolean }, projectName?: string) => { + await runInitCommand({ projectName, ...options }); +}) +``` +…receives `(projectNameString, optionsObject, command)`. The types are reversed: `options` receives the string `"my-app"`, and `projectName` receives the Commander options object. `runInitCommand` gets `{ projectName: { iac: false }, iac: undefined }`. **`bb init my-app` is broken** — project name is always `undefined`. + +Fix: +```ts +.action(async (projectName: string | undefined, options: { iac?: boolean }) => { + await runInitCommand({ projectName, ...options }); +}) +``` + +--- + +### 🟠 Major + +**1. `packages/cli/src/index.ts` — `PUBLIC_COMMANDS` missing raw flag strings** + +`checkAuthHook` reads `process.argv[2]` literally. `bb --version` → `process.argv[2]` is `"--version"`, not `"version"`. `PUBLIC_COMMANDS` contains `"version"` and `"help"` but not `"--version"`, `"-v"`, `"--help"`, `"-h"`. Unauthenticated users running `bb --version` or `bb --help` get "Not logged in." + +Fix: +```ts +const PUBLIC_COMMANDS = ["login", "logout", "version", "help", "init", "--version", "-v", "--help", "-h", "-V"]; +``` + +--- + +**2. `packages/client/src/iac/provider.tsx` — WebSocket reconnect creates a dead socket** + +The `ws.onclose` handler creates a new `WebSocket` in a `setTimeout` but attaches **none** of the event handlers (`onopen`, `onclose`, `onmessage`) to the new socket. The reconnected socket never calls `setWsReady(true)`, never handles pings, and never reconnects again if it closes. + +Additionally, the cleanup function `return () => { ws.close() }` does not cancel the `setTimeout`, so if the component unmounts while the timer is pending, a new socket is still created and a post-unmount state update fires. + +Fix: +```ts +useEffect(() => { + let timeoutId: ReturnType | null = null; + let isCleaned = false; + + function connect() { + if (isCleaned) return; + const wsUrl = `${config.url.replace(/^http/, "ws")}/betterbase/ws?project=${config.projectSlug ?? "default"}`; + const ws = new WebSocket(wsUrl); + wsRef.current = ws; + + ws.onopen = () => { if (!isCleaned) setWsReady(true); }; + ws.onclose = () => { + if (isCleaned) return; + setWsReady(false); + timeoutId = setTimeout(connect, 3_000); + }; + ws.onmessage = (event) => { + const msg = JSON.parse(event.data); + if (msg.type === "ping") ws.send(JSON.stringify({ type: "pong" })); + }; + } + + connect(); + + return () => { + isCleaned = true; + if (timeoutId !== null) clearTimeout(timeoutId); + wsRef.current?.close(); + }; +}, [config.url, config.projectSlug]); +``` + +--- + +**3. `packages/cli/src/index.ts` — `branch` default action reads wrong parameter** + +```ts +branch + .argument("[project-root]", "project root directory", process.cwd()) + .option("-p, --project-root ", ...) + .action(async (options) => { // ← "options" is actually the arg string + const projectRoot = options.projectRoot || process.cwd(); // always cwd() + }); +``` +Commander passes `(argValue, optionsObject, command)`. Fix: +```ts +.action(async (projectRootArg: string, options: { projectRoot?: string }) => { + const projectRoot = projectRootArg || options.projectRoot || process.cwd(); + await runBranchCommand([], projectRoot); +}) +``` + +--- + +**4. `packages/cli/src/index.ts` — `fn deploy` same class of bug** + +```ts +fn.command("deploy") + .argument("", "function name") + .option("--sync-env", ...) + .argument("[project-root]", ...) + .action(async (name: string, options: { syncEnv?: boolean; projectRoot?: string }) => { + const projectRoot = options.projectRoot ?? process.cwd(); // always cwd() + }); +``` +Commander passes `(name, projectRoot, options, command)`. Fix: +```ts +.action(async (name: string, projectRootArg: string, options: { syncEnv?: boolean }) => { + const projectRoot = projectRootArg ?? process.cwd(); + await runFunctionCommand(["deploy", name, options.syncEnv ? "--sync-env" : ""], projectRoot); +}) +``` + +--- + +**5. `Dockerfile.project` — `deps` stage is dead code** + +The `builder` stage starts `FROM base AS builder` and re-runs `bun install --frozen-lockfile` independently. The `deps` stage is never referenced with `COPY --from=deps`. Every build re-installs all deps from scratch, defeating Docker layer caching. + +Fix — builder should use deps: +```dockerfile +FROM deps AS builder +WORKDIR /app +# deps already installed, just copy source +COPY . . +RUN bun run build +``` + +--- + +**6. `docker-compose.self-hosted.yml` — nginx healthcheck is wrong** + +```yaml +healthcheck: + test: ["CMD", "nginx", "-t"] # tests config syntax, not live HTTP +``` +`nginx -t` passes even if nginx isn't bound to port 80. Replace: +```yaml +healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 +``` + +--- + +**7. `docker-compose.yml` — unpinned `minio:latest` and `mc:latest`** + +Production and self-hosted compose files pin MinIO to `RELEASE.2024-11-07T19-31-41Z`. Dev uses `latest`. This causes environment divergence. Pin to the same versions used in production. + +--- + +### 🔵 Minor + +**8. `apps/dashboard/src/vite-env.d.ts` — `VITE_API_URL` typed as optional** + +Per `.coderabbit.yaml`, `VITE_API_URL` is the only env var and it's required. Typing it `string?` means the compiler won't flag missing runtime values. Change to `readonly VITE_API_URL: string`. + +**9. `.github/workflows/ci.yml` — `bun-version: latest` not reproducible** + +All three jobs use `latest`. A Bun release could silently break CI. Pin to a specific version (e.g., `1.3.9` to match the Dockerfiles) and add a `cache: bun` step. + +**10. `docker-compose.dev.yml` — deprecated `version` field** + +`version: "3.9"` is obsolete in Docker Compose v2. Remove the field entirely. + +--- + +## 🤖 Agent Prompt (paste this to Kilo Code) + +``` +## Kilo Code — BetterBase Bug Fix Pass + +You are Kilo Code operating on the BetterBase monorepo. Fix ALL issues listed below +in a single orchestrated pass. Do not skip minor issues. + +### Runtime/Stack +- Bun (use process.execPath in spawn, never "bun" string) +- Hono (packages/server), React Router v7 (apps/dashboard), Drizzle ORM +- BetterAuth, JWT HS256, bcrypt rounds=12 + +### Critical Invariants — do NOT break while fixing +1. /admin/auth/setup returns 410 if any admin exists +2. Admin keys are SHA-256 hashed; plaintext returned once only +3. audit_log has no update/delete routes +4. logWebhookDelivery must be awaited +5. Request logging and audit log writes are fire-and-forget +6. Dashboard colors use CSS variables only +7. Dashboard API calls go through src/lib/api.ts only +8. Query keys come from QK factory in src/lib/query-keys.ts +9. Destructive UI actions require ConfirmDialog +10. PUBLIC_COMMANDS must contain: ["login", "logout", "init", "--version", "-v", "--help", "-h", "-V"] + +--- + +### CRITICAL — Fix First + +#### [C-1] packages/cli/src/index.ts — init command action args swapped +File: packages/cli/src/index.ts +The `init` command's `.action()` handler has argument types reversed. +Commander passes (positionalArg, optionsObject, command). Current code has them swapped. +Fix: + WRONG: async (options: { iac?: boolean }, projectName?: string) + RIGHT: async (projectName: string | undefined, options: { iac?: boolean }) +After fix, verify: runInitCommand receives the actual project name string, not an object. + +--- + +### MAJOR — Fix Before Merge + +#### [M-1] packages/cli/src/index.ts — PUBLIC_COMMANDS missing raw flag strings +checkAuthHook reads process.argv[2] literally. "bb --version" has argv[2]="--version". +Add all flag variants: + const PUBLIC_COMMANDS = ["login", "logout", "version", "help", "init", + "--version", "-v", "--help", "-h", "-V"]; + +#### [M-2] packages/client/src/iac/provider.tsx — WebSocket reconnect is broken +In ws.onclose, a new WebSocket is created via setTimeout but has NO event handlers +(no onopen, onclose, onmessage). The reconnected socket never calls setWsReady(true), +never pings back, and never reconnects again if it drops. +Additionally, the cleanup function does not cancel the setTimeout, causing a post-unmount +state update and a leaked socket. + +Fix: Extract a connect() function that self-references in its own onclose. Track the +timeout ID and clear it in cleanup. Set isCleaned=true in cleanup to guard all callbacks. + +Full corrected useEffect: +```ts +useEffect(() => { + let timeoutId: ReturnType | null = null; + let isCleaned = false; + + function connect() { + if (isCleaned) return; + const wsUrl = `${config.url.replace(/^http/, "ws")}/betterbase/ws?project=${config.projectSlug ?? "default"}`; + const ws = new WebSocket(wsUrl); + wsRef.current = ws; + ws.onopen = () => { if (!isCleaned) setWsReady(true); }; + ws.onclose = () => { + if (isCleaned) return; + setWsReady(false); + timeoutId = setTimeout(connect, 3_000); + }; + ws.onmessage = (event) => { + const msg = JSON.parse(event.data); + if (msg.type === "ping") ws.send(JSON.stringify({ type: "pong" })); + }; + } + + connect(); + return () => { + isCleaned = true; + if (timeoutId !== null) clearTimeout(timeoutId); + wsRef.current?.close(); + }; +}, [config.url, config.projectSlug]); +``` + +#### [M-3] packages/cli/src/index.ts — branch default action reads wrong parameter +Commander passes (argValue, optionsObject, command). +Current: .action(async (options) => { options.projectRoot ... }) — options is the arg string. +Fix: + .action(async (projectRootArg: string, options: { projectRoot?: string }) => { + const projectRoot = projectRootArg || options.projectRoot || process.cwd(); + await runBranchCommand([], projectRoot); + }) + +#### [M-4] packages/cli/src/index.ts — fn deploy action reads wrong parameter +Same class of bug. fn deploy has arguments (name, projectRoot) + options. +Current: .action(async (name, options) => { options.projectRoot ... }) — always undefined. +Fix: + .action(async (name: string, projectRootArg: string, options: { syncEnv?: boolean }) => { + const projectRoot = projectRootArg ?? process.cwd(); + await runFunctionCommand(["deploy", name, options.syncEnv ? "--sync-env" : ""], projectRoot); + }) + +#### [M-5] Dockerfile.project — deps stage is unused dead code +The builder stage starts FROM base and re-runs bun install independently. +The deps stage is never referenced with COPY --from=deps. +Fix: Change builder to start FROM deps AS builder and remove the redundant +bun install line in the builder stage. + +#### [M-6] docker-compose.self-hosted.yml — nginx healthcheck wrong +Current: test: ["CMD", "nginx", "-t"] +This checks config syntax only, not whether nginx is actually serving HTTP. +Fix: + healthcheck: + test: ["CMD", "wget", "-qO-", "http://localhost/health"] + interval: 30s + timeout: 10s + retries: 3 + +#### [M-7] docker-compose.yml — unpinned minio:latest and mc:latest +minio and minio-init use :latest. All other compose files pin to +RELEASE.2024-11-07T19-31-41Z and RELEASE.2024-11-08T03-47-05Z respectively. +Fix: pin to the same versions used in docker-compose.production.yml and +docker-compose.self-hosted.yml. + +--- + +### MINOR — Fix in Same Pass + +#### [m-1] apps/dashboard/src/vite-env.d.ts — VITE_API_URL typed as optional +VITE_API_URL is the only env var and is required at runtime. Change: + readonly VITE_API_URL?: string → readonly VITE_API_URL: string + +#### [m-2] .github/workflows/ci.yml — bun-version: latest not reproducible +All 3 jobs use bun-version: latest. Pin to 1.3.9 (matches all Dockerfiles). +Also add caching: + - uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.3.9" + # After checkout, before install: + - uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lock') }} + +#### [m-3] docker-compose.dev.yml — deprecated version field +Remove the `version: "3.9"` top-level field. It is ignored by Docker Compose v2 +and generates a deprecation warning. + +--- + +### Verification Steps (run after all fixes) +1. Confirm PUBLIC_COMMANDS contains all 10 entries listed in invariant `#10` above +2. Trace init, branch (default action), and fn deploy in packages/cli/src/index.ts — + verify each .action() parameter order matches Commander's (args..., options, command) convention +3. Inspect the WebSocket useEffect — confirm isCleaned guard, clearTimeout, and + all three handlers (onopen, onclose, onmessage) are wired on every connect() call +4. In Dockerfile.project, confirm `builder` stage uses `FROM deps AS builder` + and does NOT have a second `bun install` line +5. In docker-compose.self-hosted.yml, confirm nginx healthcheck hits localhost HTTP +6. In docker-compose.yml, confirm both minio and minio-init use pinned image tags +7. Run: bun typecheck on packages/cli and packages/client + +### Fix Summary Format +After completing all fixes, output: + [C-1] file:line — what was wrong — what was changed + [M-1] file:line — what was wrong — what was changed + ... (one line per issue) +``` + +
+✅ Actions performed + +Review triggered. + +> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused. + +
+ +``` +------ + + +chore: trigger CodeRabbit review - dashboard components (5/7) #12 + + +```txt + +`@Helal-maker` — Review complete. Here's the full picture: + +--- + +## Review Summary + +This PR contains **no functional changes** — only trailing blank lines appended to 30+ component files. There are **no bugs, errors, logic issues, type errors, or security vulnerabilities** to fix. No exported APIs, component logic, or control flow were modified in any file. + +--- + +## Agent AI Prompt + +Since there are no bugs in this PR, the agent prompt below is a **template** based on the actual findings (none). You can reuse this format for future PRs where real issues exist: + +--- + +``` +You are a senior TypeScript/React engineer. Your task is to fix the following bugs +and issues found in the codebase during a code review. + +## Repository Context +- Framework: React + TypeScript +- App path: apps/dashboard/src/ +- Component paths: apps/dashboard/src/components/ + +## Bugs & Issues Found +> NONE — This PR contains only trailing whitespace changes across the following files: +> - CommandPalette.tsx, ErrorBoundary.tsx, LiveLogStream.tsx +> - auth/AuthGuard.tsx, auth/SetupGuard.tsx +> - ui/Avatar.tsx, ui/ConfirmDialog.tsx, ui/EmptyState.tsx, ui/PageHeader.tsx, +> ui/PageSkeleton.tsx, ui/StatCard.tsx +> - ui/alert-dialog.tsx, ui/badge.tsx, ui/button.tsx, ui/card.tsx, +> ui/collapsible.tsx, ui/dialog.tsx, ui/dropdown-menu.tsx, ui/input.tsx, +> ui/label.tsx, ui/popover.tsx, ui/progress.tsx, ui/scroll-area.tsx, +> ui/select.tsx, ui/separator.tsx, ui/sheet.tsx, ui/skeleton.tsx, +> ui/switch.tsx, ui/table.tsx, ui/tabs.tsx, ui/textarea.tsx, ui/tooltip.tsx +> +> No action required for this PR. + +## Instructions for Agent +1. Read each file listed under "Bugs & Issues Found". +2. Apply only the minimal change needed to fix the described issue. +3. Do NOT refactor unrelated code. +4. Do NOT change exported function signatures or component props. +5. After each fix, run TypeScript type checking to confirm no new errors are introduced. +6. Output a unified diff for each changed file. +``` + +--- + +For future PRs with real issues, I'll populate the `## Bugs & Issues Found` section with structured entries like: + +``` +### Bug 1 — [File path] +- **Line**: 42 +- **Issue**: [Description of the problem] +- **Fix**: [Exact change to make] +- **Severity**: Critical / High / Medium / Low +``` + +
+✅ Actions performed + +Review triggered. + +> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused. + +
+```` + +----- + + + +chore: trigger CodeRabbit review - dashboard hooks, lib, layouts (6/7) +#11 + + + +```txt +Verify each finding against the current code and only fix it if needed. + +Outside diff comments: +In `@apps/dashboard/src/layouts/AppLayout.tsx`: +- Around line 66-73: The component is assigning window.onkeydown during render +which reassigns global handlers; move this logic into a useEffect inside +AppLayout: create a keydown handler function that checks (e.metaKey || +e.ctrlKey) && e.key === "k" and calls e.preventDefault() then setCmdOpen(true), +register it with window.addEventListener("keydown", handler) in the effect, and +remove it in the cleanup with window.removeEventListener("keydown", handler); do +not assign window.onkeydown directly and include setCmdOpen (or no deps if using +the stable setter) in the effect dependencies to ensure correct lifecycle. +``` + +------ + +chore: trigger CodeRabbit review - CLI utils & shared (2/7) +#8 + + + +```txt + +No actionable comments were generated in the recent review. 🎉 + +
+ℹ️ Recent review info + +
+⚙️ Run configuration + +**Configuration used**: Path: .coderabbit.yaml + +**Review profile**: ASSERTIVE + +**Plan**: Pro + +**Run ID**: `0bbdc724-6ed1-4028-bf7e-ed6c84e471fa` + +
+ +
+📥 Commits + +Reviewing files that changed from the base of the PR and between 3ebfdf25598c83da4874c165dad32949363a6e7c and 959a29be2e1f4a99867c8b2e32b08a3da9fb9988. + +
+ +
+📒 Files selected for processing (19) + +* `packages/cli/src/utils/api-client.ts` +* `packages/cli/src/utils/context-generator.ts` +* `packages/cli/src/utils/credentials.ts` +* `packages/cli/src/utils/logger.ts` +* `packages/cli/src/utils/prompts.ts` +* `packages/cli/src/utils/provider-prompts.ts` +* `packages/cli/src/utils/route-scanner.ts` +* `packages/cli/src/utils/scanner.ts` +* `packages/cli/src/utils/schema-scanner.ts` +* `packages/cli/src/utils/spinner.ts` +* `packages/shared/src/constants.ts` +* `packages/shared/src/errors.ts` +* `packages/shared/src/index.ts` +* `packages/shared/src/types.ts` +* `packages/shared/src/utils.ts` +* `templates/iac/betterbase/cron.ts` +* `templates/iac/betterbase/mutations/todos.ts` +* `templates/iac/betterbase/queries/todos.ts` +* `templates/iac/betterbase/schema.ts` + +
+ +
+ +--- + + + + +## Walkthrough + +This PR contains whitespace-only edits and minor reformatting across 19 files in CLI utilities, shared modules, and template examples. No functional logic, control flow, types, exported APIs, or runtime behavior were changed. + +## Changes + +|Cohort / File(s)|Summary| +|---|---| +|**CLI Utilities**
`packages/cli/src/utils/api-client.ts`, `packages/cli/src/utils/context-generator.ts`, `packages/cli/src/utils/credentials.ts`, `packages/cli/src/utils/logger.ts`, `packages/cli/src/utils/prompts.ts`, `packages/cli/src/utils/provider-prompts.ts`, `packages/cli/src/utils/route-scanner.ts`, `packages/cli/src/utils/scanner.ts`, `packages/cli/src/utils/schema-scanner.ts`, `packages/cli/src/utils/spinner.ts`|Added trailing blank lines to 10 utility files; no changes to logic, types, exports, or runtime behavior.| +|**Shared Package**
`packages/shared/src/constants.ts`, `packages/shared/src/errors.ts`, `packages/shared/src/index.ts`, `packages/shared/src/types.ts`, `packages/shared/src/utils.ts`|Added trailing blank lines to 5 shared module files; no declaration, type, or API signature changes.| +|**Template Files**
`templates/iac/betterbase/cron.ts`, `templates/iac/betterbase/mutations/todos.ts`, `templates/iac/betterbase/queries/todos.ts`, `templates/iac/betterbase/schema.ts`|Added final newlines and adjusted indentation/whitespace in template examples; no changes to exported names, argument schemas, handlers, or runtime behavior.| + +## Estimated code review effort + +🎯 1 (Trivial) | ⏱️ ~2 minutes + +## Suggested labels + +`codex` + + + + + + +
+🚥 Pre-merge checks | ✅ 1 | ❌ 2 + +### ❌ Failed checks (2 warnings) + +| Check name | Status | Explanation | Resolution | +| :----------------: | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------- | +| Title check | ⚠️ Warning | Title claims to trigger CodeRabbit review but actual changes are only trailing whitespace additions across 20 files with no functional code modifications. | Use a title that accurately describes the changes, such as 'chore: add trailing newlines to CLI utils and shared packages' or similar. | +| Docstring Coverage | ⚠️ Warning | Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. | + +
+✅ Passed checks (1 passed) + +| Check name | Status | Explanation | +| :---------------: | :------- | :---------------------------------------------------------- | +| Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. | + +
+ +✏️ Tip: You can configure your own custom pre-merge checks in the settings. + +
+ + + + + +
+✨ Finishing Touches + +
+🧪 Generate unit tests (beta) + +- [ ] Create PR with unit tests +- [ ] Commit unit tests in branch `trigger-coderabbit-2` + +
+ +
+ + + + + +--- + +Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=Helal-maker/Betterbase&utm_content=8)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. + +
+❤️ Share + +- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai) +- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai) +- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai) +- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code) + +
+ +Comment `@coderabbitai help` to get the list of available commands and usage tips. + + + +``` +----- + + + +chore: trigger CodeRabbit review - templates & scripts (3/7) +#9 + + +```txt +Verify each finding against the current code and only fix it if needed. + +Inline comments: +In `@templates/auth/src/routes/auth.ts`: +- Line 107: The OTP and backup-code generation in auth.ts (variables like otp at +the Math.random() line and the backup/code generation around lines 169-171 and +291) use non-cryptographic Math.random() and must be replaced with a +cryptographically secure RNG; import Node's crypto and use +crypto.randomInt(100000, 1000000) to produce a uniformly distributed 6-digit OTP +(ensure string zero-padding if needed) and use crypto.randomBytes (then +hex/base32/BASE62-encode or map bytes to the allowed alphabet) to generate +backup codes of the required length/entropy, replacing usages of Math.random() +throughout auth.ts (e.g., where otp and backup codes are created) so all auth +secrets come from crypto-secure randomness. +- Around line 135-136: The conditional guards in the verification endpoints +incorrectly allow bypass because they check "process.env.NODE_ENV === +'development' || code.length === 6" (which is always true due to schema +validation) before issuing sessionId; remove the "|| code.length === 6" from +those conditionals and replace with actual verification logic: call the existing +OTP/verification routine or compare the provided code against the +stored/expected code (e.g., via a verifyCode/validateOtp function or DB lookup) +and only generate sessionId (crypto.randomUUID()) when that verification +succeeds; ensure the development-only bypass remains strictly tied to NODE_ENV +=== 'development' and update all occurrences that currently use the "code.length +=== 6" pattern. +- Around line 68-83: The current token check (token.startsWith("dev-token-")) in +the auth route unconditionally accepts dev magic links; restrict this behavior +so it only runs in development: guard the dev-branch with an environment/config +check (e.g., process.env.NODE_ENV === "development" or a feature flag) around +the token.startsWith check and its mock session creation (the code that calls +crypto.randomUUID() and returns the dev user object), and otherwise fall through +to the real verification path or reject the token; update any tests or comments +to reflect that dev-token handling is disabled outside development. + +In `@templates/base/src/lib/realtime.ts`: +- Around line 263-265: Subscription lookup uses client.subscriptions.get(table) +but subscriptions are keyed by `${table}:${event}`, causing subscription to be +undefined and filters in matchesFilter to be skipped; update the lookup where +subscription is retrieved (the code that currently does +client.subscriptions.get(table)) to use the composite key `${table}:${event}` +(or otherwise derive the correct key from the message's event and table) so that +subscription?.filter is the actual stored filter before calling +this.matchesFilter(subscription?.filter, data), ensuring filters are enforced. + +In `@templates/base/src/routes/index.ts`: +- Around line 16-23: The response currently exposes err.message and err.cause +for HTTP exceptions in non-development environments; change the logic so +sensitive details are only returned when env.NODE_ENV === "development". Update +the showDetailedError calculation (remove isHttpError from it) and restrict +stack and details to only be set when showDetailedError is true; for example +keep isHttpError only for setting status but ensure error: showDetailedError ? +err.message : "Internal Server Error" and details: showDetailedError && +isHttpError ? ((err as { cause?: unknown }).cause ?? null) : null so no +exception messages/causes are leaked in production (referencing +showDetailedError, isHttpError, env.NODE_ENV, err.message, err.stack, and the +details expression). + +In `@templates/base/src/routes/storage.ts`: +- Line 402: Update the three storage route patterns that currently use ":key" so +they accept nested paths by replacing each occurrence of "/:bucket/:key" with +Hono's regex form "/:bucket/:key{.+}" (and similarly "/:bucket/:key{.+}/public" +and "/:bucket/:key{.+}/sign"); locate the routes defined via +storageRouter.get(...) (the one starting at the shown diff and the two other +routes referenced) and change their route strings only, leaving parameter access +as c.req.param("key") unchanged. + +In `@templates/base/src/routes/users.ts`: +- Line 91: The POST handler currently validates request input but doesn't +persist the new user; replace the TODO by calling the DB insert to save the +parsed user (e.g., invoke db.insert(users).values(parsed) and await the result) +or call a dedicated UsersService (e.g., UsersService.create(parsed)) to persist +and return the persisted record/ID; ensure you handle and propagate DB errors +(try/catch) and return appropriate HTTP responses (201 on success with created +user or ID, 500 on DB error) from the POST handler. +```` +--- + +chore: trigger CodeRabbit review - CLI commands (1/7) +#7 + +```txt +Verify each finding against the current code and only fix it if needed. + +Outside diff comments: +In `@packages/cli/src/commands/dev/process-manager.ts`: +- Around line 21-23: The spawn call that sets this._proc currently hardcodes +"bun" as the runtime binary; update that spawn invocation (the code creating +this._proc via spawn) to use process.execPath as the executable and pass the +original arguments (e.g., "run" and entryPoint) as the args array, keeping cwd: +this._projectRoot unchanged so the CLI runtime invariant is respected; modify +the cmd/args in the spawn call (referencing spawn, this._proc, and entryPoint) +accordingly. + +In `@packages/cli/src/commands/init.ts`: +- Line 186: Replace the hardcoded "bun" argument in the Bun.spawn call that +creates installProcess with process.execPath so the spawned process uses the +current Node/Bun executable path; update the array passed to Bun.spawn in the +installDependencies/init logic (the installProcess creation) to use +process.execPath followed by "install" (i.e., [process.execPath, "install"]) and +ensure any related tests or callers expecting "bun" are adjusted accordingly. +``` + + diff --git a/NOTICE.md b/specs/NOTICE.md similarity index 100% rename from NOTICE.md rename to specs/NOTICE.md diff --git a/specs/README.md b/specs/README.md new file mode 100644 index 0000000..b91b4f6 --- /dev/null +++ b/specs/README.md @@ -0,0 +1,647 @@ +
+ +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/weroperking/Betterbase) + +``` +██████╗ ██╗ ██╗ ██████╗ ██████╗ ██╗ ███████╗ ██████╗ ██╗ ██╗███████╗ +██╔══██╗██║ ██║██╔════╝██╔═══██╗██║ ██╔════╝ ██╔═══██╗██║ ██║██╔════╝ +██████╔╝███████║██║ ██║ ██║██║ █████╗ ██║ ██║██║ █╗ ██║█████╗ +██╔══██╗██╔══██║██║ ██║ ██║██║ ██╔══╝ ██║ ██║██║███╗██║██╔══╝ +██║ ██║██║ ██║╚██████╗╚██████╔╝███████╗███████╗ ╚██████╔╝╚███╔███╔╝███████╗ +╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝ ╚══╝ ╚══╝ ╚══════╝ +``` + +**The AI-Native Backend-as-a-Service Platform** + +Blazing-fast backend development with Sub-100ms Local Dev — Built on Bun + SQLite, deploy anywhere with PostgreSQL support. + +*Database • Authentication • Realtime Subscriptions • Storage • Serverless Functions • Vector Search* + +**Last Updated: 2026-03-30** + +
+ +--- + +## Why Choose Betterbase? + +``` +┌─────────────────────────────────────────────────────────────────────────────────────────────┐ +│ ✦ BETTERBASE ARCHITECTURE ✦ │ +├─────────────────────────────────────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────┐ ┌──────────────────────────────┐ ┌─────────┐ │ +│ │ Frontend │ │ │ │ │ │ +│ │ (React, Vue, │──────────▶│ BETTERBASE CORE │──────────▶│ DB │ │ +│ │ Mobile, │ │ │ │ SQLite/ │ │ +│ │ Svelte) │ │ Auth │ Realtime │ Storage │ │ Postgres│ │ +│ └──────────────┘ │ RLS │ Vector │ Fns │ └─────────┘ │ +│ └──────────────────────────────┘ │ │ +│ │ │ │ +│ ┌──────▼──────┐ │ │ +│ │ IaC │◀──── (Infrastructure as Code) │ │ +│ │ Layer │ │ │ +│ │ Convex-ish │ │ │ +│ └───────────┘ │ │ +│ │ │ +│ ┌──────▼──────┐│ +│ │ Inngest ││ +│ │ Workflows ││ +│ └────────────┘│ +└─────────────────────────────────────────────────────────────────────────────────────────────┘ +``` + +| | TraditionalBAAS | Betterbase | +|--|------------------|------------| +| ⚡ | Slow local dev | **Sub-100ms dev** with Bun + SQLite | +| 🗄️ | Black box DB | **Full PostgreSQL** with raw SQL access | +| 🔍 | Basic search | **Full-text + Vector search** built-in | +| 🚀 | Cloud lock-in | **Self-host anywhere** with Docker | +| 📊 | Limited analytics | **Full observability** out of the box | +| 🔐 | Closed source | **100% open source** - deploy anywhere | + +--- + +## Quick Start + +```bash +# Install the CLI +bun install -g @betterbase/cli + +# Create a new project (IaC mode - recommended) +bb init my-app +cd my-app +bun install +bb dev +``` + +Your project structure: + +``` +my-app/ +├── betterbase/ +│ ├── schema.ts # Define tables (Convex-style) +│ ├── queries/ # Read functions (auto-subscribe) +│ ├── mutations/ # Write functions (transactions) +│ └── actions/ # Side-effects (scheduled, HTTP) +├── betterbase.config.ts # Optional config +└── package.json +``` + +### Define Your Schema + +Edit `betterbase/schema.ts`: + +```typescript +import { defineSchema, defineTable, v } from "@betterbase/core/iac" + +export const schema = defineSchema({ + users: defineTable({ + name: v.string(), + email: v.string(), + }).uniqueIndex("by_email", ["email"]), + + posts: defineTable({ + title: v.string(), + content: v.string(), + published: v.boolean(), + authorId: v.id("users"), + }).index("by_author", ["authorId"]), +}) +``` + +### Write Functions + +```typescript +// betterbase/queries/posts.ts +import { query } from "@betterbase/core/iac" + +export const listPosts = query({ + args: { published: v.optional(v.boolean()) }, + handler: async (ctx, args) => { + return ctx.db.query("posts") + .filter("published", "eq", args.published ?? true) + .order("desc") + .take(50) + }, +}) +``` + +```typescript +// betterbase/mutations/posts.ts +import { mutation } from "@betterbase/core/iac" + +export const createPost = mutation({ + args: { + title: v.string(), + content: v.string(), + authorId: v.id("users"), + }, + handler: async (ctx, args) => { + return ctx.db.insert("posts", { + ...args, + published: false, + }) + }, +}) +``` + +### Run + +```bash +bb dev +``` + +Your backend runs at `http://localhost:3000`. The dashboard is at `http://localhost:3001`. + +--- + +## Key Features + +| Feature | Description | +|---------|-------------| +| **IaC Layer** | Convex-inspired: define schema + functions in TypeScript | +| **Auto-Realtime** | Queries auto-subscribe to changes | +| **Type Safety** | Full TypeScript inference, no code generation needed | +| **Migrations** | Automatic diff + apply on `bb dev` | +| **Raw SQL** | `ctx.db.execute()` for complex queries | +| **Full-Text Search** | PostgreSQL GIN indexes via `ctx.db.search()` | +| **Vector Search** | pgvector + HNSW for embeddings | +| **Serverless Functions** | Deploy custom API functions | +| **Storage** | S3-compatible object storage | +| **Webhooks** | Event-driven with signed payloads | +| **Background Jobs** | Durable workflows via Inngest | +| **RLS** | Row-level security policies | +| **Branching** | Preview environments per branch | + +--- + +## Betterbase vs Convex + +| Feature | Convex | Betterbase | +|---------|--------|------------| +| Database | Black box | Full PostgreSQL | +| Raw SQL | Not available | `ctx.db.execute()` | +| Full-Text Search | Not built-in | PostgreSQL FTS | +| Vector Search | Limited | pgvector + HNSW | +| Self-Hosting | Not supported | Docker to your infra | +| Migration | — | `bb migrate from-convex` | + +**Betterbase gives you Convex simplicity with full SQL power.** + +--- + +## Inngest Integration + +Betterbase uses [Inngest](https://www.inngest.com/) for durable workflows and background jobs. + +### Deployment Modes + +| Mode | Inngest Backend | Used By | +|------|----------------|---------| +| Cloud | `https://api.inngest.com` | BetterBase Cloud offering | +| Self-Hosted | `http://inngest:8288` | Docker deployment | +| Local Dev | `http://localhost:8288` | Development and testing | + +### Environment Variables + +```bash +# For local development +INNGEST_BASE_URL=http://localhost:8288 + +# For self-hosted production +INNGEST_BASE_URL=http://inngest:8288 +INNGEST_SIGNING_KEY=your-signing-key +INNGEST_EVENT_KEY=your-event-key +``` + +### Features + +- **Webhook Delivery**: Retryable, observable webhook delivery with automatic backoff +- **Notification Rules**: Cron-based metric polling with fan-out notifications +- **Background Exports**: Async CSV export with progress tracking + +--- + +## Project Structure + +Betterbase supports two patterns: + +### 1. IaC Pattern (Recommended) + +``` +my-app/ +├── betterbase/ +│ ├── schema.ts # defineSchema() + defineTable() +│ ├── queries/ # query() functions +│ ├── mutations/ # mutation() functions +│ ├── actions/ # action() functions +│ └── cron.ts # scheduled functions +├── betterbase.config.ts # Optional config +└── package.json +``` + +### 2. Original Pattern (Advanced) + +``` +my-app/ +├── src/ +│ ├── db/ +│ │ ├── schema.ts # Drizzle schema +│ │ └── migrate.ts # Migration runner +│ ├── routes/ # Hono routes +│ └── functions/ # Serverless functions +├── betterbase.config.ts +└── package.json +``` + +Both patterns work together. Add `betterbase/` to any existing project. + +--- + +## CLI Reference + +| Command | Description | +|---------|-------------| +| `bb init [name]` | Create new project | +| `bb dev` | Start dev server | +| `bb iac sync` | Sync IaC schema | +| `bb iac analyze` | Analyze query performance | +| `bb migrate` | Run migrations | +| `bb generate types` | Generate TypeScript types | + +--- + +## Client SDK + +```bash +bun add @betterbase/client +``` + +```typescript +import { createClient } from '@betterbase/client' + +const client = createClient({ + baseUrl: 'http://localhost:3000', +}) + +// Use IaC functions +const { data: posts } = await client.bff.queries.posts.listPosts({}) + +// Mutations +await client.bff.mutations.posts.createPost({ + title: 'Hello', + content: 'World', + authorId: 'user-123', +}) +``` + +--- + +## Deployment + +### Local + +```bash +bb dev +``` + +### Docker + +```bash +docker-compose up -d +``` + +### Self-Hosted + +See [SELF_HOSTED.md](SELF_HOSTED.md) for full documentation. + +```typescript +database: { + provider: 'neon', + connectionString: process.env.NEON_CONNECTION_STRING +} +``` + +### Turso (libSQL) + +Best for edge deployments and distributed databases. + +```typescript +database: { + provider: 'turso', + connectionString: process.env.TURSO_DATABASE_URL, + authToken: process.env.TURSO_AUTH_TOKEN +} +``` + +### MySQL + +Best for legacy applications or MySQL preference. + +```typescript +database: { + provider: 'mysql', + connectionString: process.env.MYSQL_URL +} +``` + +### PlanetScale (MySQL-compatible) + +Best for serverless MySQL with branch-based schema changes. + +```typescript +database: { + provider: 'planetscale', + connectionString: process.env.PLANETSCALE_URL +} +``` + +--- + +## Authentication + +### Setup BetterAuth + +Initialize authentication in your project: + +```bash +bb auth setup +``` + +This creates `src/auth/` with default configuration. + +### Configure Providers + +Edit `src/auth/index.ts`: + +```typescript +import { betterAuth } from 'better-auth' +import { drizzleAdapter } from 'better-auth/adapters/drizzle' +import { db } from '../db' + +export const auth = betterAuth({ + database: drizzleAdapter(db, { + provider: 'sqlite' // or 'postgres', 'mysql' + }), + emailAndPassword: { + enabled: true, + requireEmailVerification: false + }, + socialProviders: { + github: { + clientId: process.env.GITHUB_CLIENT_ID, + clientSecret: process.env.GITHUB_CLIENT_SECRET + }, + google: { + clientId: process.env.GOOGLE_CLIENT_ID, + clientSecret: process.env.GOOGLE_CLIENT_SECRET + } + }, + session: { + expiresIn: 60 * 60 * 24 * 7, // 7 days + updateAge: 60 * 60 * 24 // 1 day + } +}) +``` + +### Row Level Security + +Betterbase integrates with database RLS for secure data access: + +```typescript +// In your schema or via CLI +bb rls add \ + --table posts \ + --name users_own_posts \ + --command SELECT \ + --check "user_id = auth.uid()" +``` + +This ensures users can only access their own data. + +--- + +## Ask Deepwiki + +> *Your AI-powered development assistant, integrated directly into Betterbase.* + +Ask Deepwiki provides intelligent context for AI-assisted development: + +- **Smart Code Context**: Automatic `.betterbase-context.json` generation +- **IaC Analysis**: Understand your schema, queries, and mutations +- **Query Optimization**: Get recommendations for better performance +- **Documentation Generation**: Auto-generate docs from your code + +**Deepwiki Badge**: The badge at the top of this README links to [Ask Deepwiki](https://deepwiki.com/weroperking/Betterbase), where you can chat with an AI that understands your entire Betterbase project. + +### Using Ask Deepwiki + +1. **Development**: Get instant answers about your IaC layer +2. **Debugging**: Understand query behavior and optimization +3. **Onboarding**: New team members can ask about your architecture +4. **Refactoring**: Get AI suggestions for improving your code + +--- + +## Contributing + +We welcome contributions! Please follow these steps: + +### Getting Started + +1. **Fork** the repository +2. **Clone** your fork: `git clone https://github.com/your-username/betterbase.git` +3. **Install** dependencies: `bun install` +4. **Create** a branch: `git checkout -b feature/my-feature` + +### Development Setup + +```bash +# Install dependencies +bun install + +# Build all packages +bun run build + +# Run tests +bun test + +# Run linting +bun run lint +``` + +### Project Structure + +``` +betterbase/ +├── apps/ +│ └── test-project/ # Example/test project +├── packages/ +│ ├── cli/ # @betterbase/cli +│ ├── client/ # @betterbase/client +│ └── core/ # @betterbase/core +├── templates/ # Project templates +└── turbo.json # Turborepo configuration +``` + +### Code Style + +We use Biome for code formatting and linting: + +```bash +# Format code +bun run format + +# Lint code +bun run lint + +# Fix auto-fixable issues +bun run lint:fix +``` + +### Testing + +```bash +# Run all tests +bun test + +# Run tests for specific package +bun test --filter=@betterbase/cli + +# Run tests in watch mode +bun test --watch +``` + +### Commit Messages + +Follow Conventional Commits: + +``` +feat: add new feature +fix: resolve bug +docs: update documentation +refactor: restructure code +test: add tests +chore: maintenance +``` + +### Submitting Changes + +1. Push your branch: `git push origin feature/my-feature` +2. Open a **Pull Request** +3. Fill out the PR template +4. Wait for review + +--- + +## Code of Conduct + +### Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +### Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +### Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at conduct@betterbase.io. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. + +--- + +## Changelog + +All notable changes to this project will be documented in this section. + +### [1.0.0] - 2026-03-19 + +#### Added + +- **AI Context Generation**: Automatic `.betterbase-context.json` generation for AI-assisted development +- **Branch Management**: New `bb branch` command for creating isolated preview environments +- **Vector Search**: pgvector-powered similarity search with embeddings support +- **Auto-REST**: Automatic CRUD route generation from Drizzle schema +- **Enhanced CLI**: Added 12 commands including branch, webhook management, and storage operations + +#### Updated + +- Updated copyright year to 2026 +- Improved documentation with Last Updated timestamp +- Verified all features against current codebase structure +- Removed deprecated @betterbase/shared package references + +#### Security + +- Improved webhook signature verification +- Enhanced RLS policy engine + +--- + +## License + +Betterbase is open source under the [MIT License](../LICENSE). + +``` +MIT License + +Copyright (c) 2026 Betterbase + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` + +--- + +## Community & Support + +### Get Help + +| Resource | Link | +|----------|------| +| **Discord** | [Discord](https://discord.gg/R6Dm6Cgy2E) | +| **GitHub Issues** | [GitHub Issues](https://github.com/weroperking/Betterbase/issues) | + +### Contribute + +| Resource | Link | +|----------|------| +| **GitHub** | [GitHub](https://github.com/weroperking/Betterbase) | +| **Contributing Guide** | [CONTRIBUTING.md](../CONTRIBUTING.md) | +| **Good First Issues** | [Good First Issues](https://github.com/weroperking/Betterbase/labels/good-first-issue) | + +--- + +
+ +**Built with ❤️ by Weroperking** + +Website • [Documentation](../docs/README.md) • [Discord](https://discord.gg/R6Dm6Cgy2E) • [GitHub](https://github.com/weroperking/Betterbase) • Twitter + +
\ No newline at end of file diff --git a/SELF_HOSTED.md b/specs/SELF_HOSTED.md similarity index 100% rename from SELF_HOSTED.md rename to specs/SELF_HOSTED.md diff --git a/specs/Security_Review.md b/specs/Security_Review.md new file mode 100644 index 0000000..4612f34 --- /dev/null +++ b/specs/Security_Review.md @@ -0,0 +1,631 @@ +# Betterbase Security & Code Quality Review + +**Version**: 1.0 +**Date**: 2026-03-31 +**Status**: Critical Issues Found + +--- + +## Executive Summary + +This document provides a comprehensive security analysis and code quality review of the Betterbase project. The analysis identified **7 critical security vulnerabilities**, **3 incomplete features**, and **significant code quality issues** that require immediate attention. + +| Priority | Count | Description | +|----------|-------|-------------| +| Critical | 7 | Security vulnerabilities requiring immediate fix | +| High | 6 | Significant issues affecting production readiness | +| Medium | 5 | Code quality and incomplete features | +| Low | 3 | Code hygiene and testing improvements | + +--- + +## 1. Critical Security Vulnerabilities + +### 1.1 Authentication Bypass in Auth Template + +**Severity**: CRITICAL +**Location**: `templates/auth/src/routes/auth.ts` +**Lines**: 70, 135, 197, 221, 246, 322 + +#### Issue Description + +The auth template has 12+ incomplete implementations that completely bypass authentication in both development and production modes. + +#### Affected Endpoints + +| Line | Endpoint | Bypass Method | +|------|----------|---------------| +| 70 | `GET /magic-link/verify` | Accepts any token starting with `dev-token-` | +| 135 | `POST /otp/verify` | Accepts ANY 6-digit code | +| 197 | `POST /mfa/verify` | Same bypass | +| 221 | `POST /mfa/disable` | Same bypass | +| 246 | `POST /mfa/challenge` | Same bypass | +| 322 | `POST /phone/verify` | Same bypass | + +#### Vulnerable Code (Line 135) + +```typescript +// In templates/auth/src/routes/auth.ts:135 +if (process.env.NODE_ENV === "development" || code.length === 6) { + // Creates session without verifying OTP + const sessionId = crypto.randomUUID(); + return c.json({ + token: sessionId, + user: { id: "otp-user-id", email, name: "OTP User" }, + }); +} +``` + +#### Root Cause + +The condition `code.length === 6` is redundant because the Zod schema at line 16 already validates: +```typescript +const otpVerifySchema = z.object({ + email: z.string().email(), + code: z.string().length(6, "OTP must be 6 digits"), +}); +``` + +This means **production accepts any 6-digit code** as valid authentication. + +#### Impact + +- Complete authentication bypass in production +- Anyone can access any user account with any 6-digit code +- MFA endpoints are completely non-functional + +#### Remediation + +1. Remove the `|| code.length === 6` condition entirely +2. Implement proper OTP verification with database lookup +3. Add time-based expiry check (10 minutes) +4. Store hashed OTP codes in database + +#### Recommended Fix + +```typescript +authRoute.post("/otp/verify", async (c) => { + // ... validation ... + const { email, code } = result.data; + + // TODO: Verify OTP from database with expiry check + const pool = getPool(); + const { rows } = await pool.query( + `SELECT * FROM otp_codes + WHERE email = $1 AND code = $2 AND expires_at > NOW() + ORDER BY created_at DESC LIMIT 1`, + [email, code] + ); + + if (rows.length === 0) { + return c.json({ error: "Invalid or expired OTP" }, 401); + } + + // Delete used OTP + await pool.query("DELETE FROM otp_codes WHERE id = $1", [rows[0].id]); + + // Create session... +}); +``` + +--- + +### 1.2 No Rate Limiting on Admin Login + +**Severity**: CRITICAL +**Location**: `packages/server/src/routes/admin/auth.ts:15-45` + +#### Issue Description + +The admin login endpoint has no rate limiting or brute-force protection. + +#### Vulnerable Code + +```typescript +// In packages/server/src/routes/admin/auth.ts +authRoutes.post( + "/login", + zValidator("json", z.object({ + email: z.string().email(), + password: z.string().min(1), + })), + async (c) => { + const { email, password } = c.req.valid("json"); + // No rate limiting - attacker can brute force + const valid = await verifyPassword(password, admin.password_hash); + } +); +``` + +#### Impact + +- Vulnerable to brute-force attacks +- No account lockout after failed attempts +- Attackers can try unlimited passwords + +#### Remediation + +Add rate limiting middleware: + +```typescript +import { rateLimit } from "hono-rate-limit"; + +authRoutes.post( + "/login", + rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // 5 attempts per window + keyGenerator: (c) => c.req.header("X-Real-IP") ?? "unknown", + }), + // ... rest of handler +); +``` + +--- + +### 1.3 Unauthenticated WebSocket Connections + +**Severity**: CRITICAL +**Location**: `packages/server/src/routes/betterbase/ws.ts:142-147` + +#### Issue Description + +WebSocket connections don't require authentication, allowing anyone to connect to any project's realtime stream. + +#### Vulnerable Code + +```typescript +// In packages/server/src/routes/betterbase/ws.ts:142-147 +export function getBunServeConfig() { + return { + fetch(req: Request, server: any) { + const url = new URL(req.url); + if (url.pathname === "/betterbase/ws") { + const projectSlug = url.searchParams.get("project") ?? "default"; + // No authentication check + const upgraded = server.upgrade(req, { data: { projectSlug } }); + } + }, + }; +} +``` + +#### Impact + +- Anyone can subscribe to any project's data changes +- No authorization on which project to connect to +- Potential data leakage through realtime updates + +#### Remediation + +Add authentication validation: + +```typescript +if (url.pathname === "/betterbase/ws") { + const authHeader = req.headers.get("Authorization"); + const token = authHeader?.replace("Bearer ", ""); + + if (!token) { + return new Response("Unauthorized", { status: 401 }); + } + + const payload = await verifyAdminToken(token); + if (!payload) { + return new Response("Invalid token", { status: 401 }); + } + + const projectSlug = url.searchParams.get("project") ?? "default"; + // Verify user has access to this project + // ... +} +``` + +--- + +### 1.4 Hardcoded Default Credentials + +**Severity**: CRITICAL +**Location**: `packages/server/src/routes/betterbase/index.ts:191-192` + +#### Issue Description + +Default S3 credentials are hardcoded as fallback. + +#### Vulnerable Code + +```typescript +const s3 = new S3Client({ + endpoint: env.STORAGE_ENDPOINT ?? "http://minio:9000", + region: "us-east-1", + credentials: { + accessKeyId: env.STORAGE_ACCESS_KEY ?? "minioadmin", + secretAccessKey: env.STORAGE_SECRET_KEY ?? "minioadmin", + }, + forcePathStyle: true, +}); +``` + +#### Impact + +- If env vars aren't set, known default credentials are used +- Attackers can access MinIO storage if they discover the endpoint + +#### Remediation + +```typescript +if (!env.STORAGE_ACCESS_KEY || !env.STORAGE_SECRET_KEY) { + throw new Error("STORAGE_ACCESS_KEY and STORAGE_SECRET_KEY must be set"); +} +``` + +--- + +### 1.5 No CSRF Protection + +**Severity**: HIGH +**Location**: `packages/server/src/index.ts:52-60` + +#### Issue Description + +CORS is configured but there's no CSRF token validation for state-changing operations. + +#### Current Configuration + +```typescript +app.use( + "*", + cors({ + origin: env.CORS_ORIGINS.split(","), + credentials: true, + allowHeaders: ["Content-Type", "Authorization"], + allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], + }), +); +``` + +#### Impact + +- Cross-site request forgery attacks possible +- Attackers can make authenticated requests on behalf of users + +#### Remediation + +Add CSRF middleware with double-submit cookie pattern. + +--- + +### 1.6 Excessive JWT Token Expiry + +**Severity**: HIGH +**Location**: `packages/server/src/lib/auth.ts:7` + +#### Issue Description + +Admin JWT tokens expire after 30 days. + +```typescript +const TOKEN_EXPIRY = "30d"; +``` + +#### Impact + +- If token is compromised, attackers have ~month of access +- Violates security best practices (tokens should be short-lived) + +#### Remediation + +- Reduce to 7 days for admin tokens +- Implement refresh token mechanism +- Use shorter expiry (1-24 hours) for sensitive operations + +--- + +### 1.7 Input Validation Gaps + +**Severity**: HIGH +**Location**: `packages/server/src/routes/betterbase/index.ts:180` + +#### Issue Description + +Filename parameter not validated - potential path traversal. + +```typescript +const { contentType, filename } = await c.req.json(); +const ext = filename?.split(".").pop() ?? ""; +const s3Key = `project_${projectSlug}/${storageId}${ext ? "." + ext : ""}`; +``` + +#### Impact + +- Path traversal attacks possible (e.g., `../../etc/passwd`) +- No sanitization of file extension + +#### Remediation + +```typescript +const { contentType, filename } = await c.req.json(); + +// Validate filename +if (filename && !/^[a-zA-Z0-9_.-]+$/.test(filename)) { + return c.json({ error: "Invalid filename" }, 400); +} + +const ext = filename?.split(".").pop() ?? ""; +if (ext && !/^[a-zA-Z0-9]{1,10}$/.test(ext)) { + return c.json({ error: "Invalid file extension" }, 400); +} +``` + +--- + +## 2. Incomplete Features + +### 2.1 GraphQL Subscriptions Not Implemented + +**Status**: INCOMPLETE +**Location**: `packages/core/src/graphql/resolvers.ts:505-546` + +#### Description + +GraphQL subscriptions are marked as Phase 6 and return empty iterators. + +```typescript +resolvers.Subscription![`${tableName}Created`] = { + subscribe: () => { + // Subscriptions require realtime layer (Phase 6) + return { + [Symbol.asyncIterator]() { + return this; + }, + async next() { + return { done: true, value: undefined }; + }, + }; + }, +} as unknown as GraphQLResolver; +``` + +#### Impact + +- GraphQL subscriptions don't work +- Clients waiting for realtime updates will hang indefinitely + +#### Required Work + +Implement subscriptions using the existing WebSocket infrastructure in `packages/server/src/routes/betterbase/ws.ts`. + +--- + +### 2.2 RLS Role-Based Policies Not Implemented + +**Status**: INCOMPLETE +**Location**: `packages/core/src/rls/evaluator.ts:57-66` + +#### Description + +The `auth.role()` check always returns `false`. + +```typescript +const roleMatch = policyExpression.match(/auth\.role\(\)\s*=\s*'([^']+)'/); +if (roleMatch) { + const requiredRole = roleMatch[1]; + // In a full implementation, we'd get the user's role from the session + return false; // Deny by default if role check not implemented +} +``` + +#### Impact + +- Role-based access control doesn't work +- Admin/editor/viewer roles cannot be enforced + +#### Required Work + +Implement role retrieval from session and proper evaluation. + +--- + +### 2.3 Auth Template TODOs + +**Status**: INCOMPLETE +**Location**: `templates/auth/src/routes/auth.ts` + +#### List of Incomplete Implementations + +| Line | Feature | Description | +|------|---------|-------------| +| 57 | Magic Link API | Use better-auth's magic link API | +| 68 | Token Verification | Implement proper token verification | +| 113 | OTP Storage | Store OTP in database with expiry | +| 133 | OTP Verification | Verify OTP from database | +| 166 | MFA Enable | Use better-auth twoFactor plugin | +| 195 | MFA Verification | Verify TOTP code using better-auth | +| 220 | MFA Disable | Disable MFA using better-auth | +| 244 | MFA Challenge | Verify TOTP and return session | +| 298 | Phone OTP Storage | Store hashed code with 10-min expiry | +| 299 | Twilio Integration | Send SMS via Twilio in production | +| 320 | Phone Verification | Verify code with expiry check | + +#### Impact + +- Auth template is not production-ready +- Only works in development mode + +--- + +## 3. Code Quality Issues + +### 3.1 Excessive `any` Type Usage + +**Severity**: MEDIUM +**Count**: 327+ occurrences + +#### Examples + +| Location | Line | Usage | +|----------|------|-------| +| `packages/server/src/routes/betterbase/index.ts` | 43 | `(fn.handler as any)._args` | +| `packages/server/src/routes/betterbase/ws.ts` | 24 | `open(ws: any)` | +| `packages/core/src/iac/db-context.ts` | 60 | `params as any[]` | + +#### Remediation + +Define proper TypeScript types for all function signatures and handlers. + +--- + +### 3.2 Console Logging in Production + +**Severity**: MEDIUM +**Count**: 369+ occurrences + +#### Examples + +```typescript +// packages/server/src/index.ts:89 +console.error("[error]", err); + +// packages/core/src/rls/evaluator.ts:69 +console.warn(`[RLS] Unknown policy expression: ${policyExpression}`); +``` + +#### Remediation + +Replace with structured logger (pino or similar): + +```typescript +import { logger } from "./lib/logger"; + +logger.error({ err }, "Request failed"); +``` + +--- + +### 3.3 Placeholder Tests + +**Severity**: LOW +**Count**: 10+ test files with no real tests + +#### Examples + +```typescript +// packages/core/test/realtime-channel-manager.test.ts +it("should subscribe to channels", () => { + expect(true).toBe(true); // Placeholder +}); +``` + +#### Remediation + +Implement actual test coverage for: +- `realtime-channel-manager.test.ts` +- `subscription-tracker.ts` tests +- `invalidation-manager.ts` tests + +--- + +## 4. Testing Gaps + +### 4.1 Missing Test Coverage + +| Module | Status | Notes | +|--------|--------|-------| +| WebSocket handler | NO TESTS | `packages/server/src/routes/betterbase/ws.ts` | +| IaC route handler | NO TESTS | `packages/server/src/routes/betterbase/index.ts` | +| Subscription tracker | NO TESTS | Core realtime component | +| Invalidation manager | NO TESTS | Core realtime component | +| Storage context | NO TESTS | `packages/core/src/iac/storage/storage-ctx.ts` | + +### 4.2 Integration Tests Missing + +No end-to-end tests for: +- Full IaC pipeline (schema → function registry → route handler) +- WebSocket realtime subscriptions +- Auth flow with real database + +--- + +## 5. Security Best Practices + +### 5.1 SQL Injection Risk + +**Location**: `packages/core/src/iac/db-context.ts:105` + +```typescript +const embeddingStr = `[${embedding.join(",")}]`; +const { rows } = await this._pool.query(sql, [embeddingStr]); +``` + +While the embedding is passed as a parameter, the SQL construction should be reviewed to ensure no injection is possible through complex queries. + +### 5.2 Global Mutable State + +**Location**: `packages/core/src/iac/db-context.ts:331` + +```typescript +const mgr = (globalThis as any).__betterbaseRealtimeManager; +``` + +Using global state for realtime manager can cause issues in serverless/edge environments. + +--- + +## 6. Recommendations Summary + +### Immediate Actions (Critical) + +| # | Action | Location | Effort | +|---|--------|-----------|--------| +| 1 | Fix auth bypass | `templates/auth/src/routes/auth.ts:135` | Medium | +| 2 | Add rate limiting | `packages/server/src/routes/admin/auth.ts` | Low | +| 3 | Authenticate WebSocket | `packages/server/src/routes/betterbase/ws.ts` | Medium | +| 4 | Remove hardcoded creds | `packages/server/src/routes/betterbase/index.ts` | Low | +| 5 | Add input validation | `packages/server/src/routes/betterbase/index.ts:180` | Low | + +### Short-term (High Priority) + +| # | Action | Location | Effort | +|---|--------|-----------|--------| +| 6 | Add CSRF middleware | `packages/server/src/index.ts` | Medium | +| 7 | Reduce JWT expiry | `packages/server/src/lib/auth.ts` | Low | +| 8 | Implement RLS roles | `packages/core/src/rls/evaluator.ts` | Medium | + +### Medium-term (Feature Completion) + +| # | Action | Location | Effort | +|---|--------|-----------|--------| +| 9 | GraphQL subscriptions | `packages/core/src/graphql/resolvers.ts` | High | +| 10 | Complete auth template | `templates/auth/src/routes/auth.ts` | High | +| 11 | Replace console.log | Throughout codebase | Medium | + +### Long-term (Code Quality) + +| # | Action | Effort | +|---|--------|--------| +| 12 | Remove `any` types (327+) | High | +| 13 | Add integration tests | High | +| 14 | Structured logging migration | Medium | + +--- + +## 7. Appendix + +### File Locations Reference + +| Issue | File Path | +|-------|-----------| +| Auth bypass | `templates/auth/src/routes/auth.ts` | +| No rate limiting | `packages/server/src/routes/admin/auth.ts` | +| WebSocket auth | `packages/server/src/routes/betterbase/ws.ts` | +| Hardcoded creds | `packages/server/src/routes/betterbase/index.ts` | +| No CSRF | `packages/server/src/index.ts` | +| JWT expiry | `packages/server/src/lib/auth.ts` | +| Input validation | `packages/server/src/routes/betterbase/index.ts` | +| GraphQL subs | `packages/core/src/graphql/resolvers.ts` | +| RLS roles | `packages/core/src/rls/evaluator.ts` | +| `any` types | Throughout `packages/server` and `packages/core` | + +--- + +*Document generated: 2026-03-31* +*Review scope: Full codebase analysis* \ No newline at end of file diff --git a/templates/auth/src/routes/auth.ts b/templates/auth/src/routes/auth.ts index 5e0113b..6729b67 100644 --- a/templates/auth/src/routes/auth.ts +++ b/templates/auth/src/routes/auth.ts @@ -65,13 +65,11 @@ authRoute.get("/magic-link/verify", async (c) => { return c.json({ error: "Token is required" }, 400); } - // TODO: Implement proper token verification using better-auth - // For now, simulate verification - if (token.startsWith("dev-token-")) { - // In dev mode, create a mock session + const isDev = process.env.NODE_ENV === "development"; + + if (isDev && token.startsWith("dev-token-")) { const sessionId = crypto.randomUUID(); - // Find or create user (in real implementation, this would be done by better-auth) return c.json({ token: sessionId, user: { @@ -131,8 +129,8 @@ authRoute.post("/otp/verify", async (c) => { const { email, code } = result.data; // TODO: Verify OTP from database in production - // For dev mode, accept any 6-digit code - if (process.env.NODE_ENV === "development" || code.length === 6) { + // TODO: Integrate with better-auth for production + if (process.env.NODE_ENV === "development") { const sessionId = crypto.randomUUID(); return c.json({ @@ -193,8 +191,8 @@ authRoute.post("/mfa/verify", async (c) => { const { code } = result.data; // TODO: Verify TOTP code using better-auth - // Accept any 6-digit code in dev mode - if (process.env.NODE_ENV === "development" || code.length === 6) { + // TODO: Integrate with better-auth for production + if (process.env.NODE_ENV === "development") { return c.json({ message: "MFA enabled successfully" }); } @@ -218,7 +216,8 @@ authRoute.post("/mfa/disable", async (c) => { const { code } = result.data; // TODO: Verify and disable MFA using better-auth - if (process.env.NODE_ENV === "development" || code.length === 6) { + // TODO: Integrate with better-auth for production + if (process.env.NODE_ENV === "development") { return c.json({ message: "MFA disabled successfully" }); } @@ -242,8 +241,8 @@ authRoute.post("/mfa/challenge", async (c) => { const { code } = result.data; // TODO: Verify TOTP code and return session using better-auth - // Accept any 6-digit code in dev mode - if (process.env.NODE_ENV === "development" || code.length === 6) { + // TODO: Integrate with better-auth for production + if (process.env.NODE_ENV === "development") { const sessionId = crypto.randomUUID(); return c.json({ token: sessionId, @@ -318,8 +317,8 @@ authRoute.post("/phone/verify", async (c) => { const { phone, code } = result.data; // TODO: Verify code from database with expiry check (10 minutes) - // Accept any 6-digit code in dev mode - if (process.env.NODE_ENV === "development" || code.length === 6) { + // TODO: Integrate with better-auth for production + if (process.env.NODE_ENV === "development") { const sessionId = crypto.randomUUID(); return c.json({