VibeFlow is a containerized React/TypeScript web app (SPA) meant to run in standalone (self-hosted) mode with:
- “plain” Postgres as the database
- a small Node.js API for initial setup, authentication, and core CRUD endpoints
- Caddy as the reverse proxy (single public entrypoint)
- Nginx inside the
webcontainer only to serve the SPA static files
- Docker + Docker Compose
- Node.js 20+ + npm (for build, lint, and
.envgeneration)
Node.js is used to run npm scripts (e.g. .env generation) and for frontend development.
sudo apt update
sudo apt install -y ca-certificates curl
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
node -v
npm -vbrew install node@20
node -v
npm -v- Generate
.envfrom.env.example:
npm run env:generateIf .env already exists, the command will not overwrite it.
- Start the stack:
docker compose up -d --buildOr:
npm run stack:up- Open the app:
Check containers:
docker compose ps
docker compose logs --tail=200 db api caddy webCheck setup (no auth):
curl -i http://localhost:3000/api/setup/statusBootstrap admin (only once, then it returns 409):
curl -i -X POST http://localhost:3000/api/setup/init \
-H 'content-type: application/json' \
-d '{"email":"admin@example.com","password":"change-me-123"}'Login and session cookie:
curl -i -c cookies.txt -X POST http://localhost:3000/api/auth/login \
-H 'content-type: application/json' \
-d '{"email":"admin@example.com","password":"change-me-123"}'Protected call (uses the saved cookie):
curl -i -b cookies.txt http://localhost:3000/api/projectsOn first run, if there are no users, the app automatically redirects to /setup to create the first (admin) user:
After creation, the UI takes you to /login:
After login, the landing page is /projects:
docker compose up -d --build:
- Starts
db(Postgres) and, if the volume is empty, runs the schema init.sql - Waits for
dbto be “healthy”, then startsapi - Starts
web(frontend build + Nginx), thencaddyas the public entrypoint
Everything goes through http://localhost:3000:
GET/POST/DELETE /api/*→api:3001/*→web:80(Nginx + SPA)
Config: Caddyfile
- The SPA calls
GET /api/setup/status - If
setupComplete=false, it shows/setup POST /api/setup/initcreates the first admin user inpublic.users
- Login:
POST /api/auth/login - Logout:
POST /api/auth/logout - Session:
vf_sessioncookie (HttpOnly) +public.sessionstable - Current user:
GET /api/auth/me
On the frontend, API calls use credentials: 'include' to automatically send the cookie (see api.ts).
Defined in docker-compose.yml.
Services:
db: Postgres 16 + initial schema (mount./db/init.sql)api: Node.js (setup + auth + API)web: SPA build + static serving via Nginxcaddy: public reverse proxy (host port3000)
Persistence:
- DB:
./volumes/db/data
Runtime file: .env (not committed).
Recommended generation: npm run env:generate (script: generate-env.mjs).
Keys:
POSTGRES_PASSWORD: password for DB uservibeflowSESSION_SECRET: server-side secret used to derive session token hashesSITE_URL: public URL (local:http://localhost:3000, production:https://example.com)
Notes:
- Never commit
.env - If you change
POSTGRES_PASSWORDafter Postgres has already been started with a persistent volume, you need to migrate or reset the volume
All endpoints are under /api.
Setup:
GET /api/setup/status→{ setupComplete: boolean }POST /api/setup/init→ creates the first admin user (only if no users exist yet)
Auth:
POST /api/auth/signup→ creates a user (requires setup to be completed)POST /api/auth/login→ sets the session cookiePOST /api/auth/logout→ invalidates the sessionGET /api/auth/me→{ user: null | { id, email, is_admin, created_at, updated_at } }POST /api/auth/change-password→ change password (requires session)
Projects:
GET /api/projects→{ projects: ProjectRow[] }(only for the logged-in user)POST /api/projects→{ project: ProjectRow }DELETE /api/projects/:id→{ ok: true }
Defined in db/init.sql.
Main tables:
public.users(email, password_hash, is_admin)public.sessions(token_hash, expires_at, user_id)public.projects(user_id, project_name, project_scope)public.flows(project_id, graph jsonb)
Frontend:
npm install
npm run devNotes:
VITE_SETUP_API_BASE_URL(default/api) defines the base URL for backend calls- in dev,
/api/*is proxied toVITE_DEV_API_TARGET(defaulthttp://localhost:3000) (seevite.config.ts)
Goal: run Vite in dev (port 5173) and proxy /api/* to the local API (port 3001).
- Start Postgres
Recommended option (DB in Docker, for development only):
docker run --name vibeflow-db \
-e POSTGRES_USER=vibeflow \
-e POSTGRES_DB=vibeflow \
-e POSTGRES_PASSWORD=changeme \
-p 5432:5432 \
-v "$PWD/volumes/db/data:/var/lib/postgresql/data" \
-v "$PWD/db/init.sql:/docker-entrypoint-initdb.d/00-init.sql:ro" \
-d postgres:16-alpine- Start the local API (port 3001)
export NODE_ENV=development
export PORT=3001
export SESSION_SECRET='dev-secret-change-me'
export SITE_URL='http://127.0.0.1:5173'
export DATABASE_URL='postgres://vibeflow:changeme@127.0.0.1:5432/vibeflow'
node setup-server/index.js- Start Vite and set the proxy to the local API
In another terminal:
export VITE_DEV_API_TARGET='http://127.0.0.1:3001'
npm run dev- Quick verification
curl -i http://127.0.0.1:3001/api/setup/status
curl -i http://127.0.0.1:5173/api/setup/statusNote: in development, CORS accepts only localhost/127.0.0.1. In production, only SITE_URL is accepted.
- Clone the repo and generate
.env:
git clone https://github.com/marco-ncode/vibeflow_os.git
cd vibeflow_os
npm ci
npm run env:generate- Set the public domain in
.env:
sed -i 's|^SITE_URL=.*|SITE_URL=https://example.com|' .env- Configure Caddy for domain + automatic HTTPS (replace
:80with your domain):
example.com {
handle /api/* {
reverse_proxy api:3001
}
handle {
reverse_proxy web:80
}
}- Expose 80/443 and persist certificates (in
docker-compose.yml,caddyservice):
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config- Start:
docker compose up -d --build
docker compose psSecurity note:
- Complete
/setupimmediately after the first bootstrap: until an admin exists, the bootstrap endpoint is intentionally public.
npm run lint
npm run build- Reset DB (data loss):
docker compose down+ delete./volumes/db/data+docker compose up -d --build - Logs:
docker compose logs -f --tail=200 db api caddy web - Setup not completed: verify that
db/init.sqlran (first start with an empty volume) and thatapisees DB as healthy