12 KiB
VM Deployment (Flatlogic Executor)
This is how Flatlogic production/preview works: no Docker — PM2 + local PostgreSQL + Cloudflare tunnel. The end of this document contains a reference VM file structure (executor, pm2, project).
Docker deployment (compose / single-image / staging) is covered in a separate document:
deployment-docker.md.
The project consists of two applications:
frontend/— Vite + React + TypeScript (SPA). Build output →frontend/dist/.backend/— Express + Sequelize on TypeScript/ESM. Build output →backend/dist/.
The project lives in ~/executor/workspace, processes are managed by PM2, DB is local
PostgreSQL, external access via Cloudflare tunnel (cloudflared). Docker is not used here.
1.1. Topology
Browser
│ https://<subdomain>.dev.flatlogic.app
▼
cloudflared (tunnel)
│
▼
nginx :8080
├── / → frontend :3001 (Vite)
├── /api → backend :3000 (Express)
└── /api-docs → backend :3000
│
▼
PostgreSQL :5432 (local)
- In
NODE_ENV=dev_stagethe backend listens on 3000 (config.serverPort), frontend on 3001. - The browser opens the tunnel domain; the frontend calls the API via relative path
/api(seefrontend/src/shared/constants/api.ts), nginx proxies it to the backend — same origin, so CORS/CSRF are not a problem.
1.2. Environment Variables
Injected by the platform into the pm2 environment of the backend process (values are secrets, not committed to the repository):
| Variable | Purpose |
|---|---|
NODE_ENV=dev_stage |
mode (production-like, but without strict production checks) |
SECRET_KEY |
JWT signature (required — otherwise backend won't start) |
DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASS |
connection to local Postgres (DB_PASS = project UUID) |
GOOGLE_CLIENT_ID/SECRET, MS_CLIENT_ID/SECRET |
OAuth (optional) |
SMTP_*, EMAIL_*, MAIL_* |
email (optional) |
CF_TUNNEL_* |
Cloudflare tunnel (for cloudflared, not for the application) |
From committed backend/.env (not production-level secrets):
PORT(optional, defaults to 8080).- Seed passwords and DB credentials for development are hardcoded in
shared/constants/app.tsandseeders/*;.envis only needed for overrides. backend/src/shared/config/load-env.tsfindsbackend/.envthe same way for bothtsxand compileddist.
ALLOWED_ORIGINSis NOT set by the platform. Indev_stagethis is acceptable: the backend doesn't crash and reflects the request origin (config.auth.allowAllOrigins). StrictALLOWED_ORIGINScheck only applies in strictNODE_ENV=production.
1.3. PM2 Processes
pm2 status on the VM shows (names are fixed during provisioning):
| Process | Command (cwd) | Port |
|---|---|---|
frontend-dev |
npm run dev -- --hostname 0.0.0.0 --port 3001 (workspace/frontend) |
3001 |
backend-dev |
NODE_ENV=dev_stage npm run start (workspace/backend) |
3000 |
fl-executor |
executor.js — Flatlogic agent (cable, git, AI commands) |
— |
fl-telemetry |
telemetry-daemon.js |
— |
⚠️
--hostnameis a Next.js flag; Vite CLI rejects it (Unknown option). Thereforedev/startfor the frontend run through the wrapperfrontend/scripts/serve.mjs, which translates--hostname→--hostand starts Vite. Without it, the frontend won't start on the VM.
1.4. Deployment After git pull
PM2 commands do not install dependencies. After pulling new code:
cd ~/executor/workspace/backend && npm ci
cd ~/executor/workspace/frontend && npm ci
The DB schema is created by the initial migration. On an existing DB, npm run start
runs db:migrate (idempotent, CREATE TABLE IF NOT EXISTS) + db:seed automatically.
For a guaranteed clean state (recommended after major migrations —
will delete data):
cd ~/executor/workspace/backend && npm run db:reset # drop all tables → migrate → seed
⚠️ On VM:
npm run db:resetuses local dev credentials by default, which won't work. You must install dependencies and pass the platform-injected DB credentials explicitly:# 1. Install dependencies first cd ~/executor/workspace/backend && npm ci cd ~/executor/workspace/frontend && npm ci # 2. Get DB credentials pm2 env 1 | grep DB_PASS # DB_PASS from PM2 environment cat ~/executor/.env | grep DB_ # DB_NAME, DB_USER, DB_HOST, DB_PORT # 3. Run reset with correct credentials cd ~/executor/workspace/backend && \ DB_HOST=127.0.0.1 DB_PORT=5432 \ DB_NAME=app_<id> DB_USER=app_<id> DB_PASS=<uuid> \ npm run db:reset # 4. Restart PM2 (has platform credentials) pm2 restart backend-dev frontend-dev --update-env
Restart processes (or the executor does this after pull):
pm2 restart backend-dev frontend-dev --update-env
Scheduled maintenance: refresh-token cleanup
Expired refresh-token rows accumulate (the table is not paranoid). Schedule the
cleanup command (e.g. a daily cron) to delete rows past the retention window
(AUTH_REFRESH_TOKEN_RETENTION_MS, default 7 days):
# crontab -e — daily at 03:30
30 3 * * * cd ~/executor/workspace/backend && npm run db:cleanup-tokens >> ~/token-cleanup.log 2>&1
It is idempotent and never touches valid sessions. See
backend/docs/cookie-auth.md (Operational maintenance).
What the Startup Commands Do
backend-dev→npm run start=db:migrate(initial migration → schema) +db:seed+watch(server viatsx+ nodemon, port 3000).frontend-dev→npm run dev(viaserve.mjs) — Vite dev server on 3001,allowedHosts: trueallows the tunnel domain.
1.5. nginx
If nginx is a system service, its config should match nginx.conf from the
repository (/ → 3001, /api and /api-docs → 3000):
sudo cp ~/executor/workspace/nginx.conf /etc/nginx/nginx.conf
sudo nginx -t && sudo nginx -s reload
1.6. Verification
curl -s -o /dev/null -w "front %{http_code}\n" http://127.0.0.1:3001/
curl -s -o /dev/null -w "api %{http_code}\n" http://127.0.0.1:3000/api-docs/
pm2 status
pm2 logs backend-dev --lines 50
1.7. Production Mode on VM (Compiled Builds)
"Production mode" = compiled builds while keeping NODE_ENV=dev_stage
(frontend minification, node dist for backend, source maps). Strict NODE_ENV=production
cannot be used — it requires ALLOWED_ORIGINS, which the platform doesn't set,
and the backend will crash on startup.
To switch to production, change the pm2 commands (on the executor side):
# Frontend
cd ~/executor/workspace/frontend && npm ci && npm run build
pm2 delete frontend-dev 2>/dev/null || true
FRONT_PORT=3001 pm2 start npm --name frontend --update-env -- run start
# Backend (keep NODE_ENV=dev_stage)
cd ~/executor/workspace/backend && npm ci && npm run build
pm2 delete backend-dev 2>/dev/null || true
NODE_ENV=dev_stage pm2 start npm --name backend --update-env -- run start:production
pm2 save
frontend npm run start=vite previewonFRONT_PORT(3001), servesdist/.backend npm run start:production=db:migrate:prod+db:seed:prod+node --enable-source-maps dist/index.js(port 3000).
To have the executor automatically rebuild production on each code update — add
npm ci && npm run build to its service restart step (see vcs.js, ~line 412,
array const services = ['backend-dev', 'frontend-dev']).
1.8. Troubleshooting
| Symptom | Cause / Solution |
|---|---|
Frontend won't start, Unknown option --hostname |
old code without serve.mjs; update workspace (git pull + npm ci) |
Backend crashes: ALLOWED_ORIGINS must be configured |
running with NODE_ENV=production; on VM it should be dev_stage |
Backend crashes: SECRET_KEY required |
platform didn't pass SECRET_KEY to pm2-env (required in production/dev_stage) |
Backend crashes: Missing required database credentials |
DB_* not set in production/dev_stage (dev defaults don't apply) |
tsx: not found / vite: not found |
npm ci not run after pull |
| 502 on domain | backend/frontend not listening on 3000/3001, or nginx routing mismatch |
Part 2. VM File Structure (Reference)
Snapshot of a production VM (pool-saas-*). Useful for understanding where things are located.
2.1. Home Directory ~
~/
├── executor/ # Flatlogic agent + the project itself (see below)
├── .pm2/ # PM2: processes, logs, pids
│ ├── logs/ # *-out.log, *-error.log per process
│ ├── pids/
│ └── dump.pm2 # saved process list (pm2 save)
├── .bun/ .yarn/ .npm/ .cache/ # toolchains/caches
├── .codex/ .gemini/ .config/ # AI CLI configs
├── .ssh/ .pki/
├── recipes.md
└── google-gemini-cli-0.17.1.tgz
2.2. ~/executor/ — Flatlogic Agent
~/executor/
├── .env # agent and project config (PROJECT_UUID, PROJECT_ID,
│ # CABLE_URL, DB_NAME=app_<id>, SUBDOMAIN, FRONT_PORT, ...)
├── executor.js # main agent process (pm2: fl-executor):
│ # WebSocket cable to flatlogic.com, receiving commands,
│ # launching AI runners (gemini/codex), fs/git operations
├── gemini.js / gemini-proc.js / opencode.js / opencode-proc.js # AI runners
├── vcs.js and vcs/vcs.js # git: init/pull/push/commit, Gitea mirror,
│ # service restart (array ['backend-dev','frontend-dev'])
├── vm-tools.js # VM tools (commands from platform)
├── activity-tracker.js # runner activity tracking
├── telemetry-daemon.js / telemetry-server.js / telemetry-file-watcher.js # telemetry (pm2: fl-telemetry)
├── sentry.js # Sentry
├── config.js # WORKSPACE_ROOT etc.
├── index.php # static preloader page ("Analyzing your requirements...")
├── setup_postgres_project.sh # create role/DB for project in local Postgres
├── setup_mariadb_project.sh # same for MariaDB
├── setup_workspace_permissions.sh
├── cleanup_vm.sh
├── AGENTS.md / README.md # instructions for AI agent (describe project template)
├── otel-local.yaml / schema.json / proto/
├── node_modules/ package.json package-lock.json
├── workspace/ # ◀── THE PROJECT ITSELF (git repository = this repository)
├── workspace_baseline.tar.gz # baseline workspace snapshot
├── workspace_codegen/ # code generation workspace
└── templates/ # templates for new projects
├── app-templates/
└── frontend-tailwind-backend-nodejs/ # Next.js+Node template (NOT our stack)
Files in
executor/(agent) andtemplates/are not related to running our project — they don't need to be modified.index.phpis just a preloader during generation.
2.3. ~/executor/workspace/ — The Project
This is the git repository (40227-vm):
workspace/
├── frontend/ # Vite + React + TS → builds to frontend/dist, served on :3001
├── backend/ # Express + Sequelize TS/ESM → backend/dist, served on :3000
│ ├── .env # PORT (committed)
│ └── src/db/migrations/ # initial migration (schema)
├── nginx.conf # routing / → 3001, /api → 3000 (for system nginx)
├── Dockerfile / Dockerfile.dev / docker/ # alternative Docker path
├── 502.html
└── docs/ # including this file
2.4. ~/executor/.env — Keys (Values are Secret/Individual)
PROJECT_UUID, PROJECT_ID, CABLE_URL, GEMINI_MODEL, TELEMETRY_*, OTEL_*,
MAIL_*, SMTP_*, SUBDOMAIN, BASE_DOMAIN, FULL_DOMAIN, HOST_FQDN,
DB_NAME=app_<id>, DB_USER=app_<id>, DB_HOST=127.0.0.1, DB_PORT=5432,
FRONT_PORT=3001, SENTRY_DSN
Application secrets (SECRET_KEY, DB_PASS, OAuth/SMTP, git tokens, CF_TUNNEL_*)
are placed by the platform directly into the pm2 environment of backend-dev/fl-executor
processes, not in backend/.env.