Skip to content

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:

env
# 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=4000

Environment Variables by Service

Server

The server container is the most configurable. It reads all variables defined in the env module from the .env file.

VariableRequiredTypeDefaultDescription
JWT_SECRETYesstringSecret key used to sign JWT authentication tokens.
DATABASE_URLYesstringPostgreSQL connection string (e.g. postgresql://user:password@host:port/db).
PORTNostringHTTP port the server listens on (default: 4000).
NODE_ENVNostringEnvironment mode (development, production, test).
TRUST_PROXYNostringNumber 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_KEYNostringAPI key for ipregistry.co geolocation lookups. Optional but enables per-click IP data.
APIKEY_PEPPERYesstringPepper value (HMAC secret) used to hash API keys before storage. Required.
RATELIMIT_GRAPHQL_PER_MINNostring120Max GraphQL requests per minute per identity bucket.
RATELIMIT_MCP_PER_MINNostring120Max MCP requests per minute per identity bucket.
RATELIMIT_REDIRECT_PER_MINNostring600Max redirect follows per minute per IP.
RATELIMIT_BULK_PER_MINNostring10Max bulk operations per minute.
BULK_MAX_ITEMSNostring100Max items allowed in a single bulk create/update/delete call.
MCP_ENABLEDNostringtrueEnable the Model Context Protocol server.
MCP_PATHNostring/mcpMount path for the MCP server.
REQUIRE_AUTH_FOR_CREATENobooleanfalseRequire authentication to create shortlinks. When false, anonymous links are allowed.
AUTH_RATE_LIMIT_WINDOW_MINUTESNostring1Window (minutes) for the strict auth-mutation rate-limit bucket — login/signup/resetPassword.
AUTH_RATE_LIMIT_MAXNostring5Max auth mutations per short window per IP.
AUTH_RATE_LIMIT_HOUR_WINDOW_MINUTESNostring60Window (minutes) for the long auth-mutation rate-limit bucket.
AUTH_RATE_LIMIT_HOUR_MAXNostring20Max auth mutations per long window per IP.
LOG_LEVELNostringpino log level (trace, debug, info, warn, error, fatal). Defaults to info (debug in development).
SENTRY_DSNNostringSentry DSN for error tracking. Error reporting is disabled when unset.
IP_ANONYMIZENobooleanfalseAnonymize stored client IPs on hit records for privacy.
IP_RETENTION_DAYSNonumberAuto-purge hit records older than this many days. Unset disables retention purging.
MCP_SCOPESNolistComma-separated OAuth-style scopes granted to MCP credentials. Defaults to full access (["*"]).
REDIS_URLNostringRedis connection URL. When set, rate-limit, MCP session, and idempotency state use Redis (required for multi-replica); in-memory otherwise.
MCP_SESSION_TTL_MSNonumber1800000Idle MCP session time-to-live (ms) before it is reaped (default: 30 min).
MCP_MAX_SESSIONSNonumber100Global cap on concurrent MCP sessions across all identities.
MCP_MAX_SESSIONS_PER_IDENTITYNonumber10Cap on concurrent MCP sessions per identity (user/API key).
SLUG_CACHE_TTL_MSNonumber60000In-memory slug→target redirect cache entry TTL (ms) (default: 1 min).
SLUG_CACHE_MAX_SIZENonumber50000Maximum number of entries in the in-memory slug→target redirect cache.
CORS_ORIGINNostringAllowed CORS origin for the API. Unset means same-origin only (no cross-origin CORS headers emitted).
DIRECT_URLNostringDirect (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_URLNo*stringhttps://app.lnkify.ioPublic dashboard origin (the app. subdomain). Used for the //dash redirect, Stripe return URLs, and upgrade links. Required in production.
PUBLIC_BASE_URLNo*stringhttps://lnkify.ioPublic apex origin where short links and bio pages resolve. Used to build absolute short URLs (e.g. QR codes). Required in production.
MCP_PUBLIC_URLNo*stringhttps://mcp.lnkify.ioPublic origin of the MCP server, advertised in llms.txt. Required in production.
DOCS_URLNo*stringhttps://docs.lnkify.ioPublic docs origin, advertised in llms.txt. Required in production.
CADDY_TLS_CHECK_SECRETNostringShared 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_DAYSNonumber365Maximum lifetime (days) for a user-created API key; createApiKey rejects longer expiries.
MCP_INIT_RATE_LIMIT_PER_MINNonumber20Strict 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.js mirrors 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.

VariableTypeDefaultDescription
BILLING_ENFORCEbooleanfalseEnforce plan quotas and feature gates. When false, usage is counted but not blocked (shadow mode).
BILLING_GRACE_DAYSnumber3Days a PAST_DUE subscription keeps paid entitlements before falling back to FREE.
STRIPE_SECRET_KEYstringStripe secret API key for server-side billing. Billing is disabled when unset.
STRIPE_WEBHOOK_SECRETstringStripe webhook signing secret used to verify POST /webhooks/stripe.
STRIPE_AUTO_TAXbooleanfalseEnable Stripe automatic tax collection.
STRIPE_PRICE_STARTER_MONTHLY, STRIPE_PRICE_STARTER_ANNUALstringStripe Price IDs for the Starter subscription.
STRIPE_PRICE_PRO_MONTHLY, STRIPE_PRICE_PRO_ANNUALstringStripe Price IDs for the Pro subscription.
STRIPE_PRICE_REFILL_STARTER, STRIPE_PRICE_REFILL_PROstringStripe Price IDs for the one-off auto-refill link packs.
BILLING_SUCCESS_URL, BILLING_CANCEL_URLstringRedirect URLs after a completed or cancelled Stripe Checkout/Portal session.

Database

VariableDefaultDescription
POSTGRES_USERpostgresPostgreSQL user.
POSTGRES_PASSWORD(from .env)PostgreSQL password.
POSTGRES_DBlnkifyPostgreSQL 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:

env
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 capacity

Rate 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:

env
MCP_ENABLED=false

This 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:

env
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:

env
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 key

Redirect 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:

env
SLUG_CACHE_TTL_MS=60000   # entry lifetime
SLUG_CACHE_MAX_SIZE=50000 # max cached slugs

This 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

Released under the MIT License.