Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ on:
pull_request:
branches: [main, develop]

env:
BUN_VERSION: "1.3.10"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify Bun version parity across workflow and Dockerfiles (read-only).
set -euo pipefail

echo "== CI workflow Bun version references =="
rg -nP 'BUN_VERSION|bun-version' .github/workflows/ci.yml

echo
echo "== Dockerfile Bun base image references =="
fd -HI 'Dockerfile*' | xargs -I{} rg -nP '^\s*FROM\s+oven/bun(?::|@sha256:)' {}

echo
echo "Expected: same Bun minor/patch across CI and runtime Dockerfiles."

Repository: weroperking/Betterbase

Length of output: 636


Synchronize CI Bun version with Dockerfile versions.

CI runs 1.3.10 but all Dockerfiles pin 1.3.9-debian and 1.3.9-alpine (lines 11, 18, 23). This version skew means CI tests won't catch 1.3.9-specific failures that occur in production builds and containers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml at line 8, CI is using BUN_VERSION: "1.3.10" while
Dockerfiles are pinned to "1.3.9-debian" and "1.3.9-alpine", causing a version
skew; update the CI variable BUN_VERSION to match the Dockerfile pins (set
BUN_VERSION: "1.3.9") or instead update the Dockerfile pins to "1.3.10-..." so
all places use the same Bun version (refer to the BUN_VERSION entry and the
Dockerfile image tags "1.3.9-debian" / "1.3.9-alpine" to make the change).


jobs:
test:
name: Test
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
8 changes: 2 additions & 6 deletions Dockerfile.project
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 . .

Expand Down
15 changes: 4 additions & 11 deletions apps/dashboard/src/components/auth/SetupGuard.tsx
Original file line number Diff line number Diff line change
@@ -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);
Expand Down
9 changes: 8 additions & 1 deletion apps/dashboard/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -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;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

API_BASE can be undefined if env var unset.

Removing the fallback means ${API_BASE}/... will produce "undefined/..." as a URL when VITE_API_URL is not set, causing network failures.

Proposed fix
-const API_BASE = import.meta.env.VITE_API_URL;
+const API_BASE = import.meta.env.VITE_API_URL;
+if (!API_BASE) {
+	throw new Error("VITE_API_URL environment variable is required");
+}

Or provide a development fallback if intended:

-const API_BASE = import.meta.env.VITE_API_URL;
+const API_BASE = import.meta.env.VITE_API_URL ?? "http://localhost:3001";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const API_BASE = import.meta.env.VITE_API_URL;
const API_BASE = import.meta.env.VITE_API_URL;
if (!API_BASE) {
throw new Error("VITE_API_URL environment variable is required");
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/lib/api.ts` at line 1, API_BASE is assigned directly from
import.meta.env.VITE_API_URL and can be undefined, causing string interpolation
like `${API_BASE}/...` to produce "undefined/..."; update the assignment of
API_BASE (or create a small getApiBase() helper) to validate the env var and
either supply a sensible fallback for dev (e.g., "http://localhost:PORT") or
throw a clear error at startup if missing; ensure all usages that interpolate
API_BASE (e.g., `${API_BASE}/...`) rely on this validated/fallback value so
network requests never use "undefined" as the origin.


export class ApiError extends Error {
constructor(
Expand Down Expand Up @@ -94,3 +94,10 @@ export const api = {
return res.blob();
},
};

export async function checkSetup(): Promise<boolean> {
const res = await fetch(`${API_BASE}/admin/auth/setup/check`, {
method: "GET",
});
return res.status !== 410;
}
Comment on lines +98 to +103
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

checkSetup() uses raw fetch and swallows errors.

This bypasses the api helper pattern and doesn't handle network errors. If the fetch throws (e.g., network down), the error propagates uncaught.

Proposed fix using try/catch
 export async function checkSetup(): Promise<boolean> {
-	const res = await fetch(`${API_BASE}/admin/auth/setup/check`, {
-		method: "GET",
-	});
-	return res.status !== 410;
+	try {
+		const res = await fetch(`${API_BASE}/admin/auth/setup/check`, {
+			method: "GET",
+		});
+		return res.status !== 410;
+	} catch {
+		// Network error - assume setup not complete to allow retry
+		return false;
+	}
 }

As per coding guidelines: "All server calls go through src/lib/api.ts."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export async function checkSetup(): Promise<boolean> {
const res = await fetch(`${API_BASE}/admin/auth/setup/check`, {
method: "GET",
});
return res.status !== 410;
}
export async function checkSetup(): Promise<boolean> {
try {
const res = await fetch(`${API_BASE}/admin/auth/setup/check`, {
method: "GET",
});
return res.status !== 410;
} catch {
// Network error - assume setup not complete to allow retry
return false;
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/lib/api.ts` around lines 98 - 103, The checkSetup function
currently calls fetch directly and lets network errors bubble up; wrap the fetch
call in a try/catch so network failures are handled and do not throw uncaught,
and return a safe boolean (e.g., false) on error. Update the checkSetup
implementation (referencing checkSetup and API_BASE) to perform the fetch inside
a try block, catch any exceptions, log or handle the error as appropriate, and
return false from the catch; if you have an existing module-level request helper
(use that instead of raw fetch), call that helper to keep all server calls going
through the central API abstraction.

2 changes: 1 addition & 1 deletion apps/dashboard/src/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/// <reference types="vite/client" />

interface ImportMetaEnv {
readonly VITE_API_URL?: string;
readonly VITE_API_URL: string;
}

interface ImportMeta {
Expand Down
26 changes: 25 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 0 additions & 2 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
version: "3.9"

# Local development: runs Inngest dev server only.
# BetterBase server runs outside Docker via: bun run dev
#
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.self-hosted.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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:
Expand Down
31 changes: 21 additions & 10 deletions packages/cli/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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<void> {
const commandName = process.argv[2];
async function checkAuthHook(this: Command, actionCommand?: Command): Promise<void> {
const commandName = actionCommand?.name() ?? this.args?.[0] ?? process.argv[2] ?? "";

// Skip auth check for public commands
if (PUBLIC_COMMANDS.includes(commandName)) {
Expand Down Expand Up @@ -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 });
});

Expand Down Expand Up @@ -476,9 +487,10 @@ export function createProgram(): Command {
.argument("<name>", "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 ────────────────────────────────────────
Expand Down Expand Up @@ -541,9 +553,8 @@ export function createProgram(): Command {
});

branch
.argument("[project-root]", "project root directory", process.cwd())
.option("-p, --project-root <path>", "project root directory", process.cwd())
.action(async (options) => {
.action(async (options: { projectRoot?: string }) => {
const projectRoot = options.projectRoot || process.cwd();
await runBranchCommand([], projectRoot);
});
Expand Down
68 changes: 49 additions & 19 deletions packages/client/src/iac/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof setTimeout> | 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 (
<BetterBaseContext.Provider
Expand Down
Loading