Skip to content
Merged
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
119 changes: 55 additions & 64 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,98 +6,89 @@
# ============================================================================

# ----------------------------------------------------------------------------
# Required: Database
# Database (PostgreSQL)
# ----------------------------------------------------------------------------
# PostgreSQL connection string (for postgres, neon, supabase)
# Format: postgresql://user:password@host:port/database
DATABASE_URL=postgres://user:password@localhost:5432/betterbase
POSTGRES_USER=betterbase
POSTGRES_PASSWORD=your_secure_password_here
POSTGRES_DB=betterbase

# Or for Neon (serverless PostgreSQL)
# DATABASE_URL=postgres://user:password@ep-xxx.us-east-1.aws.neon.tech/neondb?sslmode=require

# Or for Turso (libSQL)
# TURSO_URL=libsql://your-database.turso.io
# TURSO_AUTH_TOKEN=your-auth-token
DATABASE_URL=postgresql://betterbase:your_secure_password_here@localhost:5432/betterbase
Comment on lines +11 to +15
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

DATABASE_URL password is hardcoded, not derived from POSTGRES_PASSWORD.

POSTGRES_PASSWORD is set to your_secure_password_here but DATABASE_URL has the same literal string hardcoded. Users who change POSTGRES_PASSWORD may forget to update DATABASE_URL, causing connection failures.

Suggested improvement
 POSTGRES_USER=betterbase
 POSTGRES_PASSWORD=your_secure_password_here
 POSTGRES_DB=betterbase
 
-DATABASE_URL=postgresql://betterbase:your_secure_password_here@localhost:5432/betterbase
+# Construct from above values: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}
+DATABASE_URL=postgresql://betterbase:your_secure_password_here@localhost:5432/betterbase
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 12-12: [UnorderedKey] The POSTGRES_PASSWORD key should go before the POSTGRES_USER key

(UnorderedKey)


[warning] 13-13: [UnorderedKey] The POSTGRES_DB key should go before the POSTGRES_PASSWORD key

(UnorderedKey)

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

In @.env.example around lines 11 - 15, The DATABASE_URL in .env.example is
hardcoded and will diverge from POSTGRES_PASSWORD if users change it; update
DATABASE_URL to reference the existing variables so it derives its password (and
user/db) dynamically, e.g. replace the literal URL with a template that uses
POSTGRES_USER, POSTGRES_PASSWORD, and POSTGRES_DB (and optionally
POSTGRES_HOST/POSTGRES_PORT) such as
postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@localhost:5432/${POSTGRES_DB}
so changes to POSTGRES_PASSWORD automatically apply.


# ----------------------------------------------------------------------------
# Required: Authentication
# Authentication
# ----------------------------------------------------------------------------
# Generate a secure secret: openssl rand -base64 32
AUTH_SECRET=your-super-secret-key-min-32-chars-long-change-in-production
# Generate: openssl rand -base64 32
AUTH_SECRET=your-super-secret-key-min-32-chars-long
AUTH_URL=http://localhost:3000
Comment on lines 17 to 22
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 | 🔴 Critical

Environment variable names don't match server code.

Lines 21-22 define AUTH_SECRET and AUTH_URL, but the server's env.ts schema expects BETTERBASE_JWT_SECRET and BETTERBASE_PUBLIC_URL. Same issue with CORS_ORIGIN (line 51) vs expected CORS_ORIGINS.

Either update the server code or use the expected variable names here.

Proposed fix to match server expectations
 # ----------------------------------------------------------------------------
 # Authentication
 # ----------------------------------------------------------------------------
 # Generate: openssl rand -base64 32
-AUTH_SECRET=your-super-secret-key-min-32-chars-long
-AUTH_URL=http://localhost:3000
+BETTERBASE_JWT_SECRET=your-super-secret-key-min-32-chars-long
+BETTERBASE_PUBLIC_URL=http://localhost:3000
-CORS_ORIGIN=http://localhost:3000,http://localhost:5173
+CORS_ORIGINS=http://localhost:3000,http://localhost:5173
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.env.example around lines 17 - 22, Replace the mismatched environment
variable names in .env.example to align with the server's env.ts schema: rename
AUTH_SECRET to BETTERBASE_JWT_SECRET, AUTH_URL to BETTERBASE_PUBLIC_URL, and
CORS_ORIGIN to CORS_ORIGINS; keep the same example values and comments (e.g.,
generation hint for the secret) and ensure CORS_ORIGINS uses the expected format
(comma-separated or array as env.ts expects) so the server reads the variables
correctly.


# ----------------------------------------------------------------------------
# Optional: Storage (S3-compatible)
# MinIO (S3-compatible storage)
# ----------------------------------------------------------------------------
# Provider: s3, r2, backblaze, minio
STORAGE_PROVIDER=r2
STORAGE_BUCKET=betterbase-storage
STORAGE_REGION=auto
STORAGE_ENDPOINT=https://your-r2-endpoint.r2.cloudflarestorage.com
STORAGE_ACCESS_KEY_ID=your-access-key
STORAGE_SECRET_ACCESS_KEY=your-secret-key
MINIO_ROOT_USER=minioadmin
MINIO_ROOT_PASSWORD=minioadmin_password

STORAGE_ENDPOINT=http://localhost:9000
STORAGE_ACCESS_KEY=minioadmin
STORAGE_SECRET_KEY=minioadmin_password
STORAGE_BUCKET=betterbase

# ----------------------------------------------------------------------------
# Optional: Email (SMTP)
# Inngest (Durable Workflow Engine)
# ----------------------------------------------------------------------------
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your-smtp-user
SMTP_PASS=your-smtp-password
SMTP_FROM=noreply@your-domain.com
INNGEST_LOG_LEVEL=info
# Generate: openssl rand -hex 32
INNGEST_SIGNING_KEY=
# Generate: openssl rand -hex 16
INNGEST_EVENT_KEY=

# ----------------------------------------------------------------------------
# Optional: OAuth Providers
# Application Settings
# ----------------------------------------------------------------------------
# GitHub
# GITHUB_CLIENT_ID=your-github-client-id
# GITHUB_CLIENT_SECRET=your-github-client-secret
NODE_ENV=development
PORT=3001
HOST=0.0.0.0

# Google
# GOOGLE_CLIENT_ID=your-google-client-id
# GOOGLE_CLIENT_SECRET=your-google-client-secret
CORS_ORIGIN=http://localhost:3000,http://localhost:5173

# Discord
# DISCORD_CLIENT_ID=your-discord-client-id
# DISCORD_CLIENT_SECRET=your-discord-client-secret
LOG_LEVEL=debug

# ----------------------------------------------------------------------------
# Optional: Phone Auth (Twilio)
# Storage (S3-compatible - for production with external services)
# ----------------------------------------------------------------------------
# TWILIO_ACCOUNT_SID=your-twilio-sid
# TWILIO_AUTH_TOKEN=your-twilio-token
# TWILIO_PHONE_NUMBER=+1234567890
# STORAGE_PROVIDER=r2
# STORAGE_BUCKET=betterbase-storage
# STORAGE_REGION=auto
# STORAGE_ENDPOINT=https://your-r2-endpoint.r2.cloudflarestorage.com
# STORAGE_ACCESS_KEY_ID=your-access-key
# STORAGE_SECRET_ACCESS_KEY=your-secret-key

# ----------------------------------------------------------------------------
# Application Settings
# Email (optional)
# ----------------------------------------------------------------------------
NODE_ENV=development
PORT=3000
HOST=0.0.0.0
# SMTP_HOST=smtp.example.com
# SMTP_PORT=587
# SMTP_USER=your-smtp-user
# SMTP_PASS=your-smtp-password
# SMTP_FROM=noreply@your-domain.com

# Comma-separated list of allowed CORS origins
CORS_ORIGIN=http://localhost:3000,http://localhost:5173
# ----------------------------------------------------------------------------
# OAuth Providers (optional)
# ----------------------------------------------------------------------------
# GITHUB_CLIENT_ID=your-github-client-id
# GITHUB_CLIENT_SECRET=your-github-client-secret

# Logging
LOG_LEVEL=debug
# GOOGLE_CLIENT_ID=your-google-client-id
# GOOGLE_CLIENT_SECRET=your-google-client-secret

# ----------------------------------------------------------------------------
# Vector Search (optional)
# Phone Auth (optional - Twilio)
# ----------------------------------------------------------------------------
# VECTOR_PROVIDER=openai
# OPENAI_API_KEY=your-openai-api-key
# TWILIO_ACCOUNT_SID=your-twilio-sid
# TWILIO_AUTH_TOKEN=your-twilio-token
# TWILIO_PHONE_NUMBER=+1234567890

# ----------------------------------------------------------------------------
# Inngest (Durable Workflow Engine)
# Vector Search (optional)
# ----------------------------------------------------------------------------
# For local development with Inngest Docker:
# INNGEST_BASE_URL=http://localhost:8288
#
# For self-hosted production:
# INNGEST_BASE_URL=http://inngest:8288
# Generate signing key: openssl rand -hex 32
# INNGEST_SIGNING_KEY=change-me-to-a-random-hex-string
# Generate event key: openssl rand -hex 16
# INNGEST_EVENT_KEY=change-me-to-another-random-hex-string
# Log level (debug | info | warn | error)
# INNGEST_LOG_LEVEL=info
# VECTOR_PROVIDER=openai
# OPENAI_API_KEY=your-openai-api-key
Comment on lines +93 to +94
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Missing trailing newline.

Static analysis flags missing blank line at end of file (EndingBlankLine).

Fix
 # OPENAI_API_KEY=your-openai-api-key
+
📝 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
# VECTOR_PROVIDER=openai
# OPENAI_API_KEY=your-openai-api-key
# VECTOR_PROVIDER=openai
# OPENAI_API_KEY=your-openai-api-key
🧰 Tools
🪛 dotenv-linter (4.0.0)

[warning] 94-94: [EndingBlankLine] No blank line at the end of the file

(EndingBlankLine)

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

In @.env.example around lines 93 - 94, Add a single trailing newline at the end
of the .env.example file so the file ends with a blank line; ensure the last
lines containing the commented variables VECTOR_PROVIDER and OPENAI_API_KEY are
followed by a newline character to satisfy EndingBlankLine static analysis.

53 changes: 53 additions & 0 deletions apps/dashboard/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# ============================================================================
# Betterbase Dashboard Dockerfile
#
# Builds the React dashboard and serves it with nginx.
# Used in production self-hosted deployments.
#
# Build:
# docker build -f apps/dashboard/Dockerfile -t betterbase-dashboard .
# ============================================================================

FROM oven/bun:1.3.9-debian AS builder

WORKDIR /app

COPY package.json bun.lock* ./

RUN bun install --frozen-lockfile

COPY . .

ARG VITE_API_URL=http://localhost:3001
ENV VITE_API_URL=$VITE_API_URL

RUN bun run build

FROM nginx:alpine

COPY --from=builder /app/dist /usr/share/nginx/html
Comment on lines +13 to +28
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 | 🔴 Critical

Build context and paths are incorrect for monorepo structure.

The build instructions specify building from repo root (docker build -f apps/dashboard/Dockerfile -t betterbase-dashboard .), so:

  1. Line 15 copies root package.json, not apps/dashboard/package.json
  2. Line 17 bun install --frozen-lockfile installs root deps, may fail if bun.lock is optional (bun.lock* glob)
  3. Line 19 COPY . . copies entire monorepo
  4. Line 24 bun run build runs root's build script, not dashboard's
  5. Line 28 expects dist at /app/dist but dashboard build outputs to /app/apps/dashboard/dist
Proposed fix for monorepo-aware build
 FROM oven/bun:1.3.9-debian AS builder
 
 WORKDIR /app
 
-COPY package.json bun.lock* ./
+COPY package.json bun.lock ./
+COPY apps/dashboard/package.json ./apps/dashboard/
 
 RUN bun install --frozen-lockfile
 
 COPY . .
 
+WORKDIR /app/apps/dashboard
+
 ARG VITE_API_URL=http://localhost:3001
 ENV VITE_API_URL=$VITE_API_URL
 
-RUN bun run build
+RUN cd /app/apps/dashboard && bun run build
 
 FROM nginx:alpine
 
-COPY --from=builder /app/dist /usr/share/nginx/html
+COPY --from=builder /app/apps/dashboard/dist /usr/share/nginx/html
📝 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
WORKDIR /app
COPY package.json bun.lock* ./
RUN bun install --frozen-lockfile
COPY . .
ARG VITE_API_URL=http://localhost:3001
ENV VITE_API_URL=$VITE_API_URL
RUN bun run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
WORKDIR /app
COPY package.json bun.lock ./
COPY apps/dashboard/package.json ./apps/dashboard/
RUN bun install --frozen-lockfile
COPY . .
WORKDIR /app/apps/dashboard
ARG VITE_API_URL=http://localhost:3001
ENV VITE_API_URL=$VITE_API_URL
RUN cd /app/apps/dashboard && bun run build
FROM nginx:alpine
COPY --from=builder /app/apps/dashboard/dist /usr/share/nginx/html
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/Dockerfile` around lines 13 - 28, The Dockerfile is copying
root files and building from repo root; change it to build specifically for the
dashboard by setting WORKDIR to the dashboard folder, copy only the dashboard
package files, run bun install and build in that context, and copy the dashboard
dist to nginx. Concretely: set WORKDIR /app/apps/dashboard (or copy
apps/dashboard into /app), replace COPY package.json bun.lock* ./ with COPY
apps/dashboard/package.json apps/dashboard/bun.lock* ./ (or COPY apps/dashboard
. .), run RUN bun install --frozen-lockfile in that dashboard workdir, run RUN
bun run build (which will now run the dashboard build script), and update the
final stage COPY --from=builder to copy /app/apps/dashboard/dist (or /app/dist
if you build into /app) to /usr/share/nginx/html so the built dashboard assets
are used.


RUN cat > /etc/nginx/conf.d/default.conf << 'EOF'
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;

location /assets/ {
expires 1y;
add_header Cache-Control "public, immutable";
}

location / {
try_files $uri $uri/ /index.html;
}

add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
}
EOF

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]
Comment on lines +26 to +53
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Add healthcheck and non-root user for production hardening.

The nginx runtime stage lacks a healthcheck and runs as root. For production self-hosted deployments, these are recommended.

Proposed additions
 FROM nginx:alpine
 
 COPY --from=builder /app/apps/dashboard/dist /usr/share/nginx/html
 
 RUN cat > /etc/nginx/conf.d/default.conf << 'EOF'
 ...
 EOF
 
+RUN chown -R nginx:nginx /usr/share/nginx/html
+USER nginx
+
+HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
+  CMD wget -qO- http://localhost:80/ || exit 1
+
 EXPOSE 80
 
 CMD ["nginx", "-g", "daemon off;"]
🧰 Tools
🪛 Checkov (3.2.510)

[low] 1-53: Ensure that HEALTHCHECK instructions have been added to container images

(CKV_DOCKER_2)


[low] 1-53: Ensure that a user for the container has been created

(CKV_DOCKER_3)

🪛 Hadolint (2.14.0)

[error] 31-31: unexpected 's'
expecting '#', '', ADD, ARG, CMD, COPY, ENTRYPOINT, ENV, EXPOSE, FROM, HEALTHCHECK, LABEL, MAINTAINER, ONBUILD, RUN, SHELL, STOPSIGNAL, USER, VOLUME, WORKDIR, a pragma, at least one space, or end of input

(DL1000)

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

In `@apps/dashboard/Dockerfile` around lines 26 - 53, Add a non-root runtime user
and a container HEALTHCHECK: create a limited user and group, chown
/usr/share/nginx/html and any needed runtime dirs, switch to that user before
the CMD (reference the existing RUN ... /etc/nginx/conf.d/default.conf block and
CMD ["nginx", "-g", "daemon off;"] and EXPOSE 80), and add a HEALTHCHECK
instruction that probes the nginx HTTP endpoint (e.g., curl -f
http://localhost:80/health or /) to return success; also ensure the nginx config
(the server block written by RUN cat > /etc/nginx/conf.d/default.conf) includes
a simple /health location that returns 200 so the healthcheck works.

95 changes: 48 additions & 47 deletions docker-compose.self-hosted.yml
Original file line number Diff line number Diff line change
@@ -1,34 +1,49 @@
version: "3.9"
# ============================================================================
# Betterbase Self-Hosted Production Docker Compose
#
# Full self-hosted deployment with all services.
# No external dependencies required.
#
# Usage:
# 1. Build: bun run build
# 2. cp .env.example .env && edit .env
# 3. docker compose -f docker-compose.self-hosted.yml up -d
#
# Services:
# - PostgreSQL: localhost:5432
# - MinIO: localhost:9000 (console: localhost:9001)
# - Inngest: localhost:8288
# - Server: localhost:3001
# - Dashboard: localhost (via nginx)
# ============================================================================

services:
# ─── Postgres ──────────────────────────────────────────────────────────────
postgres:
image: postgres:16-alpine
container_name: betterbase-postgres
restart: unless-stopped
environment:
POSTGRES_USER: betterbase
POSTGRES_USER: ${POSTGRES_USER:-betterbase}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-betterbase}
POSTGRES_DB: betterbase
POSTGRES_DB: ${POSTGRES_DB:-betterbase}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U betterbase"]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-betterbase}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- betterbase-internal

# ─── MinIO (S3-compatible storage) ─────────────────────────────────────────
minio:
image: minio/minio:RELEASE.2024-01-16T16-07-38Z
image: minio/minio:latest
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unpinned MinIO images may cause unexpected breakage.

minio/minio:latest and minio/mc:latest can introduce breaking changes. Consider pinning to a specific release (e.g., RELEASE.2024-*) for production stability.

Also applies to: 58-58

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

In `@docker-compose.self-hosted.yml` at line 40, Replace the unpinned MinIO images
"minio/minio:latest" and "minio/mc:latest" with specific release tags (e.g.,
"minio/minio:RELEASE.2024-<x>" and "minio/mc:RELEASE.2024-<x>") to avoid
unexpected breaking changes; update the two occurrences of image:
minio/minio:latest and image: minio/mc:latest in the compose file to use pinned
RELEASE.* tags (or configurable env vars) so production deployments use a fixed,
tested MinIO release.

container_name: betterbase-minio
restart: unless-stopped
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: ${STORAGE_ACCESS_KEY:-minioadmin}
MINIO_ROOT_PASSWORD: ${STORAGE_SECRET_KEY:-minioadmin}
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
volumes:
- minio_data:/data
healthcheck:
Expand All @@ -39,41 +54,33 @@ services:
networks:
- betterbase-internal

# ─── MinIO bucket init (runs once, exits) ──────────────────────────────────
minio-init:
image: minio/mc:RELEASE.2024-01-06T18-51-57Z
image: minio/mc:latest
container_name: betterbase-minio-init
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://minio:9000 ${STORAGE_ACCESS_KEY:-minioadmin} ${STORAGE_SECRET_KEY:-minioadmin};
mc alias set local http://minio:9000 ${MINIO_ROOT_USER:-minioadmin} ${MINIO_ROOT_PASSWORD:-minioadmin};
mc mb --ignore-existing local/betterbase;
echo 'MinIO bucket initialized.';
"
networks:
- betterbase-internal

# ─── Inngest (Durable Workflow Engine) ────────────────────────────────────
inngest:
image: inngest/inngest:latest
image: inngest/inngest:v1.17.5
container_name: betterbase-inngest
restart: unless-stopped
command: inngest start --host 0.0.0.0 --port 8288
command: inngest dev --host 0.0.0.0 --port 8288
environment:
INNGEST_LOG_LEVEL: ${INNGEST_LOG_LEVEL:-info}
volumes:
- inngest_data:/data
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:8288/health || exit 1"]
interval: 10s
timeout: 5s
retries: 5
networks:
- betterbase-internal

# ─── Betterbase Server ─────────────────────────────────────────────────────
betterbase-server:
build:
context: .
Expand All @@ -88,64 +95,58 @@ services:
minio-init:
condition: service_completed_successfully
inngest:
condition: service_healthy
condition: service_started
environment:
DATABASE_URL: postgresql://betterbase:${POSTGRES_PASSWORD:-betterbase}@postgres:5432/betterbase
BETTERBASE_JWT_SECRET: ${BETTERBASE_JWT_SECRET:?JWT secret required - set BETTERBASE_JWT_SECRET in .env}
BETTERBASE_ADMIN_EMAIL: ${BETTERBASE_ADMIN_EMAIL:-}
BETTERBASE_ADMIN_PASSWORD: ${BETTERBASE_ADMIN_PASSWORD:-}
BETTERBASE_PUBLIC_URL: ${BETTERBASE_PUBLIC_URL:-http://localhost}
DATABASE_URL: postgresql://${POSTGRES_USER:-betterbase}:${POSTGRES_PASSWORD:-betterbase}@postgres:5432/${POSTGRES_DB:-betterbase}
AUTH_SECRET: ${AUTH_SECRET:?JWT secret required - set AUTH_SECRET in .env}
AUTH_URL: ${AUTH_URL:-http://localhost:3000}
STORAGE_ENDPOINT: http://minio:9000
STORAGE_ACCESS_KEY: ${STORAGE_ACCESS_KEY:-minioadmin}
STORAGE_SECRET_KEY: ${STORAGE_SECRET_KEY:-minioadmin}
STORAGE_ACCESS_KEY: ${MINIO_ROOT_USER:-minioadmin}
STORAGE_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-minioadmin}
STORAGE_BUCKET: betterbase
PORT: "3001"
NODE_ENV: production
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost}
CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost}
INNGEST_BASE_URL: http://inngest:8288
INNGEST_SIGNING_KEY: ${INNGEST_SIGNING_KEY}
INNGEST_EVENT_KEY: ${INNGEST_EVENT_KEY}
networks:
- betterbase-internal
INNGEST_SIGNING_KEY: ${INNGEST_SIGNING_KEY:-}
INNGEST_EVENT_KEY: ${INNGEST_EVENT_KEY:-}
Comment on lines 99 to +112
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 | 🔴 Critical

Critical: Environment variable names don't match server expectations.

The server's env.ts Zod schema and code expect:

  • BETTERBASE_JWT_SECRET (not AUTH_SECRET) — auth.ts line 5 reads this directly
  • BETTERBASE_PUBLIC_URL (not AUTH_URL) — routes/device/index.ts line 27
  • CORS_ORIGINS plural (not CORS_ORIGIN) — index.ts line 55

With current config, the server will either fail Zod validation (missing required BETTERBASE_JWT_SECRET) or silently use localhost defaults.

Proposed fix to match server expectations
     environment:
       DATABASE_URL: postgresql://${POSTGRES_USER:-betterbase}:${POSTGRES_PASSWORD:-betterbase}@postgres:5432/${POSTGRES_DB:-betterbase}
-      AUTH_SECRET: ${AUTH_SECRET:?JWT secret required - set AUTH_SECRET in .env}
-      AUTH_URL: ${AUTH_URL:-http://localhost:3000}
+      BETTERBASE_JWT_SECRET: ${BETTERBASE_JWT_SECRET:?JWT secret required - set BETTERBASE_JWT_SECRET in .env}
+      BETTERBASE_PUBLIC_URL: ${BETTERBASE_PUBLIC_URL:-http://localhost:3000}
       STORAGE_ENDPOINT: http://minio:9000
       STORAGE_ACCESS_KEY: ${MINIO_ROOT_USER:-minioadmin}
       STORAGE_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-minioadmin}
       STORAGE_BUCKET: betterbase
       PORT: "3001"
       NODE_ENV: production
-      CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost}
+      CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost}
       INNGEST_BASE_URL: http://inngest:8288
       INNGEST_SIGNING_KEY: ${INNGEST_SIGNING_KEY:-}
       INNGEST_EVENT_KEY: ${INNGEST_EVENT_KEY:-}

As per coding guidelines: "BETTERBASE_JWT_SECRET must be at least 32 chars — enforced by env.ts Zod schema."

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

In `@docker-compose.self-hosted.yml` around lines 99 - 112, Update the environment
variable names and value constraints in the docker-compose service to match the
server's expectations: replace AUTH_SECRET with BETTERBASE_JWT_SECRET (ensure
this secret is at least 32 chars to satisfy the env.ts Zod schema), replace
AUTH_URL with BETTERBASE_PUBLIC_URL, and replace CORS_ORIGIN with CORS_ORIGINS
(plural); verify these map to the same values currently provided (e.g., keep
existing defaults like http://localhost or the MinIO settings) so auth.ts (which
reads BETTERBASE_JWT_SECRET), routes/device/index.ts (which reads
BETTERBASE_PUBLIC_URL) and index.ts (which reads CORS_ORIGINS) pass Zod
validation and runtime checks.

healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:3001/health || exit 1"]
interval: 10s
timeout: 5s
retries: 5
networks:
- betterbase-internal

# ─── Dashboard ─────────────────────────────────────────────────────────────
betterbase-dashboard:
build:
context: .
dockerfile: apps/dashboard/Dockerfile # Dashboard Dockerfile — see SH-25
args:
VITE_API_URL: ${BETTERBASE_PUBLIC_URL:-http://localhost}
dashboard:
image: nginx:alpine
container_name: betterbase-dashboard
restart: unless-stopped
volumes:
- ./apps/dashboard/dist:/usr/share/nginx/html:ro
depends_on:
betterbase-server:
condition: service_healthy
- betterbase-server
networks:
- betterbase-internal
Comment on lines +121 to 130
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

Dashboard service requires pre-built assets.

The dashboard service mounts ./apps/dashboard/dist:/usr/share/nginx/html:ro, requiring users to run bun run build in apps/dashboard before starting compose. The header comments mention bun run build but this builds the server, not the dashboard.

Consider documenting explicitly or adding a build step to the instructions.

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

In `@docker-compose.self-hosted.yml` around lines 121 - 130, The docker-compose
dashboard service mounts pre-built frontend assets at
./apps/dashboard/dist:/usr/share/nginx/html:ro (service name "dashboard"), but
the README only mentions running "bun run build" for the server; update
documentation or add an explicit build step: instruct users to run "bun --cwd
apps/dashboard run build" (or equivalent) to produce ./apps/dashboard/dist
before starting docker-compose, or add a compose build step (e.g., a build stage
or an auxiliary service) that runs the dashboard build automatically so the
volume has the required files.


# ─── Nginx Reverse Proxy ───────────────────────────────────────────────────
nginx:
image: nginx:alpine
container_name: betterbase-nginx
restart: unless-stopped
depends_on:
- betterbase-server
- betterbase-dashboard
- dashboard
ports:
- "${HTTP_PORT:-80}:80"
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- betterbase-internal
healthcheck:
test: ["CMD", "nginx", "-t"]
interval: 30s
timeout: 10s
retries: 3
networks:
- betterbase-internal

volumes:
postgres_data:
Expand Down
Loading
Loading