From 7f78e70a35eb99f928f92b23be90f9e22b2a1fca Mon Sep 17 00:00:00 2001
From: Davidutz_ <71325082+DavidutzDev@users.noreply.github.com>
Date: Tue, 24 Feb 2026 22:53:49 +0100
Subject: [PATCH 1/6] fix: make prisma provider configurable by env
DATABASE_PROVIDER (#71)
---
backend/prisma/schema.prisma | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma
index 3e2bb588..c3ab2425 100644
--- a/backend/prisma/schema.prisma
+++ b/backend/prisma/schema.prisma
@@ -5,7 +5,7 @@ generator client {
}
datasource db {
- provider = "sqlite"
+ provider = env("DATABASE_PROVIDER")
url = env("DATABASE_URL")
}
From be21e8323ccc2a2e5efb04dcaf658e2ba9fbfec5 Mon Sep 17 00:00:00 2001
From: Davidutz_ <71325082+DavidutzDev@users.noreply.github.com>
Date: Tue, 24 Feb 2026 22:58:36 +0100
Subject: [PATCH 2/6] docs: Documenting the env var for DATABASE_PROVIDER (#71)
---
backend/.env.example | 1 +
backend/vitest.config.ts | 1 +
docker-compose.prod.yml | 1 +
docker-compose.yml | 1 +
4 files changed, 4 insertions(+)
diff --git a/backend/.env.example b/backend/.env.example
index aae43fff..455c32ac 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -1,6 +1,7 @@
# Backend Environment Variables
PORT=8000
NODE_ENV=production
+DATABASE_PROVIDER=sqlite
DATABASE_URL=file:/app/prisma/dev.db
FRONTEND_URL=http://localhost:6767
# Keep disabled unless traffic always comes through a trusted reverse proxy.
diff --git a/backend/vitest.config.ts b/backend/vitest.config.ts
index 728ed29a..0e50c81d 100644
--- a/backend/vitest.config.ts
+++ b/backend/vitest.config.ts
@@ -8,6 +8,7 @@ export default defineConfig({
testTimeout: 30000,
hookTimeout: 30000,
env: {
+ DATABASE_PROVIDER: "sqlite",
DATABASE_URL: "file:./prisma/test.db",
NODE_ENV: "test",
AUTH_MODE: "local",
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 0a6a8f76..71e8d22b 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -3,6 +3,7 @@ services:
image: zimengxiong/excalidash-backend:latest
container_name: excalidash-backend
environment:
+ - DATABASE_PROVIDER=sqlite
- DATABASE_URL=file:/app/prisma/dev.db
- PORT=8000
- NODE_ENV=production
diff --git a/docker-compose.yml b/docker-compose.yml
index bc75cc3f..5294f109 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -5,6 +5,7 @@ services:
dockerfile: Dockerfile
container_name: excalidash-backend
environment:
+ - DATABASE_PROVIDER=sqlite
- DATABASE_URL=file:/app/prisma/dev.db
- PORT=8000
- NODE_ENV=production
From acd5e94acdc0f807ca293d5e880d5a763d82078e Mon Sep 17 00:00:00 2001
From: Davidutz_ <71325082+DavidutzDev@users.noreply.github.com>
Date: Tue, 24 Feb 2026 23:42:05 +0100
Subject: [PATCH 3/6] feat(docker): add DATABASE_PROVIDER env var support for
runtime provider switching
Allows container to use sqlite (default) or postgresql by setting
DATABASE_PROVIDER and DATABASE_URL environment variables at runtime.
---
backend/Dockerfile | 8 ++++++--
backend/docker-entrypoint.sh | 19 +++++++++++++++++++
2 files changed, 25 insertions(+), 2 deletions(-)
diff --git a/backend/Dockerfile b/backend/Dockerfile
index e50b0eec..bbeb1b22 100644
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -16,7 +16,11 @@ RUN npm ci && npm cache clean --force
# Copy prisma schema
COPY prisma ./prisma/
-# Generate Prisma Client
+# Set default provider for build-time Prisma generation (runtime can override via DATABASE_PROVIDER)
+RUN sed -i 's/provider = env("[^"]*")/provider = "sqlite"/' prisma/schema.prisma
+
+# Generate Prisma Client at build time (for TypeScript compilation)
+# Runtime will regenerate if DATABASE_PROVIDER differs
RUN npx prisma generate
# Copy source code
@@ -51,7 +55,7 @@ COPY prisma ./prisma_template/
# Copy built application from builder
COPY --from=builder /app/dist ./dist
-# Copy the generated Prisma Client from builder to maintain the same structure
+# Copy the generated Prisma Client from builder (will be regenerated at runtime if provider changes)
COPY --from=builder /app/src/generated ./dist/generated
# Create necessary directories (ownership will be set in entrypoint)
diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh
index 894073a1..9e94fea2 100644
--- a/backend/docker-entrypoint.sh
+++ b/backend/docker-entrypoint.sh
@@ -64,6 +64,25 @@ mkdir -p /app/prisma/migrations
cp /app/prisma_template/schema.prisma /app/prisma/schema.prisma
cp -R /app/prisma_template/migrations/. /app/prisma/migrations/
+# Set default DATABASE_PROVIDER if not set
+if [ -z "${DATABASE_PROVIDER:-}" ]; then
+ echo "DATABASE_PROVIDER not set, defaulting to sqlite"
+ DATABASE_PROVIDER="sqlite"
+fi
+
+# Update schema.prisma with the runtime provider (handles both env() and static values)
+echo "Configuring Prisma for provider: ${DATABASE_PROVIDER}"
+sed -i 's/provider = env("[^"]*")/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma
+sed -i 's/provider = "[^"]*"/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma
+
+# Generate Prisma Client at runtime (run as root since schema is owned by root)
+echo "Generating Prisma Client..."
+npx prisma generate --schema=/app/prisma/schema.prisma
+
+# Copy generated client to the expected location for the application
+mkdir -p /app/dist/generated
+cp -r /app/src/generated/* /app/dist/generated/
+
# 2. Fix permissions unconditionally (Running as root)
echo "Fixing filesystem permissions..."
chown -R nodejs:nodejs /app/uploads
From 1c2a1c9b0c37f03a0dd25091501ad4dc3d164735 Mon Sep 17 00:00:00 2001
From: Davidutz_ <71325082+DavidutzDev@users.noreply.github.com>
Date: Wed, 25 Feb 2026 16:13:18 +0100
Subject: [PATCH 4/6] fix: I'm not sure how to deal this... Prisma is really
annoying on databases provider and it's not really a good idea to do like
this. This should only be a temporary fix
---
backend/docker-entrypoint.sh | 1 +
1 file changed, 1 insertion(+)
diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh
index 9e94fea2..eec234f6 100644
--- a/backend/docker-entrypoint.sh
+++ b/backend/docker-entrypoint.sh
@@ -74,6 +74,7 @@ fi
echo "Configuring Prisma for provider: ${DATABASE_PROVIDER}"
sed -i 's/provider = env("[^"]*")/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma
sed -i 's/provider = "[^"]*"/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma
+sed -i 's/^provider = ".*"/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/migrations/migration_lock.toml
# Generate Prisma Client at runtime (run as root since schema is owned by root)
echo "Generating Prisma Client..."
From 5d3e19ba37259c6a31de1828871f2f4a360c3e9a Mon Sep 17 00:00:00 2001
From: Davidutz_ <71325082+DavidutzDev@users.noreply.github.com>
Date: Wed, 25 Feb 2026 20:51:52 +0100
Subject: [PATCH 5/6] fix: Better implementation. Instead of messing with some
experimental stuff. Now we just create a separate migrations folder for
postgres and update docker
---
README.md | 77 +++++-
backend/.env.example | 1 +
backend/docker-entrypoint.sh | 36 +--
backend/package-lock.json | 11 +-
backend/prisma.config.ts | 12 -
.../20260225171236_init/migration.sql | 233 ++++++++++++++++++
.../migrations/postgresql/migration_lock.toml | 3 +
.../20251122021659_init/migration.sql | 0
.../20251122032308_add_preview/migration.sql | 0
.../migration.sql | 0
.../migration.sql | 0
.../migration.sql | 0
.../migration.sql | 0
.../migration.sql | 0
.../migration.sql | 0
.../migration.sql | 0
.../migration.sql | 0
.../migration.sql | 0
.../migration.sql | 0
.../migration.sql | 0
.../{ => sqlite}/migration_lock.toml | 0
docker-compose.pg-test.yml | 43 ++++
scripts/generate-migrations.sh | 147 +++++++++++
23 files changed, 523 insertions(+), 40 deletions(-)
delete mode 100644 backend/prisma.config.ts
create mode 100644 backend/prisma/migrations/postgresql/20260225171236_init/migration.sql
create mode 100644 backend/prisma/migrations/postgresql/migration_lock.toml
rename backend/prisma/migrations/{ => sqlite}/20251122021659_init/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20251122032308_add_preview/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20251122065455_add_files_column/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20251124220546_add_library_model/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20260124145151_add_user_auth/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20260124152839_add_password_reset_audit_refresh_tokens/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20260206173000_add_auth_enabled_system_config/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20260206195000_add_auth_login_rate_limit_config/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20260207000000_add_query_indexes/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20260210153000_add_auth_onboarding_completed/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20260210190000_add_auth_identity/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20260211232000_add_bootstrap_setup_code/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/20260217214759_drawing_sharing/migration.sql (100%)
rename backend/prisma/migrations/{ => sqlite}/migration_lock.toml (100%)
create mode 100644 docker-compose.pg-test.yml
create mode 100755 scripts/generate-migrations.sh
diff --git a/README.md b/README.md
index 73564c1e..def2221d 100644
--- a/README.md
+++ b/README.md
@@ -217,7 +217,7 @@ Why:
| Area | Limitation |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| Database | The backend uses a local **SQLite file** database by default (`DATABASE_URL=file:/.../dev.db`). Running multiple backend replicas either creates split-brain state (separate DB files/volumes) or requires sharing a single SQLite file across hosts, which is not a reliable deployment pattern. |
+| Database | The backend uses **SQLite** by default. For production deployments requiring high availability, use **PostgreSQL** (`DATABASE_PROVIDER=postgresql`). SQLite still works for single-instance deployments but is not recommended for multi-replica setups. |
| Collaboration | Real-time presence state is tracked **in-memory** in the backend process, so multiple replicas will fragment presence/collaboration unless a shared Socket.IO adapter is added. |
Recommended deployment pattern:
@@ -336,14 +336,73 @@ docker compose -f docker-compose.oidc.yml down
Base values are documented in `backend/.env.example`. Common ones to care about:
-| Variable | Default / Example | Description |
-| -------------- | ------------------------- | ----------------------------------------------------------------------------------- |
-| `DATABASE_URL` | `file:/app/prisma/dev.db` | SQLite file or external DB URL. |
-| `FRONTEND_URL` | `http://localhost:6767` | Allowed frontend origin(s), comma-separated for multiple entries. |
-| `TRUST_PROXY` | `false` | `false`, `true`, or hop count (for example `1`). |
-| `JWT_SECRET` | `change-this-secret...` | Recommended in production so sessions remain stable across restarts and migrations. |
-| `CSRF_SECRET` | `change-this-secret` | Recommended in production so CSRF validation remains stable across restarts. |
-| `AUTH_MODE` | `local` | `local`, `hybrid`, `oidc_enforced`. |
+| Variable | Default / Example | Description |
+| ------------------- | ------------------------- | ----------------------------------------------------------------------------------- |
+| `DATABASE_PROVIDER` | `sqlite` | Database provider: `sqlite` or `postgresql`. See [Database Provider](#database-provider) for details. |
+| `DATABASE_URL` | `file:/app/prisma/dev.db` | SQLite file or external DB URL. |
+| `FRONTEND_URL` | `http://localhost:6767` | Allowed frontend origin(s), comma-separated for multiple entries. |
+| `TRUST_PROXY` | `false` | `false`, `true`, or hop count (for example `1`). |
+| `JWT_SECRET` | `change-this-secret...` | Recommended in production so sessions remain stable across restarts and migrations. |
+| `CSRF_SECRET` | `change-this-secret` | Recommended in production so CSRF validation remains stable across restarts. |
+| `AUTH_MODE` | `local` | `local`, `hybrid`, `oidc_enforced`. |
+
+
+
+
+Database Provider
+
+## Database Provider
+
+ExcaliDash supports both **SQLite** (default) and **PostgreSQL** as database providers. The provider is controlled by the `DATABASE_PROVIDER` environment variable.
+
+### SQLite (Default)
+
+SQLite is the default and works out of the box without additional configuration:
+
+```yaml
+# docker-compose.prod.yml
+services:
+ backend:
+ environment:
+ - DATABASE_PROVIDER=sqlite
+ - DATABASE_URL=file:/app/prisma/dev.db
+```
+
+### PostgreSQL
+
+To use PostgreSQL instead of SQLite:
+
+1. **Set the environment variables:**
+
+```yaml
+# docker-compose.prod.yml
+services:
+ backend:
+ environment:
+ - DATABASE_PROVIDER=postgresql
+ - DATABASE_URL=postgresql://user:password@host:5432/excalidash
+```
+
+2. **Generate PostgreSQL migrations:**
+
+The repository includes SQLite migrations by default. To use PostgreSQL, you need to generate migrations for the PostgreSQL provider:
+
+```bash
+# 1. Set the provider in your local environment
+export DATABASE_PROVIDER=postgresql
+export DATABASE_URL="postgresql://user:password@localhost:5432/excalidash"
+
+# 2. Remove existing SQLite migrations
+rm -rf backend/prisma/migrations/postgresql
+
+# 3. Generate initial migration for PostgreSQL
+cd backend
+npx prisma migrate dev --name init
+```
+
+This creates a new `backend/prisma/migrations/postgresql/` folder with PostgreSQL-specific migrations.
+
+**Note:** When switching database providers, you cannot migrate existing data. The new database will start empty.
diff --git a/backend/.env.example b/backend/.env.example
index 455c32ac..ea270d81 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -1,6 +1,7 @@
# Backend Environment Variables
PORT=8000
NODE_ENV=production
+# Database provider: sqlite or postgresql
DATABASE_PROVIDER=sqlite
DATABASE_URL=file:/app/prisma/dev.db
FRONTEND_URL=http://localhost:6767
diff --git a/backend/docker-entrypoint.sh b/backend/docker-entrypoint.sh
index eec234f6..436d4dfb 100644
--- a/backend/docker-entrypoint.sh
+++ b/backend/docker-entrypoint.sh
@@ -50,31 +50,33 @@ fi
export CSRF_SECRET
-# 1. Ensure schema and migrations are present (Running as root)
-# Never copy the entire prisma directory, as that can unintentionally overwrite
-# persisted SQLite files or copy stray *.db artifacts into the volume.
-if [ ! -f "/app/prisma/schema.prisma" ]; then
- echo "Mount appears empty (missing schema.prisma). Bootstrapping schema and migrations..."
-else
- # Volume exists but may be missing new migrations from an upgrade.
- echo "Syncing schema and migrations from template..."
-fi
-
-mkdir -p /app/prisma/migrations
-cp /app/prisma_template/schema.prisma /app/prisma/schema.prisma
-cp -R /app/prisma_template/migrations/. /app/prisma/migrations/
-
# Set default DATABASE_PROVIDER if not set
if [ -z "${DATABASE_PROVIDER:-}" ]; then
echo "DATABASE_PROVIDER not set, defaulting to sqlite"
DATABASE_PROVIDER="sqlite"
fi
+# Validate DATABASE_PROVIDER
+if [ "${DATABASE_PROVIDER}" != "sqlite" ] && [ "${DATABASE_PROVIDER}" != "postgresql" ]; then
+ echo "ERROR: DATABASE_PROVIDER must be 'sqlite' or 'postgresql', got '${DATABASE_PROVIDER}'"
+ exit 1
+fi
+
+# Ensure migrations directory exists
+mkdir -p /app/prisma/migrations
+
+# Copy schema.prisma from template
+cp /app/prisma_template/schema.prisma /app/prisma/schema.prisma
+
+# Clear and copy provider-specific migrations folder
+echo "Copying ${DATABASE_PROVIDER} migrations..."
+rm -rf /app/prisma/migrations/*
+cp -R /app/prisma_template/migrations/"${DATABASE_PROVIDER}"/. /app/prisma/migrations/
+
# Update schema.prisma with the runtime provider (handles both env() and static values)
echo "Configuring Prisma for provider: ${DATABASE_PROVIDER}"
sed -i 's/provider = env("[^"]*")/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma
sed -i 's/provider = "[^"]*"/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/schema.prisma
-sed -i 's/^provider = ".*"/provider = "'"${DATABASE_PROVIDER}"'"/' /app/prisma/migrations/migration_lock.toml
# Generate Prisma Client at runtime (run as root since schema is owned by root)
echo "Generating Prisma Client..."
@@ -86,9 +88,9 @@ cp -r /app/src/generated/* /app/dist/generated/
# 2. Fix permissions unconditionally (Running as root)
echo "Fixing filesystem permissions..."
-chown -R nodejs:nodejs /app/uploads
-chown -R nodejs:nodejs /app/prisma
+chown -R nodejs:nodejs /app/uploads /app/prisma /app/dist/generated
chmod 755 /app/uploads
+chmod -R 755 /app/dist/generated
chmod 600 "${JWT_SECRET_FILE}"
chmod 600 "${CSRF_SECRET_FILE}"
diff --git a/backend/package-lock.json b/backend/package-lock.json
index ddaa75c7..1b579b58 100644
--- a/backend/package-lock.json
+++ b/backend/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "backend",
- "version": "0.4.7",
+ "version": "0.4.27",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "backend",
- "version": "0.4.7",
+ "version": "0.4.27",
"license": "ISC",
"dependencies": {
"@prisma/client": "^5.22.0",
@@ -1177,6 +1177,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz",
"integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -2676,6 +2677,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.0.tgz",
"integrity": "sha512-XdpJDLxfztVY59X0zPI6sibRiGcxhTPXRD3IhJmjKf2jwMvkRGV1j7loB8U+heeamoU3XvihAaGRTR4aXXUN3A==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -4164,6 +4166,7 @@
"integrity": "sha512-vtpjW3XuYCSnMsNVBjLMNkTj6OZbudcPPTPYHqX0CJfpcdWciI1dM8uHETwmDxxiqEwCIE6WvXucWUetJgfu/A==",
"hasInstallScript": true,
"license": "Apache-2.0",
+ "peer": true,
"dependencies": {
"@prisma/engines": "5.22.0"
},
@@ -5176,6 +5179,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -5334,6 +5338,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5424,6 +5429,7 @@
"integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -5517,6 +5523,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
diff --git a/backend/prisma.config.ts b/backend/prisma.config.ts
deleted file mode 100644
index d73df7b3..00000000
--- a/backend/prisma.config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import "dotenv/config";
-import { defineConfig, env } from "prisma/config";
-
-export default defineConfig({
- schema: "prisma/schema.prisma",
- migrations: {
- path: "prisma/migrations",
- },
- datasource: {
- url: env("DATABASE_URL"),
- },
-});
diff --git a/backend/prisma/migrations/postgresql/20260225171236_init/migration.sql b/backend/prisma/migrations/postgresql/20260225171236_init/migration.sql
new file mode 100644
index 00000000..6f0327d2
--- /dev/null
+++ b/backend/prisma/migrations/postgresql/20260225171236_init/migration.sql
@@ -0,0 +1,233 @@
+-- CreateTable
+CREATE TABLE "User" (
+ "id" TEXT NOT NULL,
+ "username" TEXT,
+ "email" TEXT NOT NULL,
+ "passwordHash" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "role" TEXT NOT NULL DEFAULT 'USER',
+ "mustResetPassword" BOOLEAN NOT NULL DEFAULT false,
+ "isActive" BOOLEAN NOT NULL DEFAULT true,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "User_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "SystemConfig" (
+ "id" TEXT NOT NULL DEFAULT 'default',
+ "authEnabled" BOOLEAN NOT NULL DEFAULT false,
+ "authOnboardingCompleted" BOOLEAN NOT NULL DEFAULT false,
+ "registrationEnabled" BOOLEAN NOT NULL DEFAULT false,
+ "authLoginRateLimitEnabled" BOOLEAN NOT NULL DEFAULT true,
+ "authLoginRateLimitWindowMs" INTEGER NOT NULL DEFAULT 900000,
+ "authLoginRateLimitMax" INTEGER NOT NULL DEFAULT 20,
+ "bootstrapSetupCodeHash" TEXT,
+ "bootstrapSetupCodeIssuedAt" TIMESTAMP(3),
+ "bootstrapSetupCodeExpiresAt" TIMESTAMP(3),
+ "bootstrapSetupCodeFailedAttempts" INTEGER NOT NULL DEFAULT 0,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "SystemConfig_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Collection" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Collection_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Drawing" (
+ "id" TEXT NOT NULL,
+ "name" TEXT NOT NULL,
+ "elements" TEXT NOT NULL,
+ "appState" TEXT NOT NULL,
+ "files" TEXT NOT NULL DEFAULT '{}',
+ "preview" TEXT,
+ "version" INTEGER NOT NULL DEFAULT 1,
+ "userId" TEXT NOT NULL,
+ "collectionId" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Drawing_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DrawingPermission" (
+ "id" TEXT NOT NULL,
+ "drawingId" TEXT NOT NULL,
+ "granteeUserId" TEXT NOT NULL,
+ "permission" TEXT NOT NULL,
+ "createdByUserId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "DrawingPermission_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "DrawingLinkShare" (
+ "id" TEXT NOT NULL,
+ "drawingId" TEXT NOT NULL,
+ "permission" TEXT NOT NULL,
+ "tokenHash" TEXT NOT NULL,
+ "passphraseHash" TEXT,
+ "expiresAt" TIMESTAMP(3),
+ "revokedAt" TIMESTAMP(3),
+ "failedAttempts" INTEGER NOT NULL DEFAULT 0,
+ "lockedUntil" TIMESTAMP(3),
+ "lastUsedAt" TIMESTAMP(3),
+ "lastUsedIp" TEXT,
+ "createdByUserId" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "DrawingLinkShare_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "Library" (
+ "id" TEXT NOT NULL,
+ "items" TEXT NOT NULL DEFAULT '[]',
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "Library_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "PasswordResetToken" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "token" TEXT NOT NULL,
+ "expiresAt" TIMESTAMP(3) NOT NULL,
+ "used" BOOLEAN NOT NULL DEFAULT false,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "PasswordResetToken_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "RefreshToken" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "token" TEXT NOT NULL,
+ "expiresAt" TIMESTAMP(3) NOT NULL,
+ "revoked" BOOLEAN NOT NULL DEFAULT false,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "RefreshToken_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AuditLog" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT,
+ "action" TEXT NOT NULL,
+ "resource" TEXT,
+ "ipAddress" TEXT,
+ "userAgent" TEXT,
+ "details" TEXT,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+
+ CONSTRAINT "AuditLog_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateTable
+CREATE TABLE "AuthIdentity" (
+ "id" TEXT NOT NULL,
+ "userId" TEXT NOT NULL,
+ "provider" TEXT NOT NULL,
+ "issuer" TEXT NOT NULL,
+ "subject" TEXT NOT NULL,
+ "emailAtLink" TEXT NOT NULL,
+ "lastLoginAt" TIMESTAMP(3),
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+
+ CONSTRAINT "AuthIdentity_pkey" PRIMARY KEY ("id")
+);
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
+
+-- CreateIndex
+CREATE INDEX "Collection_userId_updatedAt_idx" ON "Collection"("userId", "updatedAt");
+
+-- CreateIndex
+CREATE INDEX "Drawing_userId_updatedAt_idx" ON "Drawing"("userId", "updatedAt");
+
+-- CreateIndex
+CREATE INDEX "Drawing_userId_collectionId_updatedAt_idx" ON "Drawing"("userId", "collectionId", "updatedAt");
+
+-- CreateIndex
+CREATE INDEX "DrawingPermission_granteeUserId_idx" ON "DrawingPermission"("granteeUserId");
+
+-- CreateIndex
+CREATE INDEX "DrawingPermission_drawingId_idx" ON "DrawingPermission"("drawingId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DrawingPermission_drawingId_granteeUserId_key" ON "DrawingPermission"("drawingId", "granteeUserId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "DrawingLinkShare_tokenHash_key" ON "DrawingLinkShare"("tokenHash");
+
+-- CreateIndex
+CREATE INDEX "DrawingLinkShare_drawingId_idx" ON "DrawingLinkShare"("drawingId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "PasswordResetToken_token_key" ON "PasswordResetToken"("token");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "RefreshToken_token_key" ON "RefreshToken"("token");
+
+-- CreateIndex
+CREATE INDEX "AuthIdentity_userId_idx" ON "AuthIdentity"("userId");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AuthIdentity_issuer_subject_key" ON "AuthIdentity"("issuer", "subject");
+
+-- CreateIndex
+CREATE UNIQUE INDEX "AuthIdentity_provider_userId_key" ON "AuthIdentity"("provider", "userId");
+
+-- AddForeignKey
+ALTER TABLE "Collection" ADD CONSTRAINT "Collection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Drawing" ADD CONSTRAINT "Drawing_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "Drawing" ADD CONSTRAINT "Drawing_collectionId_fkey" FOREIGN KEY ("collectionId") REFERENCES "Collection"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DrawingPermission" ADD CONSTRAINT "DrawingPermission_drawingId_fkey" FOREIGN KEY ("drawingId") REFERENCES "Drawing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DrawingPermission" ADD CONSTRAINT "DrawingPermission_granteeUserId_fkey" FOREIGN KEY ("granteeUserId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "DrawingLinkShare" ADD CONSTRAINT "DrawingLinkShare_drawingId_fkey" FOREIGN KEY ("drawingId") REFERENCES "Drawing"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "PasswordResetToken" ADD CONSTRAINT "PasswordResetToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "RefreshToken" ADD CONSTRAINT "RefreshToken_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "AuditLog" ADD CONSTRAINT "AuditLog_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+
+-- AddForeignKey
+ALTER TABLE "AuthIdentity" ADD CONSTRAINT "AuthIdentity_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/backend/prisma/migrations/postgresql/migration_lock.toml b/backend/prisma/migrations/postgresql/migration_lock.toml
new file mode 100644
index 00000000..fbffa92c
--- /dev/null
+++ b/backend/prisma/migrations/postgresql/migration_lock.toml
@@ -0,0 +1,3 @@
+# Please do not edit this file manually
+# It should be added in your version-control system (i.e. Git)
+provider = "postgresql"
\ No newline at end of file
diff --git a/backend/prisma/migrations/20251122021659_init/migration.sql b/backend/prisma/migrations/sqlite/20251122021659_init/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20251122021659_init/migration.sql
rename to backend/prisma/migrations/sqlite/20251122021659_init/migration.sql
diff --git a/backend/prisma/migrations/20251122032308_add_preview/migration.sql b/backend/prisma/migrations/sqlite/20251122032308_add_preview/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20251122032308_add_preview/migration.sql
rename to backend/prisma/migrations/sqlite/20251122032308_add_preview/migration.sql
diff --git a/backend/prisma/migrations/20251122065455_add_files_column/migration.sql b/backend/prisma/migrations/sqlite/20251122065455_add_files_column/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20251122065455_add_files_column/migration.sql
rename to backend/prisma/migrations/sqlite/20251122065455_add_files_column/migration.sql
diff --git a/backend/prisma/migrations/20251124220546_add_library_model/migration.sql b/backend/prisma/migrations/sqlite/20251124220546_add_library_model/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20251124220546_add_library_model/migration.sql
rename to backend/prisma/migrations/sqlite/20251124220546_add_library_model/migration.sql
diff --git a/backend/prisma/migrations/20260124145151_add_user_auth/migration.sql b/backend/prisma/migrations/sqlite/20260124145151_add_user_auth/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20260124145151_add_user_auth/migration.sql
rename to backend/prisma/migrations/sqlite/20260124145151_add_user_auth/migration.sql
diff --git a/backend/prisma/migrations/20260124152839_add_password_reset_audit_refresh_tokens/migration.sql b/backend/prisma/migrations/sqlite/20260124152839_add_password_reset_audit_refresh_tokens/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20260124152839_add_password_reset_audit_refresh_tokens/migration.sql
rename to backend/prisma/migrations/sqlite/20260124152839_add_password_reset_audit_refresh_tokens/migration.sql
diff --git a/backend/prisma/migrations/20260206173000_add_auth_enabled_system_config/migration.sql b/backend/prisma/migrations/sqlite/20260206173000_add_auth_enabled_system_config/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20260206173000_add_auth_enabled_system_config/migration.sql
rename to backend/prisma/migrations/sqlite/20260206173000_add_auth_enabled_system_config/migration.sql
diff --git a/backend/prisma/migrations/20260206195000_add_auth_login_rate_limit_config/migration.sql b/backend/prisma/migrations/sqlite/20260206195000_add_auth_login_rate_limit_config/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20260206195000_add_auth_login_rate_limit_config/migration.sql
rename to backend/prisma/migrations/sqlite/20260206195000_add_auth_login_rate_limit_config/migration.sql
diff --git a/backend/prisma/migrations/20260207000000_add_query_indexes/migration.sql b/backend/prisma/migrations/sqlite/20260207000000_add_query_indexes/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20260207000000_add_query_indexes/migration.sql
rename to backend/prisma/migrations/sqlite/20260207000000_add_query_indexes/migration.sql
diff --git a/backend/prisma/migrations/20260210153000_add_auth_onboarding_completed/migration.sql b/backend/prisma/migrations/sqlite/20260210153000_add_auth_onboarding_completed/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20260210153000_add_auth_onboarding_completed/migration.sql
rename to backend/prisma/migrations/sqlite/20260210153000_add_auth_onboarding_completed/migration.sql
diff --git a/backend/prisma/migrations/20260210190000_add_auth_identity/migration.sql b/backend/prisma/migrations/sqlite/20260210190000_add_auth_identity/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20260210190000_add_auth_identity/migration.sql
rename to backend/prisma/migrations/sqlite/20260210190000_add_auth_identity/migration.sql
diff --git a/backend/prisma/migrations/20260211232000_add_bootstrap_setup_code/migration.sql b/backend/prisma/migrations/sqlite/20260211232000_add_bootstrap_setup_code/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20260211232000_add_bootstrap_setup_code/migration.sql
rename to backend/prisma/migrations/sqlite/20260211232000_add_bootstrap_setup_code/migration.sql
diff --git a/backend/prisma/migrations/20260217214759_drawing_sharing/migration.sql b/backend/prisma/migrations/sqlite/20260217214759_drawing_sharing/migration.sql
similarity index 100%
rename from backend/prisma/migrations/20260217214759_drawing_sharing/migration.sql
rename to backend/prisma/migrations/sqlite/20260217214759_drawing_sharing/migration.sql
diff --git a/backend/prisma/migrations/migration_lock.toml b/backend/prisma/migrations/sqlite/migration_lock.toml
similarity index 100%
rename from backend/prisma/migrations/migration_lock.toml
rename to backend/prisma/migrations/sqlite/migration_lock.toml
diff --git a/docker-compose.pg-test.yml b/docker-compose.pg-test.yml
new file mode 100644
index 00000000..3cb9e9c1
--- /dev/null
+++ b/docker-compose.pg-test.yml
@@ -0,0 +1,43 @@
+services:
+ # PostgreSQL database for testing
+ postgres:
+ image: postgres:16-alpine
+ environment:
+ - POSTGRES_USER=excalidash
+ - POSTGRES_PASSWORD=test123
+ - POSTGRES_DB=excalidash
+ ports:
+ - "5433:5432"
+ volumes:
+ - pgdata:/var/lib/postgresql/data
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U excalidash"]
+ interval: 5s
+ timeout: 5s
+ retries: 5
+ restart: unless-stopped
+
+ # Backend with PostgreSQL
+ backend:
+ build:
+ context: ./backend
+ dockerfile: Dockerfile
+ environment:
+ - DATABASE_PROVIDER=postgresql
+ - DATABASE_URL=postgresql://excalidash:test123@postgres:5432/excalidash
+ - FRONTEND_URL=http://localhost:6767
+ - JWT_SECRET=test-secret-pg-min-32-chars-long
+ - CSRF_SECRET=test-secret-pg
+ - AUTH_MODE=local
+ ports:
+ - "8000:8000"
+ depends_on:
+ postgres:
+ condition: service_healthy
+ volumes:
+ - uploads:/app/uploads
+ restart: unless-stopped
+
+volumes:
+ pgdata:
+ uploads:
diff --git a/scripts/generate-migrations.sh b/scripts/generate-migrations.sh
new file mode 100755
index 00000000..d871624a
--- /dev/null
+++ b/scripts/generate-migrations.sh
@@ -0,0 +1,147 @@
+#!/bin/bash
+set -e
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PROJECT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
+PRISMA_DIR="${PROJECT_DIR}/backend/prisma"
+SCHEMA_FILE="${PRISMA_DIR}/schema.prisma"
+MIGRATIONS_DIR="${PRISMA_DIR}/migrations"
+
+echo "=== ExcaliDash Migration Generator ==="
+echo ""
+echo "Select database provider:"
+echo "1) SQLite"
+echo "2) PostgreSQL"
+echo ""
+read -p "Enter choice (1 or 2): " choice
+
+case "$choice" in
+ 1)
+ PROVIDER="sqlite"
+ ;;
+ 2)
+ PROVIDER="postgresql"
+ ;;
+ *)
+ echo "Invalid choice. Exiting."
+ exit 1
+ ;;
+esac
+
+echo ""
+echo "Provider selected: ${PROVIDER}"
+
+OTHER_PROVIDER=$([ "$PROVIDER" = "sqlite" ] && echo "postgresql" || echo "sqlite")
+
+# Check if provider folder exists
+if [ ! -d "${MIGRATIONS_DIR}/${PROVIDER}" ]; then
+ echo "ERROR: Migrations folder for '${PROVIDER}' does not exist at ${MIGRATIONS_DIR}/${PROVIDER}"
+ exit 1
+fi
+
+echo ""
+echo "Step 1: Backing up schema.prisma..."
+
+# Backup current schema.prisma
+cp "${SCHEMA_FILE}" "${SCHEMA_FILE}.backup"
+
+echo ""
+echo "Step 2: Updating schema.prisma to use ${PROVIDER}..."
+
+# Update schema.prisma to use the selected provider
+sed -i 's/provider = env("DATABASE_PROVIDER")/provider = "'"${PROVIDER}"'"/' "${SCHEMA_FILE}"
+
+echo ""
+echo "Step 3: Setting up migrations..."
+
+# Clear migrations folder
+rm -rf "${MIGRATIONS_DIR}"/*
+
+# Recreate folders
+mkdir -p "${MIGRATIONS_DIR}/sqlite"
+mkdir -p "${MIGRATIONS_DIR}/postgresql"
+
+# Copy migrations for selected provider (if any exist)
+if [ "$PROVIDER" = "sqlite" ]; then
+ # SQLite has existing migrations - copy them
+ cp -R "${MIGRATIONS_DIR}/sqlite/." "${MIGRATIONS_DIR}/"
+ echo " Copied $(ls -1 ${MIGRATIONS_DIR} | wc -l) SQLite migrations"
+else
+ # PostgreSQL - create from empty database
+ echo " Will create initial PostgreSQL migration from schema"
+fi
+
+echo ""
+echo "Step 4: Running Prisma migrate..."
+echo ""
+
+# Check for DATABASE_URL
+if [ -z "${DATABASE_URL}" ]; then
+ echo "DATABASE_URL is not set. Please enter your database URL:"
+ echo "Examples:"
+ echo " SQLite: file:./dev.db"
+ echo " PostgreSQL: postgresql://user:password@localhost:5432/excalidash"
+ read -p "DATABASE_URL: " DATABASE_URL
+ export DATABASE_URL
+fi
+
+cd "${PROJECT_DIR}/backend"
+
+# Run prisma migrate
+if [ "$1" = "--dev" ]; then
+ MIGRATION_NAME="${2:-new_migration}"
+ echo " Running: npx prisma migrate dev --name ${MIGRATION_NAME}"
+ npx prisma migrate dev --name "${MIGRATION_NAME}"
+else
+ echo " Running: npx prisma migrate deploy"
+ npx prisma migrate deploy
+fi
+
+echo ""
+echo "Step 5: Restoring schema.prisma..."
+
+# Restore original schema.prisma
+mv "${SCHEMA_FILE}.backup" "${SCHEMA_FILE}"
+
+echo ""
+echo "Step 6: Organizing migrations..."
+
+# Clear migrations folder
+rm -rf "${MIGRATIONS_DIR}"/*
+
+# Recreate folders
+mkdir -p "${MIGRATIONS_DIR}/sqlite"
+mkdir -p "${MIGRATIONS_DIR}/postgresql"
+
+# Restore other provider (empty for postgresql, or from git for sqlite)
+if [ "$PROVIDER" = "postgresql" ]; then
+ # PostgreSQL folder - just the lock file
+ echo 'provider = "postgresql"' > "${MIGRATIONS_DIR}/postgresql/migration_lock.toml"
+ # Restore SQLite from git
+ git checkout HEAD -- prisma/migrations/ 2>/dev/null || true
+ mkdir -p sqlite postgresql 2>/dev/null
+ mv 202* sqlite/ 2>/dev/null || true
+ mv migration_lock.toml sqlite/ 2>/dev/null || true
+ echo 'provider = "postgresql"' > postgresql/migration_lock.toml 2>/dev/null || true
+else
+ # SQLite - restore PostgreSQL (empty)
+ echo 'provider = "postgresql"' > "${MIGRATIONS_DIR}/postgresql/migration_lock.toml"
+fi
+
+# Move newly generated migrations to the provider folder
+if [ "$(ls -d ${MIGRATIONS_DIR}/2* 2>/dev/null)" ]; then
+ for dir in "${MIGRATIONS_DIR}"/2*/; do
+ if [ -d "$dir" ]; then
+ cp -R "$dir" "${MIGRATIONS_DIR}/${PROVIDER}/"
+ rm -rf "$dir"
+ fi
+ done
+fi
+
+echo ""
+echo "=== Done! ==="
+echo ""
+echo "Generated migrations are in: ${MIGRATIONS_DIR}/${PROVIDER}/"
+ls -la "${MIGRATIONS_DIR}/${PROVIDER}/"
+echo ""
+echo "Your schema.prisma has been restored to use env(DATABASE_PROVIDER)"
From ad41a7b883fdda799cc02468f16f5ce7543142ff Mon Sep 17 00:00:00 2001
From: BaptGosse
Date: Mon, 9 Mar 2026 01:07:35 +0100
Subject: [PATCH 6/6] fix: health check, because kubernetes do it in http
---
backend/src/__tests__/health-check.test.ts | 58 ++++++++++++++++++++++
backend/src/index.ts | 5 ++
2 files changed, 63 insertions(+)
create mode 100644 backend/src/__tests__/health-check.test.ts
diff --git a/backend/src/__tests__/health-check.test.ts b/backend/src/__tests__/health-check.test.ts
new file mode 100644
index 00000000..8d943908
--- /dev/null
+++ b/backend/src/__tests__/health-check.test.ts
@@ -0,0 +1,58 @@
+/**
+ * Health check endpoint test
+ * Ensures /health returns 200 OK without redirects (for Kubernetes probes)
+ */
+
+import { describe, it, expect, beforeEach } from "vitest";
+import express from "express";
+import request from "supertest";
+
+describe("Health check endpoint", () => {
+ it("should return 200 OK with status 'ok'", async () => {
+ const app = express();
+
+ app.get("/health", (req, res) => {
+ res.status(200).json({ status: "ok" });
+ });
+
+ const response = await request(app).get("/health");
+
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual({ status: "ok" });
+ });
+
+ it("should not redirect even when behind HTTPS middleware", async () => {
+ const app = express();
+
+ // Simulate HTTPS redirect middleware with /health exception
+ app.use((req, res, next) => {
+ // Skip HTTPS redirect for health check endpoint
+ if (req.path === "/health") {
+ return next();
+ }
+
+ if (req.header("x-forwarded-proto") !== "https") {
+ return res.redirect(`https://example.com${req.path}`);
+ }
+ next();
+ });
+
+ app.get("/health", (req, res) => {
+ res.status(200).json({ status: "ok" });
+ });
+
+ app.get("/other", (req, res) => {
+ res.status(200).json({ status: "ok" });
+ });
+
+ // Health check should NOT redirect (even without x-forwarded-proto)
+ const healthResponse = await request(app).get("/health");
+ expect(healthResponse.status).toBe(200);
+ expect(healthResponse.body).toEqual({ status: "ok" });
+
+ // Other routes should still redirect
+ const otherResponse = await request(app).get("/other");
+ expect(otherResponse.status).toBe(302);
+ expect(otherResponse.headers.location).toContain("https://");
+ });
+});
diff --git a/backend/src/index.ts b/backend/src/index.ts
index f0c1e29a..b34f7fbb 100644
--- a/backend/src/index.ts
+++ b/backend/src/index.ts
@@ -243,6 +243,11 @@ if (shouldEnforceHttps) {
);
app.use((req, res, next) => {
+ // Skip HTTPS redirect for health check endpoint (for Kubernetes probes)
+ if (req.path === "/health") {
+ return next();
+ }
+
if (req.header("x-forwarded-proto") !== "https") {
// Avoid Host-header based open redirects; prefer a configured canonical origin/host.
const rawHost = String(req.header("host") || "").trim().toLowerCase();