Troubleshooting
Database Connection Pooling
Open Saasframe relies on PostgreSQL connection pooling to avoid "too many clients" errors. Tune the pool via environment variables to match your deployment footprint.
Pool Configuration
DB_POOL_MIN— Minimum connections in the pool (default:2)DB_POOL_MAX— Maximum connections in the pool (default:20)DB_POOL_IDLE_TIMEOUT— Idle timeout in milliseconds before a connection is released (default:30000)DB_POOL_ACQUIRE_TIMEOUT— Acquire timeout in milliseconds when waiting for a connection (default:60000)
Recommended Settings
Production baseline:
# Adjust according to PostgreSQL max_connections
DB_POOL_MAX=20
DB_POOL_MIN=5
DB_POOL_IDLE_TIMEOUT=30000
DB_POOL_ACQUIRE_TIMEOUT=60000
Local development:
# Smaller pool for local workflows
DB_POOL_MAX=5
DB_POOL_MIN=1
DB_POOL_IDLE_TIMEOUT=10000
DB_POOL_ACQUIRE_TIMEOUT=30000
The native monorepo dev runtime can start multiple PostgreSQL-backed processes at once: the Next.js app server, queue workers, and scheduler:start once an enabled schedule exists. Size DB_POOL_MAX for the aggregate footprint, not for a single process. For example, DB_POOL_MAX=20 can translate to roughly 60 potential connections before you count psql, migrations, or other local tools.
DB_POOL_MAX=20 is now the conservative default for local and general development. If your deployment previously relied on the older implicit default of 50, set DB_POOL_MAX explicitly based on your real concurrency and PostgreSQL max_connections budget.
If you continue to hit connection limits, verify that background workers and migrations use the same pool settings, and confirm that your database max_connections value comfortably exceeds the aggregate pool size across all applications connecting to it.
Dev Memory Usage (🧠 Memory ... RSS)
The dev runtime prints a 🧠 Memory ... RSS (peak ...) line that measures the process-tree RSS rooted at the app runtime — so the Next.js dev server, the auto-spawned queue workers, and the scheduler are all included in the number. A high idle RSS often reflects per-queue runner overhead rather than Next.js itself.
The monorepo and standalone yarn dev wrappers enable lazy worker and lazy scheduler auto-spawn by default to keep local idle RSS down. Direct saasframe server dev and production saasframe server start keep the historical eager default unless you opt in:
SF_AUTO_SPAWN_WORKERS_LAZY=true SF_AUTO_SPAWN_SCHEDULER_LAZY=true saasframe server dev
To force the historical eager worker and scheduler modes from the dev wrapper, run:
SF_AUTO_SPAWN_WORKERS_LAZY=false SF_AUTO_SPAWN_SCHEDULER_LAZY=false yarn dev
In lazy mode the runtime starts lightweight supervisors. The worker supervisor watches each discovered queue for the first ready job and only then spawns a worker. yarn dev uses the shared lazy mode, so the first ready job starts one saasframe queue worker --all process instead of one process per ready queue; set SF_AUTO_SPAWN_WORKERS_LAZY_MODE=per-queue to use the older per-queue process model. The scheduler supervisor watches for enabled schedules and only then spawns saasframe scheduler start. Idle queues and empty scheduler installations do not contribute polling timers (local strategy) or BullMQ Worker/Redis resources (async strategy) to the process tree. See Queue Workers — Lazy Worker Auto-Spawn for the env variable reference.
For production deployments, prefer AUTO_SPAWN_WORKERS=false plus separately managed worker processes — lazy mode targets memory-sensitive dev and small unified deployments.
The monorepo package watcher also defaults to low-memory one-shot rebuilds. Two near-identical env vars tune different layers of that path, so it pays to keep them straight:
SF_WATCH_PACKAGES_MODE(dispatch-level) picks which watcher implementationyarn devruns. Default is the consolidated single-process watcher (scripts/watch-packages.mjs). SetSF_WATCH_PACKAGES_MODE=legacyto fall back to the Turbo per-package fan-out (yarn watch:packages:legacy) when debugging the new path or pinning the old behavior.SF_PACKAGE_WATCH_MODE(per-package mode, only meaningful under the legacy Turbo fan-out) chooses between the low-memory one-shot mode (default) and the olderpersistentesbuild-context mode that keeps the incremental graph in memory for faster rebuilds at the cost of ~1 GB of idle RSS.
To restore the previous faster-but-heavier incremental watcher for a session, combine both:
SF_WATCH_PACKAGES_MODE=legacy SF_PACKAGE_WATCH_MODE=persistent yarn dev
Choosing which packages the watcher tracks (watch scope)
In the monorepo, yarn dev watches every workspace package (packages/* plus any activated external/official-modules/packages/*). On large checkouts you rarely edit more than a handful of packages in one session, so the watcher can be narrowed to a subset. The watch unit is the workspace package (its short label — core, ui, shared, ai-assistant, …); the app itself is still watched in full by Next.js/Turbopack regardless of the scope.
Narrowing the watch scope is the main lever for reducing dev-mode memory usage: the watcher's footprint scales with the number of packages it tracks, so watching only the packages you actually edit meaningfully lowers idle RSS on memory-constrained machines.
Pick a mode with the SF_WATCH_SCOPE env var or the --watch=<mode> flag on yarn dev / yarn dev:greenfield (the CLI flag wins over the env var). The active mode is printed with an emoji at dev startup — e.g. watch scope: 🌐 all — all packages or watch scope: ⚡ auto-optimized — recently touched — so you can confirm at a glance which packages are live:
| Mode | What it watches |
|---|---|
all (default) | Every discovered package. Unchanged behavior. |
auto-optimized | Only packages touched recently — your git working-tree changes plus the current branch's diff against its base ref. The watcher re-checks every 2 minutes and expands to newly-touched packages (it never drops a watcher mid-session). |
popular | Only the most frequently changed packages, ranked from recent git log history (capped by SF_WATCH_POPULAR_LIMIT, default 6). Falls back to core, ui, shared when no history is available. |
env | Exactly the packages you list in SF_WATCH_PACKAGES, or the selection saved by the interactive picker. |
# Equivalent ways to select a mode
yarn dev --watch=auto-optimized
SF_WATCH_SCOPE=auto-optimized yarn dev
# Watch only what you name (env scope)
SF_WATCH_SCOPE=env SF_WATCH_PACKAGES=core,ui yarn dev
yarn dev --watch=env --watch-packages=core,ui
# Most-popular packages, capped at 4
yarn dev --watch=popular --watch-popular-limit=4
# Greenfield boot, auto-optimized scope
yarn dev:greenfield --watch=auto-optimized
Prefer to pick interactively? Run the picker, then start dev in env scope:
yarn dev:watch-select # choose packages by number/name; saved to .saasframe/watch-packages.local.json
yarn dev --watch=env
Tuning knobs for auto-optimized:
SF_WATCH_GIT_STATUS=off— ignore uncommitted working-tree changes (use only the branch diff).SF_WATCH_GIT_BRANCH=off— ignore the branch diff (use only the working tree).SF_WATCH_BASE_REF=origin/main— override the base ref the branch diff is computed against (defaults to the first oforigin/develop,develop,origin/main,mainthat exists).
If a non-all scope ever leaves out a package you are actively editing, either switch back with SF_WATCH_SCOPE=all / --watch=all, add it to SF_WATCH_PACKAGES, or just wait — auto-optimized will pick it up at the next 2-minute sweep once it has a git change.
Standalone create-app projects do not have a local workspace-package tree in normal use, so this scope selector is not part of their default dev workflow. Next.js/Turbopack still watches standalone app source files directly.
PostgreSQL auth errors on local setup
If local commands such as yarn generate, yarn db:generate, or app startup fail with errors like:
password authentication failed for user "postgres"role "postgres" does not existcould not connect to server
you may be connecting to a different PostgreSQL instance already bound to localhost:5432 (for example, a host-installed Postgres), not the Docker one started for Open Saasframe.
Fix
Start Docker PostgreSQL on a different host port and use the same port in DATABASE_URL:
POSTGRES_PORT=55432 docker compose up -d postgres
DATABASE_URL=postgresql://postgres:postgres@localhost:55432/saasframe
Verify
psql "postgresql://postgres:postgres@localhost:55432/saasframe" -c "select 1;"
If this succeeds but :5432 still fails, keep using the alternate port (or stop the other local PostgreSQL service that occupies 5432).