Appearance
Configuration
Every setting the api and workers read, all from environment variables (the host .env). The server validates them once at boot and refuses to start with a clear message if one is missing or malformed — apps/server/src/config.ts is the single reader and .env.example mirrors it.
Booleans accept true / false / 1 / 0; an empty value means "use the default". Lists are comma-separated.
Core
| Variable | Default | Description |
|---|---|---|
NODE_ENV | development | development, production or test. In production the console's index.html is cached in memory. |
PORT | 8097 | Port the api listens on. |
HOST | 127.0.0.1 | Bind address. Keep loopback behind host nginx. |
PUBLIC_URL | http://localhost:5173 | Public origin of the console and API. Used for the Google redirect URI and invite links. |
APP_SECRET | — required | ≥ 32 characters (openssl rand -hex 32). Root key for encrypting TOTP and signing secrets at rest and signing short-lived tickets. Rotating it signs everyone out and invalidates stored two-factor and callback signing secrets. |
Database & Redis
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | — required | Postgres connection string, e.g. postgres://developer_utils:…@127.0.0.1:5432/developer_utils. |
REDIS_URL | redis://127.0.0.1:6379/5 | Redis for queues, rate limits and login throttles. Use a dedicated DB index. |
QUEUE_PREFIX | du | Prefix for every BullMQ queue and Redis key, so queues can't collide with another stack's. |
Console sign-in
| Variable | Default | Description |
|---|---|---|
PASSWORD_LOGIN | true | Allow email + password accounts (and password invite acceptance). |
GOOGLE_CLIENT_ID | — | Google OAuth client id. Google sign-in is on only when this and the secret are set. |
GOOGLE_CLIENT_SECRET | — | Google OAuth client secret. |
GOOGLE_ALLOWED_DOMAINS | (empty) | Google Workspace / email domains allowed to sign in with Google (e.g. finnoto.com). Invited people can use Google with any domain matching their invite email. |
GOOGLE_AUTO_PROVISION | true | Create a member account on first Google sign-in from an allowed domain. false makes Google sign-in invite-only. |
SESSION_TTL_DAYS | 14 | Console session lifetime; extended as you use the console. |
COOKIE_SECURE | true | Mark the session cookie Secure. Only turn off for plain-http local development. |
TRUST_PROXY | true | Read the client IP from CF-Connecting-IP / X-Forwarded-For / X-Real-IP (correct behind Cloudflare + nginx). |
Execution
| Variable | Default | Description |
|---|---|---|
RUNNER | lambda | lambda for real deployments; mock simulates every utility (with their exact response shapes) for local development and tests. |
MOCK_LATENCY_FACTOR | 1 | Scales the mock runner's simulated latency (tests use 0.02). |
LAMBDA_TRANSPORT | invoke | invoke calls the functions directly via the AWS SDK (up to 15 min). http calls their HTTP endpoints instead (capped at 29 s). |
AWS_REGION | ap-south-1 | Region of the utility functions (and of the Secrets Manager secret, if used). |
AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY | — | The gateway's cloud credentials. Read by the AWS SDK's standard credential chain, so an instance role or profile works too. |
SYNC_TIMEOUT_MS | 90000 | How long a sync request waits before answering 202 with the job id (the job keeps running). Keep it under Cloudflare's 100 s edge limit. |
WORKER_CONCURRENCY | 8 | Async jobs each worker replica runs at once. |
JOB_MAX_ATTEMPTS | 3 | 1–5. Attempts per job for transient failures (throttling, 502/503, network). A utility's own 4xx/5xx is never retried. |
Per-utility wiring
Each utility reads UTILITY_<SLUG>_*, where <SLUG> is the slug upper-cased with - → _ (html-pdf → UTILITY_HTML_PDF_…).
| Suffix | Description |
|---|---|
_ENABLED | false hides the utility from /v1 and the catalog (404). Default: enabled. |
_FUNCTION | Function name to invoke. Defaults below. |
_URL | The function's HTTP endpoint — used only with LAMBDA_TRANSPORT=http. |
_JWT_SECRET | The signing key the function validates its token against (raw value). |
_JWT_SECRET_NAME | Or: the Secrets Manager secret holding that key (fetched once and cached). |
| Utility | Slug | Variable prefix | Default _FUNCTION |
|---|---|---|---|
| HTML → PDF | html-pdf | UTILITY_HTML_PDF_ | HtmlToPdfFunction |
| Split PDF | split-pdf | UTILITY_SPLIT_PDF_ | SplitPdfFunction |
| Zip files | zip-files | UTILITY_ZIP_FILES_ | ZipFilesFunction |
| Unzip archive | unzip-files | UTILITY_UNZIP_FILES_ | UnzipFilesFunction |
| XLS → XLSX | xls-to-xlsx | UTILITY_XLS_TO_XLSX_ | xls-to-xlsx-converter |
If neither _JWT_SECRET nor _JWT_SECRET_NAME is set for a utility, its jobs fail with upstream_error naming the variable to set.
Storage
| Variable | Default | Description |
|---|---|---|
STORAGE_MODE | r2 | r2 copies every output to R2 and returns R2 links. passthrough returns the functions' own S3 links unchanged. With r2 and the real runner, the three R2_ENDPOINT/R2_ACCESS_KEY_ID/R2_SECRET_ACCESS_KEY variables are required. |
SOURCE_S3_BUCKET | finnoto-data | The bucket the functions write their outputs to. |
SOURCE_S3_REGION | ap-south-1 | Its region. |
SOURCE_S3_ENDPOINT | — | Only for a local S3-compatible store (MinIO) in development. |
SOURCE_S3_ACCESS_KEY_ID / SOURCE_S3_SECRET_ACCESS_KEY | — | Explicit credentials for the source bucket; otherwise the AWS credential chain is used. |
SOURCE_DELETE_AFTER_COPY | false | Delete the function's S3 copy once it's on R2 (needs s3:DeleteObject). |
R2_ENDPOINT | — | https://<account id>.r2.cloudflarestorage.com. |
R2_ACCESS_KEY_ID / R2_SECRET_ACCESS_KEY | — | An R2 API token with Object Read & Write on the bucket. |
R2_BUCKET | developer-utils | The R2 bucket. |
R2_PUBLIC_BASE_URL | — | The bucket's public custom domain (e.g. https://files.servicelabs.dev). Unset → links are presigned. |
R2_PRESIGN_TTL_SECONDS | 604800 | Lifetime of presigned links (max 7 days). Links within a day of expiry are re-signed whenever the job is read. |
R2_KEY_PREFIX | (empty) | Optional prefix for every object key (e.g. prod/). |
Email
Invites, password resets, security notices (password changed, 2FA on/off, new API key), key-expiry reminders and the daily digest are sent through Emithook's /v1/emails.
| Variable | Default | Description |
|---|---|---|
EMITHOOK_API_KEY | (unset) | Emithook API key allowed to send email. Unset → emails are only logged (dev/test), and "Forgot password?" is hidden. |
EMITHOOK_API_URL | https://api.emithook.com | Emithook API base URL. |
EMAIL_FROM | ServiceLabs <[email protected]> | From address. Its domain must be one of the Emithook org's verified sending identities (the platform identity, or a custom domain enabled for outbound sending), or Emithook rejects the send. |
EMAIL_REPLY_TO | (unset) | Optional Reply-To. |
DIGEST_TZ | Asia/Kolkata | Timezone of the daily digest: it covers the previous calendar day there. |
DIGEST_HOUR | 9 | Local hour (0–23) the digest is sent; key-expiry reminders go out 15 minutes later. |
Callbacks & limits
| Variable | Default | Description |
|---|---|---|
CALLBACK_ALLOW_HTTP | false | Accept http:// callback URLs (development only). |
CALLBACK_ALLOW_PRIVATE_NETWORKS | false | Allow callbacks to private / loopback / link-local addresses (development only — in production this would let callers reach the host's internal services). |
CALLBACK_TIMEOUT_MS | 10000 | Per-attempt timeout for a callback delivery. |
DEFAULT_RATE_LIMIT_PER_MIN | 600 | Rate limit given to new API keys (and to every key a guest creates). Admins can change it per key. |
JOB_RETENTION_DAYS | 90 | Finished jobs (inputs, results, timelines) older than this are deleted daily. Usage rollups are kept. |
Logging
| Variable | Default | Description |
|---|---|---|
LOG_LEVEL | info | debug, info, warn or error. Logs are JSON lines on stdout/stderr — one object per line, jq-friendly. |
A minimal production .env
ini
NODE_ENV=production
PUBLIC_URL=https://app.servicelabs.dev
APP_SECRET=<openssl rand -hex 32>
DATABASE_URL=postgres://developer_utils:<password>@127.0.0.1:5432/developer_utils
REDIS_URL=redis://:<password>@127.0.0.1:6379/5
GOOGLE_CLIENT_ID=<…>
GOOGLE_CLIENT_SECRET=<…>
GOOGLE_ALLOWED_DOMAINS=finnoto.com
AWS_ACCESS_KEY_ID=<gateway user>
AWS_SECRET_ACCESS_KEY=<…>
UTILITY_HTML_PDF_JWT_SECRET=<…>
UTILITY_ZIP_FILES_JWT_SECRET=<…>
UTILITY_UNZIP_FILES_JWT_SECRET=<…>
UTILITY_SPLIT_PDF_JWT_SECRET_NAME=<secrets manager name>
UTILITY_XLS_TO_XLSX_JWT_SECRET=<…>
R2_ENDPOINT=https://<account id>.r2.cloudflarestorage.com
R2_ACCESS_KEY_ID=<…>
R2_SECRET_ACCESS_KEY=<…>
R2_BUCKET=developer-utils
R2_PUBLIC_BASE_URL=https://files.servicelabs.dev