40227-vm/docs/deployment-vm.md
2026-06-12 06:55:35 +02:00

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_stage the backend listens on 3000 (config.serverPort), frontend on 3001.
  • The browser opens the tunnel domain; the frontend calls the API via relative path /api (see frontend/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.ts and seeders/*; .env is only needed for overrides.
  • backend/src/shared/config/load-env.ts finds backend/.env the same way for both tsx and compiled dist.

ALLOWED_ORIGINS is NOT set by the platform. In dev_stage this is acceptable: the backend doesn't crash and reflects the request origin (config.auth.allowAllOrigins). Strict ALLOWED_ORIGINS check only applies in strict NODE_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

⚠️ --hostname is a Next.js flag; Vite CLI rejects it (Unknown option). Therefore dev/start for the frontend run through the wrapper frontend/scripts/serve.mjs, which translates --hostname--host and 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

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-devnpm run start = db:migrate (initial migration → schema) + db:seed + watch (server via tsx + nodemon, port 3000).
  • frontend-devnpm run dev (via serve.mjs) — Vite dev server on 3001, allowedHosts: true allows 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 preview on FRONT_PORT (3001), serves dist/.
  • 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) and templates/ are not related to running our project — they don't need to be modified. index.php is 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.