Configuration
Syncle is configured through environment variables, plus a small set of runtime settings edited in the web interface and stored in its metadata database. This page lists every variable with its shipped default, where each configuration file lives, and how the in-app settings layer over the env values.
Where configuration lives
Which file matters depends on how you run Syncle. There is no dotfile config — no .synclerc, no syncle.config.js; configuration is exclusively environment variables plus the in-app settings described at the end of this page.
| File | Applies to | How it gets there |
|---|---|---|
$SYNCLE_HOME/.env (default ~/.syncle/.env) | Docker install | Written by install.sh with mode 600; passed to Docker Compose as the env file. Holds SYNCLE_MASTER_KEY and the pinned SYNCLE_IMAGE. |
apps/api/.env | API, source checkout | Copied from apps/api/.env.example by scripts/setup-env.mjs. |
apps/web/.env.local | Web app, source checkout | Copied from apps/web/.env.example by the same script. |
scripts/setup-env.mjs runs before pnpm dev and pnpm start — not on pnpm install — and never overwrites a file that already exists, so your edits survive every run.
In the Docker install the container environment is fixed inside docker-compose.app.yml (PORT=4002, a DATABASE_URL pointing at the bundled Postgres, and so on); only SYNCLE_MASTER_KEY and SYNCLE_IMAGE flow in from $SYNCLE_HOME/.env. The syncle launcher itself reads SYNCLE_PORT and SYNCLE_HOME from your shell — the installation page covers those.
API environment variables
Read from apps/api/.env (or the container environment) at boot. Defaults below are the shipped apps/api/.env.example values or, where that file has no line, the code defaults.
| Variable | Default | Purpose |
|---|---|---|
PORT | 4002 | Port the API listens on. Must match the web app's SYNCLE_API_ORIGIN. |
DATABASE_URL | postgresql://postgres:postgres@localhost:5433/syncle?schema=public | PostgreSQL URL for Syncle's own metadata store (connections, bridges, jobs) — not a database you sync. No code fallback: the API cannot start without it. |
REDIS_URL | redis://localhost:6379 | Redis behind the job queue. Only running bridge jobs needs it; the API boots fine with Redis down. |
SYNCLE_MASTER_KEY | unset (auto-generated) | Base64 32-byte key that encrypts stored credentials and signs session cookies. See the master key. |
SYNCLE_DATA_DIR | apps/api/.syncle | Directory for local state — the auto-generated master.key, the first-run setup token, and syncle.db. Created with mode 700. |
WEB_ORIGIN | http://localhost:3002 | Browser origins allowed by credentialed CORS, comma-separated. Only matters when the browser calls the API directly via NEXT_PUBLIC_API_URL. |
WEB_PORT | 3002 | The web app's port; sets the default CORS origin and the address in the ready banner. |
SYNCLE_JOB_CONCURRENCY | 5 | How many bridge jobs may execute concurrently. |
SYNCLE_MAX_QUERY_ROWS | 5000 | Reported as the settings default. The working ad-hoc query cap is the built-in 5000, or a per-connection maxQueryRows option. |
SYNCLE_POOL_IDLE_MS | 300000 | Idle milliseconds before a pooled database connection is closed. |
SYNCLE_SECURE_COOKIES | unset (auto-detect) | true forces the Secure attribute on session cookies, false forces it off; unset detects HTTPS from the request. |
SYNCLE_BLOCK_PRIVATE_DESTINATIONS | unset (off) | true refuses HTTP destinations that resolve to loopback, private, or link-local addresses. |
SYNCLE_SQLITE_DIR | unset (no restriction) | When set, SQLite connections may only open files under this directory. |
Three details worth knowing. SYNCLE_HOOK_CONCURRENCY is the legacy name for SYNCLE_JOB_CONCURRENCY and is still honored as a fallback; prefer the new name. SYNCLE_BLOCK_PRIVATE_DESTINATIONS is compared to the literal string true — 1 or TRUE leaves the guard off (cloud metadata endpoints are blocked regardless of this flag). And Secure-cookie detection follows the request's X-Forwarded-Proto header via Express trust-proxy — NODE_ENV plays no part in it, despite a stale comment in the env example.
The master key
SYNCLE_MASTER_KEY is a base64-encoded 32-byte key with two jobs: it encrypts stored connection credentials with AES-256-GCM, and an HKDF-derived sub-key signs login session cookies. Generate one with either of:
openssl rand -base64 32
# or, without openssl:
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Left unset, the API generates a random key on first run and writes it to master.key in the data directory with mode 600, logging a warning to set the variable in production. That auto-generated key sits beside the data it protects: a data-directory backup carries both, and losing the volume loses the key. The installer avoids this by generating a key into $SYNCLE_HOME/.env and preserving it on every re-run. A key of the wrong length is rejected at boot with SYNCLE_MASTER_KEY must be a base64-encoded 32-byte value.
Web app environment variables
Read from apps/web/.env.local (or the container environment). The browser normally calls a relative /api on the web app's own origin, and the web server proxies that to the API — so the API's address is never baked into the browser bundle.
| Variable | Default | Purpose |
|---|---|---|
SYNCLE_API_ORIGIN | http://127.0.0.1:4002 | Where the /api proxy forwards requests; must match the API's PORT. Read at request time, not build time. |
WEB_PORT | 3002 | Port the web app runs on. |
API_PORT | 4002 | Fallback port for the proxy's default origin when SYNCLE_API_ORIGIN is unset. |
NEXT_PUBLIC_API_URL | unset | Optional absolute API URL (e.g. https://api.example.com/api) that makes the browser call the API directly, skipping the proxy. |
NEXT_OUTPUT | unset | Set to standalone to build the self-contained server the Docker image runs. |
NEXT_PUBLIC_API_URL is inlined into the browser bundle at build time. Setting it turns on cross-origin requests, so the API's WEB_ORIGIN must then list the web app's origin. Leave it unset for the default same-origin proxy — the self-hosting page discusses when the direct route is worth it.
In-app settings
The Settings dialog (in the user menu) edits a handful of server-wide values at runtime. They persist as a single row in the metadata database and layer over the env values — the env vars act as defaults, not ceilings. The same values are readable and writable over HTTP via GET /api/settings and PUT /api/settings, covered on the API page.
| Setting | Default | Range | Purpose |
|---|---|---|---|
defaultPollIntervalMs | 5000 | 1000 – 3,600,000 | Default poll cadence (ms) for polling bridges. |
defaultMaxPerPoll | 500 | 1 – 5000 | Default rows fetched per poll. |
defaultCdcOperations | insert, update, delete | non-empty subset of the three | Default operation set for CDC bridges. |
maxQueryRows | SYNCLE_MAX_QUERY_ROWS, else 5000 | 1 – 1,000,000 | Cap on rows from one ad-hoc query. |
poolIdleMs | SYNCLE_POOL_IDLE_MS, else 300,000 | 10,000 – 86,400,000 | Idle ms before a pooled connection closes. |
jobConcurrency | SYNCLE_JOB_CONCURRENCY, else 5 | 1 – 100 | Concurrent bridge jobs. |
sessionTtlMinutes | 10080 (one week) | 15 – 43,200 | Minutes before a login session expires. |
A candid note on what is wired up in 1.0. sessionTtlMinutes takes effect immediately and has no env var — it is edited only here, under Settings › Security. The others are stored and reported back by the API, but do not yet steer the engine: the bridge builder hard-codes a 5 second poll, 500 rows per poll and all three CDC operations regardless of the default* values; the working query cap is the built-in 5000 or the per-connection maxQueryRows option; and worker concurrency comes solely from SYNCLE_JOB_CONCURRENCY at boot. Treat those dialog values as declarations of intent until a release wires them through. A settings row persisted before the bridges rename under the old hookConcurrency key is migrated to jobConcurrency automatically.
