Environment variables
Every environment variable the Pinwren server reads, with its type, default and effect.
Pinwren validates its configuration when it starts. A missing or invalid value stops the server with one message that lists every problem, for example:
Invalid Pinwren configuration:
- PINWREN_URL: PINWREN_URL must be the public URL, e.g. https://feedback.example.com
- PINWREN_SECRET: PINWREN_SECRET must be at least 32 charactersOnly three variables are required: DATABASE_URL, PINWREN_URL and PINWREN_SECRET. Everything else has a
default.
Environment beats the dashboard
Some settings can be edited by an admin in the dashboard and set in the environment — SMTP, the session lifetime and the update check. The rule is the same for all of them:
- The value an admin saves lives in the database.
- A variable set in the environment overrides that value and locks the field: the dashboard shows it
as read-only, and saving it is refused with
409 SETTING_LOCKED. - Remove the variable and restart to hand the setting back to the dashboard.
The Enterprise licence works the same way: with PINWREN_LICENSE_FILE set, the upload in Settings ›
Licence is locked and refused with 409 LICENSE_LOCKED.
Use the environment for anything you manage as code; use the dashboard for anything you want admins to change without a redeploy.
With Docker Compose
Every variable in docker/.env reaches the app (env_file in docker/compose.yml, Docker Compose v2.24+)
— see Passing more settings.
Client addresses
Rate limits and the sign-in limiter key on the client's IP address. PINWREN_TRUST_PROXY is the
number of reverse proxies in front of Pinwren whose X-Forwarded-For entries it believes:
| Value | Meaning |
|---|---|
0 or false (default) | No proxy. Forwarded headers are ignored; the TCP peer address is used. |
1 or true | One proxy (Caddy, nginx, Traefik). The client is the address that proxy connected from or appended. |
2, 3, … | That many proxies in a chain, e.g. 2 for a CDN in front of your proxy. |
Pinwren walks X-Forwarded-For from the right and skips one entry per trusted hop, so whatever a client writes
into the header itself is never taken as its address. X-Real-IP is never read. Set the count to exactly what
you run: too low and every client looks like your proxy (one shared rate limit); too high and clients can choose
their own address. The details are under Reverse proxy.
Sign-up and instance admins
- Sign-up. With
PINWREN_DISABLE_SIGNUPunset, the first account can always be created and after that only people with a pending invitation can sign up.PINWREN_DISABLE_SIGNUP=falseopens sign-up to anyone.PINWREN_DISABLE_SIGNUP=truebehaves like unset: the first account and invited people can still sign up. - Instance admins (SMTP, security, licence and other instance settings) are the first account created on
the server, plus every address in
PINWREN_ADMIN_EMAILS— see First run. Keep a new server private until that first account exists.
Sign in with Google or GitHub
Both are optional and offered only when configured. Their secrets are read from the environment only, never stored in the database.
- Create an OAuth app: in Google Cloud (APIs & Services › Credentials › OAuth client ID, type "Web application") or on GitHub (Settings › Developer settings › OAuth Apps).
- Register the callback URL:
PINWREN_URL/api/auth/callback/googleorPINWREN_URL/api/auth/callback/github. Settings › Security › Sign-in methods shows both, ready to copy. - Set
GOOGLE_CLIENT_IDandGOOGLE_CLIENT_SECRET, orGITHUB_CLIENT_IDandGITHUB_CLIENT_SECRET, and restart. Setting only one value of a pair stops the server with a configuration error.
New accounts from Google or GitHub follow the same sign-up rules as a password sign-up (first account,
invitations, PINWREN_DISABLE_SIGNUP). Passkeys and email sign-in links need no variables: they are switched in
Settings › Security, and email links also need SMTP. Passkeys are bound to the host name in PINWREN_URL, so
changing that host means everyone adds their passkeys again.
Outbound requests
Webhooks, chat channels (Slack, Discord, Teams, Telegram, ntfy), webhook panel actions, the GitHub/GitLab base URL and the Jira site URL an admin enters are URLs typed by users, so Pinwren refuses to send requests to private and internal addresses: loopback, RFC 1918 ranges, CGNAT, link-local and cloud metadata addresses, IPv6 unique-local and link-local, multicast, documentation and reserved ranges — including IPv6 forms that embed one of those IPv4 addresses. Redirects are never followed.
PINWREN_OUTBOUND_ALLOWLISTadmits internal targets, comma- or space-separated: exact host names (gitlab.internal), wildcard subdomains (*.corp.example) and CIDRs (10.20.0.0/16). Use it for a self-managed GitLab, Mattermost or an internal webhook receiver.PINWREN_OUTBOUND_ALLOW_PRIVATE=trueswitches the address check off entirely. It is meant for local development and tests only — never set it on a server people can reach.
GITHUB_API_URL, GITLAB_URL and SLACK_API_URL come from you, not from a user, so they may point at private
addresses without the allowlist (timeouts, size caps and no-redirects still apply). SLACK_API_URL (default
https://slack.com/api) is the Slack Web API the Slack app calls; change it only for a
mock or a proxy.
AI triage
Optional. Set these to configure the AI provider once for the whole instance; an admin can instead choose it under Settings → AI, and what is saved there wins. See AI triage.
| Variable | Meaning |
|---|---|
AI_PROVIDER | anthropic (default), openai or ollama |
AI_MODEL | The model name |
AI_API_KEY | The provider key |
AI_BASE_URL | Provider endpoint. From you, not a user, so it may be a private address (like Ollama) |
PINWREN_EU_PRIVACY | true switches AI enrichment off for every project |
Intake and invitation limits
| Variable | Default | Limit |
|---|---|---|
WIDGET_RATE_LIMIT_PER_MIN | 10 | Reports (and replies) per minute from one address to one project, unless the project sets its own. |
WIDGET_PROJECT_REPORTS_PER_MIN | 120 | Reports per minute to one project, however many addresses send them (429 PROJECT_RATE_LIMITED). |
WIDGET_PROJECT_MB_PER_HOUR | 500 | Megabytes of files (screenshots, attachments, replays) stored per project and hour (429 PROJECT_STORAGE_LIMITED). |
PINWREN_INVITES_PER_HOUR | 30 | Invitations one organisation may send per hour. |
Cloud billing (Dodo Payments)
Self-hosted installs have no billing: nothing below is read unless PINWREN_MODE=cloud. On Pinwren Cloud the
plans (Free and Pro) are sold through Dodo Payments. Billing stays off until
DODO_PAYMENTS_API_KEY is set.
| Variable | Purpose |
|---|---|
DODO_PAYMENTS_API_KEY | Server-side API key. Billing is disabled while it is empty. |
DODO_PAYMENTS_ENVIRONMENT | test_mode (default) or live_mode; picks test.dodopayments.com or live.dodopayments.com. |
DODO_PAYMENTS_WEBHOOK_KEY | Signing secret (whsec_…) of the webhook endpoint that points at /api/billing/webhook. |
DODO_PRO_MONTHLY_PRODUCT_ID | Id of the Pro monthly subscription product you created in the Dodo dashboard. |
DODO_PRO_ANNUAL_PRODUCT_ID | Id of the Pro annual subscription product. |
An interval whose product id is empty is not offered, and asking for it answers 503 BILLING_NOT_CONFIGURED
naming the missing variable. Incoming billing events are checked against the signing secret above (five-minute
replay window) and applied once per event id. A workspace is matched to its subscription by the
pinwren_org_id metadata set at checkout.
Cloud sign-up
Only read when PINWREN_MODE=cloud. Self-hosted sign-up is still decided by PINWREN_DISABLE_SIGNUP.
Cloud sign-up is email and password with a confirmation link, so SMTP must be configured.
| Variable | Default | Meaning |
|---|---|---|
PINWREN_CLOUD_SIGNUPS | closed | open lets anyone create an account; closed pauses it (the kill switch). Invited addresses can still sign up while it is closed. |
PINWREN_CLOUD_SIGNUPS_PER_IP_HOUR | 5 | New accounts one client address may create per hour. Needs a correct PINWREN_TRUST_PROXY behind a proxy. |
PINWREN_CLOUD_SIGNUPS_PER_HOUR | 100 | New accounts the whole instance accepts per hour. |
PINWREN_TERMS_URL | https://pinwren.xride-labs.in/legal/terms | Where the "Terms of Service" link in the sign-up form points. |
PINWREN_PRIVACY_URL | https://pinwren.xride-labs.in/legal/privacy | Where the "Privacy Policy" link points. |
Each new account gets a personal workspace on the Free plan and, after it confirms its email address, a guided first run: name the workspace, create the first project, copy the widget snippet and invite teammates (within the plan's seats). Disposable-mailbox domains are refused. Not included yet: CAPTCHA, social sign-up in Cloud, and cleanup of accounts that never confirm.
Reference
Required
| Variable | Type | Default | Description |
|---|---|---|---|
DATABASE_URLrequired | string | — | Postgres connection string. The Docker compose file builds it from POSTGRES_PASSWORD. |
PINWREN_URLrequired | url | — | The public URL people open and the widget calls, without a trailing slash, e.g. https://feedback.example.com. |
PINWREN_SECRETrequired | string | — | At least 32 characters (openssl rand -base64 48). Signs sessions and encrypts stored credentials such as the SMTP password and tracker tokens. Keep it stable: changing it signs everyone out and makes stored credentials unreadable. |
Server
| Variable | Type | Default | Description |
|---|---|---|---|
PINWREN_DATA_DIR | string | ./data | Folder for uploaded files when STORAGE_DRIVER=fs. The Docker image sets it to /data. |
PINWREN_DISABLE_SIGNUP | "true" | "false" | "" | — | Close public sign-up once your team has accounts; invite everyone else. |
PINWREN_ADMIN_EMAILS | string | — | Comma-separated emails of instance admins (email settings, security settings). Unset: the first user who signed up. |
PINWREN_ROLE | "all" | "web" | "worker" | all | all serves HTTP and runs background jobs; web serves only (jobs are still enqueued); worker runs jobs only. |
PINWREN_WORKERS | number | 2 | Node processes serving requests in the Docker image (node:cluster), roughly one per CPU core. 1 runs a single process. |
DATABASE_POOL_MAX | number | 10 | Postgres connections per process for requests. |
PINWREN_JOBS_POOL_MAX | number | 3 | Postgres connections per process for the pg-boss job queue. |
WIDGET_RATE_LIMIT_PER_MIN | number | 10 | Reports and replies one IP may send per project per minute. Each project can override it. |
PINWREN_SKIP_MIGRATIONS | boolean | false | true skips the automatic database migrations on start. |
PINWREN_MIGRATIONS_DIR | string | — | Where the SQL migrations live. Set by the Docker image; leave it alone. |
Security
| Variable | Type | Default | Description |
|---|---|---|---|
PINWREN_TRUST_PROXY | string | false | Believe X-Forwarded-For, -Proto and -Host. Turn on only behind a reverse proxy that sets them (the compose file defaults it to on). |
PINWREN_ENFORCE_HTTPS | boolean | false | Redirect or refuse plain-HTTP requests that don't come from localhost. |
PINWREN_SESSION_DAYS | number (1–90) | — | Session lifetime in days (1–90) for new sign-ins. Setting it locks the field in Settings › Security. |
Email (SMTP)
| Variable | Type | Default | Description |
|---|---|---|---|
SMTP_HOST | string | — | SMTP server. Without it, emails are skipped (in-app notifications still work). |
SMTP_PORT | number (1–65535) | — | SMTP port, usually 587 (STARTTLS) or 465 (TLS). |
SMTP_USER | string | — | SMTP username. |
SMTP_PASSWORD | string | — | SMTP password. |
SMTP_SECURE | boolean | — | true for implicit TLS (port 465); false uses STARTTLS when the server offers it. |
SMTP_REQUIRE_TLS | boolean | — | true refuses to send unless the server upgrades the connection with STARTTLS. |
SMTP_TLS_REJECT_UNAUTHORIZED | boolean | — | false accepts a self-signed or otherwise unverifiable server certificate (internal relays only). |
EMAIL_FROM | string | — | Sender, e.g. "Pinwren <feedback@example.com>". |
Storage
| Variable | Type | Default | Description |
|---|---|---|---|
STORAGE_DRIVER | "fs" | "s3" | fs | fs stores files under PINWREN_DATA_DIR; s3 uses any S3-compatible bucket. |
S3_ENDPOINT | string | — | S3 endpoint URL (AWS, Cloudflare R2, MinIO, Backblaze B2, …). Required with STORAGE_DRIVER=s3. |
S3_BUCKET | string | — | Bucket name. Required with STORAGE_DRIVER=s3. Keep it private. |
S3_REGION | string | auto | Bucket region; auto works for R2 and most S3-compatible services. |
S3_ACCESS_KEY_ID | string | — | Access key. Required with STORAGE_DRIVER=s3. |
S3_SECRET_ACCESS_KEY | string | — | Secret key. Required with STORAGE_DRIVER=s3. |
S3_FORCE_PATH_STYLE | boolean | true | Path-style URLs (endpoint/bucket/key), needed by MinIO and most self-hosted S3. |
Sign-in providers
| Variable | Type | Default | Description |
|---|---|---|---|
GOOGLE_CLIENT_ID | string | — | OAuth client id of a Google Cloud OAuth app. With GOOGLE_CLIENT_SECRET, the sign-in page offers Continue with Google. Callback: PINWREN_URL/api/auth/callback/google. |
GOOGLE_CLIENT_SECRET | string | — | Client secret of that Google OAuth app. Set both or neither. |
GITHUB_CLIENT_ID | string | — | Client id of a GitHub OAuth app. With GITHUB_CLIENT_SECRET, the sign-in page offers Continue with GitHub. Callback: PINWREN_URL/api/auth/callback/github. |
GITHUB_CLIENT_SECRET | string | — | Client secret of that GitHub OAuth app. Set both or neither. |
Issue trackers
| Variable | Type | Default | Description |
|---|---|---|---|
GITHUB_API_URL | url | https://api.github.com | GitHub API base URL. Each integration can override it (GitHub Enterprise Server). |
GITLAB_URL | url | https://gitlab.com | GitLab base URL. Each integration can override it (self-managed GitLab). |
Slack
| Variable | Type | Default | Description |
|---|---|---|---|
SLACK_API_URL | url | https://slack.com/api | Slack Web API base URL used by the Slack app integration. Change it only for a mock or a proxy. |
Updates
| Variable | Type | Default | Description |
|---|---|---|---|
PINWREN_UPDATE_CHECK | boolean | — | Daily check for a newer release, shown to instance admins. Setting it locks the switch in Settings › Security; it is off on its own while any project uses privacy mode. |
PINWREN_NUDGES | boolean | true | Show instance admins the "update available" strip and, now and then, a dismissible star/sponsor card. false hides all of them. Never shown on Pinwren Cloud. |
PINWREN_UPDATE_REPO | string | Krithik-m-codes/pinwren | GitHub repository whose latest release is compared with PINWREN_VERSION. |
PINWREN_VERSION | string | 0.6.0 | The running version (set by the release); used by the update check. |
Enterprise licence
| Variable | Type | Default | Description |
|---|---|---|---|
PINWREN_LICENSE_FILE | string | — | Path to an Enterprise licence file inside the container. Setting it overrides and locks the upload in Settings › Licence. Community installs need no licence. |
PINWREN_INSTANCE_ID | string | — | Only for licences bound to one instance: this instance's id, matched against the licence. |
Other
| Variable | Type | Default | Description |
|---|---|---|---|
PINWREN_MODE | "selfhost" | "cloud" | selfhost | — |
PINWREN_SERVICE_DATABASE_URL | string | — | — |
PINWREN_CLOUD_SIGNUPS | "open" | "closed" | closed | Cloud only: whether strangers may create accounts. closed (the default) pauses public sign-up, so
a deployment never opens it by accident; invitations to existing workspaces still work. |
PINWREN_CLOUD_SIGNUPS_PER_IP_HOUR | number | 5 | Cloud only: new accounts one client address may create per hour. |
PINWREN_CLOUD_SIGNUPS_PER_HOUR | number | 100 | Cloud only: new accounts the whole instance accepts per hour (a flood brake). |
PINWREN_TERMS_URL | url | https://pinwren.xride-labs.in/legal/terms | Cloud only: the legal pages the sign-up consent checkbox links to. |
PINWREN_PRIVACY_URL | url | https://pinwren.xride-labs.in/legal/privacy | — |
PINWREN_OUTBOUND_ALLOWLIST | string | — | Hosts (gitlab.internal, *.corp.example) and CIDRs (10.0.0.0/8) that webhooks, panel actions
and tracker URLs may reach although they are private. Everything private is refused otherwise. |
PINWREN_OUTBOUND_ALLOW_PRIVATE | boolean | false | Development and tests only: let outbound requests reach any address. |
WIDGET_PROJECT_REPORTS_PER_MIN | number | 120 | Per project, whatever the sender's address: reports per minute and stored bytes per hour. |
WIDGET_PROJECT_MB_PER_HOUR | number | 500 | — |
PINWREN_INVITES_PER_HOUR | number | 30 | Invitations an organisation may send per hour. |
DODO_PAYMENTS_API_KEY | string | — | Cloud billing (WP-22, Dodo Payments). Only read when PINWREN_MODE=cloud; billing is off while the API key is unset. Product ids map the Pro plan's intervals to Dodo products created in its dashboard. |
DODO_PAYMENTS_ENVIRONMENT | "test_mode" | "live_mode" | test_mode | — |
DODO_PAYMENTS_BASE_URL | url | — | Overrides the API origin derived from the environment (tests, proxies). |
DODO_PAYMENTS_WEBHOOK_KEY | string | — | Signing secret of the webhook endpoint pointing at /api/billing/webhook (whsec_�). |
DODO_PRO_MONTHLY_PRODUCT_ID | string | — | — |
DODO_PRO_ANNUAL_PRODUCT_ID | string | — | — |
Generated from packages/api/src/env.ts on every build.
The table is generated on every build from the Zod schema the server validates its configuration with,
packages/api/src/env.ts,
plus the few variables read elsewhere (the cluster entry, the database pools and migrations). Boolean
variables accept true, false, 1 or 0, except PINWREN_DISABLE_SIGNUP, which takes true or false.
Compose-only variables
docker/compose.yml reads a few more values from docker/.env to wire the containers together. They are not
passed to Pinwren itself.
| Variable | Default | What it does |
|---|---|---|
POSTGRES_PASSWORD | — (required) | Password of the pinwren database user; compose builds DATABASE_URL from it. |
PINWREN_IMAGE | pinwren:latest | The image to run. Set ghcr.io/krithik-m-codes/pinwren:<version> to use a release instead of building. |
PINWREN_PORT | 3000 | Host port the app is published on. |
PINWREN_BIND | 127.0.0.1 | Host address the port is bound to. 0.0.0.0 exposes it on every interface (not recommended without a proxy). |
Besides the three required values, the compose file forwards PINWREN_TRUST_PROXY, PINWREN_DISABLE_SIGNUP,
PINWREN_ADMIN_EMAILS, PINWREN_OUTBOUND_ALLOWLIST, PINWREN_WORKERS, PINWREN_LICENSE_FILE, the AI variables and the DODO_* Cloud billing variables from
docker/.env. It sets PINWREN_TRUST_PROXY=1 by default, because it expects one reverse proxy in front. Set
PINWREN_TRUST_PROXY=0 if clients reach the container directly, and a higher number if there are more proxies
— see Client addresses.
Generating secrets
openssl rand -base64 48 # PINWREN_SECRET
openssl rand -hex 24 # POSTGRES_PASSWORDPINWREN_SECRET signs sessions and derives the AES-256-GCM key that encrypts stored credentials (the SMTP
password, webhook signing secrets, tracker tokens). Changing it signs everyone out, and those stored secrets
must be entered again.
Deploy on Fly.io with Supabase
Run Pinwren Cloud on Fly.io with Supabase Postgres and Storage, with roles, offline migrations and provisioning, secrets, domain and a rollback runbook.
Reverse proxy and HTTPS
Put Caddy, nginx or Traefik in front of Pinwren for TLS, compression, the forwarded headers and the body sizes it needs.