Configuration
lnkify is configured entirely through environment variables defined in a .env file at the repository root. Docker Compose reads this file and injects the variables into the appropriate containers.
The .env File
The .env file uses standard KEY=VALUE syntax. Lines starting with # are comments. Values should be quoted if they contain spaces or special characters.
Example:
# Required
JWT_SECRET=a1b2c3d4e5f6...
APIKEY_PEPPER=f6e5d4c3b2a1...
DATABASE_URL=postgresql://postgres:password@db:5432/lnkify
# Optional
IPREGISTRY_API_KEY=ir_abc123...
MCP_ENABLED=true
PORT=4000Environment Variables by Service
Server
The server container is the most configurable. It reads all variables defined in the env module from the .env file.
| Variable | Required | Type | Default | Description |
|---|---|---|---|---|
JWT_SECRET | Yes | string | — | Secret key used to sign JWT authentication tokens. |
DATABASE_URL | Yes | string | — | PostgreSQL connection string (e.g. postgresql://user:password@host:port/db). |
PORT | No | string | — | HTTP port the server listens on (default: 4000). |
NODE_ENV | No | string | — | Environment mode (development, production, test). |
TRUST_PROXY | No | string | — | Number of trusted proxy hops in front of the server. Default 1 = single Caddy hop. Only raise if you add more trusted proxies; never use "true" in production (trusts entire attacker-controlled XFF chain). |
IPREGISTRY_API_KEY | No | string | — | API key for ipregistry.co geolocation lookups. Optional but enables per-click IP data. |
APIKEY_PEPPER | Yes | string | — | Pepper value (HMAC secret) used to hash API keys before storage. Required. |
RATELIMIT_GRAPHQL_PER_MIN | No | string | 120 | Max GraphQL requests per minute per identity bucket. |
RATELIMIT_MCP_PER_MIN | No | string | 120 | Max MCP requests per minute per identity bucket. |
RATELIMIT_REDIRECT_PER_MIN | No | string | 600 | Max redirect follows per minute per IP. |
RATELIMIT_BULK_PER_MIN | No | string | 10 | Max bulk operations per minute. |
BULK_MAX_ITEMS | No | string | 100 | Max items allowed in a single bulk create/update/delete call. |
MCP_ENABLED | No | string | true | Enable the Model Context Protocol server. |
MCP_PATH | No | string | /mcp | Mount path for the MCP server. |
REQUIRE_AUTH_FOR_CREATE | No | boolean | false | Require authentication to create shortlinks. When false, anonymous links are allowed. |
AUTH_RATE_LIMIT_WINDOW_MINUTES | No | string | 1 | Window (minutes) for the strict auth-mutation rate-limit bucket — login/signup/resetPassword. |
AUTH_RATE_LIMIT_MAX | No | string | 5 | Max auth mutations per short window per IP. |
AUTH_RATE_LIMIT_HOUR_WINDOW_MINUTES | No | string | 60 | Window (minutes) for the long auth-mutation rate-limit bucket. |
AUTH_RATE_LIMIT_HOUR_MAX | No | string | 20 | Max auth mutations per long window per IP. |
LOG_LEVEL | No | string | — | pino log level (trace, debug, info, warn, error, fatal). Defaults to info (debug in development). |
SENTRY_DSN | No | string | — | Sentry DSN for error tracking. Error reporting is disabled when unset. |
IP_ANONYMIZE | No | boolean | false | Anonymize stored client IPs on hit records for privacy. |
IP_RETENTION_DAYS | No | number | — | Auto-purge hit records older than this many days. Unset disables retention purging. |
MCP_SCOPES | No | list | — | Comma-separated OAuth-style scopes granted to MCP credentials. Defaults to full access (["*"]). |
REDIS_URL | No | string | — | Redis connection URL. When set, rate-limit, MCP session, and idempotency state use Redis (required for multi-replica); in-memory otherwise. |
MCP_SESSION_TTL_MS | No | number | 1800000 | Idle MCP session time-to-live (ms) before it is reaped (default: 30 min). |
MCP_MAX_SESSIONS | No | number | 100 | Global cap on concurrent MCP sessions across all identities. |
MCP_MAX_SESSIONS_PER_IDENTITY | No | number | 10 | Cap on concurrent MCP sessions per identity (user/API key). |
SLUG_CACHE_TTL_MS | No | number | 60000 | In-memory slug→target redirect cache entry TTL (ms) (default: 1 min). |
SLUG_CACHE_MAX_SIZE | No | number | 50000 | Maximum number of entries in the in-memory slug→target redirect cache. |
CORS_ORIGIN | No | string | — | Allowed CORS origin for the API. Unset means same-origin only (no cross-origin CORS headers emitted). |
DIRECT_URL | No | string | — | Direct (session-mode) Postgres URL used by prisma migrate / db-init. Needed when DATABASE_URL points at a transaction pooler, since migrations can't run over one. |
APP_URL | No* | string | https://app.lnkify.io | Public dashboard origin (the app. subdomain). Used for the / → /dash redirect, Stripe return URLs, and upgrade links. Required in production. |
PUBLIC_BASE_URL | No* | string | https://lnkify.io | Public apex origin where short links and bio pages resolve. Used to build absolute short URLs (e.g. QR codes). Required in production. |
MCP_PUBLIC_URL | No* | string | https://mcp.lnkify.io | Public origin of the MCP server, advertised in llms.txt. Required in production. |
DOCS_URL | No* | string | https://docs.lnkify.io | Public docs origin, advertised in llms.txt. Required in production. |
CADDY_TLS_CHECK_SECRET | No | string | — | Shared secret Caddy sends as X-Tls-Check-Secret to /internal/tls-check for on-demand (custom-domain) TLS. When set, the endpoint fails closed without a matching header. |
APIKEY_MAX_TTL_DAYS | No | number | 365 | Maximum lifetime (days) for a user-created API key; createApiKey rejects longer expiries. |
MCP_INIT_RATE_LIMIT_PER_MIN | No | number | 20 | Strict per-IP limit for unauthenticated MCP requests (session init / failed auth), tighter than the general MCP bucket. |
* These origin variables fall back to the hosted defaults in development but are required in production (
NODE_ENV=production).
Note: The canonical variable list lives in
server/src/utils/env.ts;docs/scripts/generate-env.jsmirrors it into.vitepress/data/env.json. Keep this table in sync when the env module changes.
Billing (Stripe)
Billing is optional and off by default. A standard self-host leaves all of these unset — usage is counted but never blocked. Set them only if you want to enforce plan quotas and charge through Stripe. See Billing & Usage.
| Variable | Type | Default | Description |
|---|---|---|---|
BILLING_ENFORCE | boolean | false | Enforce plan quotas and feature gates. When false, usage is counted but not blocked (shadow mode). |
BILLING_GRACE_DAYS | number | 3 | Days a PAST_DUE subscription keeps paid entitlements before falling back to FREE. |
STRIPE_SECRET_KEY | string | — | Stripe secret API key for server-side billing. Billing is disabled when unset. |
STRIPE_WEBHOOK_SECRET | string | — | Stripe webhook signing secret used to verify POST /webhooks/stripe. |
STRIPE_AUTO_TAX | boolean | false | Enable Stripe automatic tax collection. |
STRIPE_PRICE_STARTER_MONTHLY, STRIPE_PRICE_STARTER_ANNUAL | string | — | Stripe Price IDs for the Starter subscription. |
STRIPE_PRICE_PRO_MONTHLY, STRIPE_PRICE_PRO_ANNUAL | string | — | Stripe Price IDs for the Pro subscription. |
STRIPE_PRICE_REFILL_STARTER, STRIPE_PRICE_REFILL_PRO | string | — | Stripe Price IDs for the one-off auto-refill link packs. |
BILLING_SUCCESS_URL, BILLING_CANCEL_URL | string | — | Redirect URLs after a completed or cancelled Stripe Checkout/Portal session. |
Database
| Variable | Default | Description |
|---|---|---|
POSTGRES_USER | postgres | PostgreSQL user. |
POSTGRES_PASSWORD | (from .env) | PostgreSQL password. |
POSTGRES_DB | lnkify | PostgreSQL database name. |
Caddy
Caddy is configured via Caddyfile at the repository root, not through environment variables. See Reverse Proxy for the Caddyfile walkthrough.
Frontend & Docs
These nginx-based services do not use environment variables. They serve static builds and require no runtime configuration.
Customizing Rate Limits
The default rate limits are designed for a typical self-hosted instance with a handful of users. Adjust them based on your needs:
RATELIMIT_GRAPHQL_PER_MIN=300 # Increase for heavy API usage
RATELIMIT_REDIRECT_PER_MIN=1200 # Increase for high-traffic shortlinks
RATELIMIT_BULK_PER_MIN=30 # Increase for frequent bulk operations
BULK_MAX_ITEMS=200 # Increase bulk operation capacityRate limit counters are stored in memory and reset on server restart. See Scaling for details on replacing the in-memory store with Redis.
Disabling the MCP Server
If you don't need the Model Context Protocol server, disable it:
MCP_ENABLED=falseThis prevents the MCP endpoint from being mounted, reducing the server's attack surface.
Enabling Trust Proxy
When running behind Caddy (or any reverse proxy), Express needs to trust the proxy to correctly pick up the client's real IP address. The default TRUST_PROXY=1 trusts the single Caddy hop, which is correct and secure — clients cannot spoof their IP by prepending X-Forwarded-For entries. Only increase this value if you add additional trusted proxies in front of Caddy.
Type Checking
Every variable is supplied as a string in .env; the Type column above reflects how the server interprets it. Boolean variables (MCP_ENABLED, REQUIRE_AUTH_FOR_CREATE, IP_ANONYMIZE) use the string values "true"/"false". Numeric variables (PORT, TRUST_PROXY, rate limits, BULK_MAX_ITEMS, MCP_SESSION_TTL_MS, MCP_MAX_SESSIONS, SLUG_CACHE_*, IP_RETENTION_DAYS) are parsed to numbers at runtime. MCP_SCOPES is a comma-separated list.
Privacy & Data Retention
Click tracking stores the client IP and IP-derived geolocation per hit. To minimize the personal data you retain:
IP_ANONYMIZE=true # store anonymized IPs instead of raw addresses
IP_RETENTION_DAYS=90 # purge hit records older than 90 days (daily sweep)Per-link tracking can also be turned off entirely when creating a shortlink. See Analytics.
MCP Session Limits
The MCP server caps how many concurrent sessions it will hold, and reaps idle ones, to bound resource use:
MCP_SESSION_TTL_MS=1800000 # reap a session after 30 min idle
MCP_MAX_SESSIONS=100 # global concurrent-session cap
MCP_MAX_SESSIONS_PER_IDENTITY=10 # per user / API keyRedirect Cache
Hot redirects are served from an in-memory slug→target cache so most clicks never hit the database. It is invalidated automatically on update/delete:
SLUG_CACHE_TTL_MS=60000 # entry lifetime
SLUG_CACHE_MAX_SIZE=50000 # max cached slugsThis cache is per-process (short TTL, invalidated on write), so it stays in memory even with Redis enabled. For multi-replica deployments, see Scaling for moving the rate-limit, MCP session, and idempotency state to Redis via REDIS_URL.
Next: DNS & TLS