WWebBuddyMarketplace
Install and run

Configuration

Every environment variable and admin setting in LinkNest, which ones are build-time, how payment keys are resolved, and the values that only live in code.

9 min readUpdated Oct 17, 2026Applies to LinkNest 1.0.0

On this page

LinkNest is configured in three places:

  • Environment variables bootstrap the app: the database, sign-in, admins, the encryption key for payment secrets, your public hostname and the optional AI and email keys.
  • Admin settings are stored in the site_settings table and edited in the admin panel: branding and SEO under Admin → Brand, gateway keys under Admin → Payment settings. Plans have their own table, edited under Admin → Plans.
  • Code constants cover a few limits that have no setting yet. They are listed at the end of this page.

Environment variables

Core

Variable Required Default What it does
DATABASE_URL Production Embedded in-memory PGLite Postgres connection string. Empty or whitespace counts as unset. Without it, all data is lost on restart and the migration script skips itself.
BETTER_AUTH_SECRET Production Random per process Signs sessions. Without it, everyone is signed out whenever the server restarts. Generate with openssl rand -base64 32.
BETTER_AUTH_URL Recommended Worked out per request Your public origin with the scheme, for example https://links.example.com. Used as the sign-in base URL and added to the trusted origins.
VITE_PUBLIC_HOSTNAME Production Unset Your public host without the scheme, for example links.example.com. Read at build time for the share-image URL in every page's head, and at runtime by the server for trusted sign-in origins, the checkout return allow-list, the custom-domain target and the default broadcast sender.
ADMIN_EMAILS Production Unset: nobody new becomes admin Comma-separated email addresses, for example you@example.com,ops@example.com. A signed-in account with a listed address is seated as a platform admin; nobody else is. Read at runtime. Email addresses are not verified, so register every listed address yourself before the site is public. See Admins.
SETTINGS_ENCRYPTION_KEY Recommended BETTER_AUTH_SECRET A long random string. Encrypts the payment secrets saved under Admin → Payment settings. See Encrypted payment secrets.

.env.example fills ADMIN_EMAILS with you@example.com and the two secrets with placeholder text. Replace all three before you deploy: a placeholder secret is public, and an unregistered example address can be claimed by anyone.

Build time

These are compiled into the browser bundle. Changing them means rebuilding (in Docker, pass them with --build-arg).

Variable Default What it does
VITE_PUBLIC_HOSTNAME Unset See above. Pass it at build time and set it at runtime.
VITE_SOCIAL_AUTH Off Exactly true shows "Continue with Google" and "Continue with X" in the sign-in modal. Leave it false: those buttons go through a hosted sign-in broker that a self-hosted install does not have credentials for. See Social sign-in.
VITE_AUTH_ENABLED Sign-in on Exactly false turns sign-in off and runs every request as one shared development user. The server also reads it at runtime and refuses every request when a DATABASE_URL is set. Development only.
NITRO_PRESET vercel Set to node-server for Docker or a VM. The Dockerfile sets it for you.

Server

Variable Default What it does
PORT 3000 for the built server (the Docker image sets 3000); the dev server is always 8080 Listening port.
HOST All interfaces (the Docker image sets 0.0.0.0) Listening address.

AI and email

Variable Default What it does
XAI_API_KEY Unset Server-only. Turns on model-written plans in the AI co-pilot. Without it the co-pilot uses its rule-based planner. Never give it a VITE_ name.
RESEND_API_KEY Unset Sends creators' email broadcasts through Resend. Without it a broadcast is saved as "ready" for the creator to copy.
RESEND_FROM hello@ plus VITE_PUBLIC_HOSTNAME The sender address. It must be on a domain verified in Resend.

Payment fallbacks

Every gateway key can be set here or in Admin → Payment settings. Read how they are resolved before choosing.

Variable Admin field
STRIPE_SECRET_KEY Stripe secret key
STRIPE_PUBLISHABLE_KEY (also VITE_STRIPE_PUBLISHABLE_KEY or NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) Stripe publishable key
STRIPE_WEBHOOK_SECRET Stripe webhook secret
PAYPAL_CLIENT_ID PayPal client ID
PAYPAL_CLIENT_SECRET (also PAYPAL_SECRET) PayPal secret
PAYPAL_WEBHOOK_ID PayPal webhook ID
RAZORPAY_KEY_ID Razorpay key ID
RAZORPAY_KEY_SECRET Razorpay key secret
RAZORPAY_WEBHOOK_SECRET Razorpay webhook secret

.env.example lists most of these but not PAYPAL_WEBHOOK_ID or RAZORPAY_WEBHOOK_SECRET.

Social sign-in

Variable Default What it does
GROK_AUTH_ISSUER https://auth.grok.me The hosted sign-in broker behind the Google and X buttons.
GROK_AUTH_CLIENT_ID, GROK_AUTH_CLIENT_SECRET A built-in preview client Credentials for that broker. The built-in client only accepts preview hosts, so on your own domain the buttons fail.

Email and password is the supported way to sign in on a self-hosted install. Google and X sign-in cannot be pointed at your own OAuth apps without changing src/lib/auth/server.ts.

Set by the platform

Variable What it does
VERCEL_URL, VERCEL_PROJECT_PRODUCTION_URL Set by Vercel. Their hosts are added to the trusted sign-in hosts.

Demo mode

Variable Required Default What it does
DEMO_MODE No Off true adds Explore the creator studio and Explore the admin panel to the sign-in dialog and seeds a sample Premium page, /u/solandclay, with sample customers for the admin panel. Demo accounts use the reserved demo.invalid domain and are read-only on the server: no saves, broadcasts, checkouts, payouts or xAI calls. Real accounts are unaffected. BETTER_AUTH_SECRET must be set.

Note

The demo admin sees only demo data and fully masked payment keys. Real admins still see the demo pages under People, but the demo payments and payouts are left out of Finance.

Admin settings

Brand and SEO

Edited under Admin → Brand, stored in site_settings. There is no environment fallback for these.

Field Key Default Where it shows
Product name brand_name LinkNest Page titles, the public page's tab title, PayPal and Razorpay plan names, tip checkout line items. Up to 40 characters.
Tagline brand_tagline Built-in line Under the name in the admin sidebar only.
Logo brand_logo The built-in mark Site header, studio and admin sidebars. Uploaded, cropped square to 256 px and stored in the database.
Favicon brand_favicon /favicon.svg Browser tab. Uploaded, cropped to 64 px and stored in the database.
SEO title seo_title The product name <title>, Open Graph and Twitter titles. Up to 70 characters.
SEO description seo_description Built-in description Meta, Open Graph and Twitter descriptions. Up to 180 characters.
Share image seo_og_image https://<VITE_PUBLIC_HOSTNAME>/og.jpg og:image.
X handle seo_twitter Empty twitter:site.
Hide from search engines seo_noindex Off Adds robots: noindex, nofollow.

Tip

An uploaded share image is stored as a data: URL, which social networks do not fetch. For link previews that work, leave the share image empty and replace public/og.jpg (1200 by 630) before you build.

Rebranding through Admin → Brand does not reach every string. Several screens print linknest.app/<username> or "LinkNest" from the source; see Known limitations.

Payment settings

Edited under Admin → Payment settings. Each gateway has a Test connection button.

Gateway Fields Good to know
Stripe Publishable key, secret key, webhook secret, test mode Keys are checked for the pk_, sk_ or rk_ and whsec_ prefixes. The test-mode switch is a label: the key you paste decides test or live.
PayPal Client ID, secret, webhook ID, sandbox Sandbox is on until you turn it off. It decides whether LinkNest talks to api-m.sandbox.paypal.com or api-m.paypal.com.
Razorpay Key ID, key secret, webhook secret, test mode, currency, INR per 1 USD Currency is USD (default) or INR. Prices are in USD; INR charges convert them with the INR per 1 USD rate, which appears when INR is chosen. Without a rate, Razorpay charges USD. The test-mode switch is a label: the key decides. See Razorpay.

A gateway counts as connected when its two keys are present: Stripe's secret and publishable keys, PayPal's client ID and secret, Razorpay's key ID and secret. Each screen shows the webhook URL to paste into the provider; Selling it as SaaS has the steps.

How payment settings are resolved

For each payment key, LinkNest reads the database first:

  1. If a value is saved in site_settings, it wins.
  2. If nothing is saved and an environment variable is set, the environment value is used and copied into site_settings on that first read.
  3. Otherwise the gateway is not configured.

Because of step 2, changing a payment variable in the environment after the app has read it has no effect. Change it in Admin → Payment settings instead, or remove the saved copy so the new environment value is copied on the next read: Clear under a saved secret does this for secrets, and for the other keys delete the row from site_settings.

Saving an empty field in the admin panel leaves the old value in place. To remove a saved secret, use Clear next to it (shown for the Stripe secret key and webhook secret, the PayPal secret, and the Razorpay key secret and webhook secret). Checkout through that gateway stops until a new one is saved. If an environment variable also sets that secret, the screen says so, and the environment value is used and saved again on the next read.

Encrypted payment secrets

These five settings are stored encrypted with AES-256-GCM: the Stripe secret key, the Stripe webhook secret, the PayPal secret, the Razorpay key secret and the Razorpay webhook secret. The publishable key, PayPal client ID, PayPal webhook ID and Razorpay key ID are stored as plain text, as are the other settings. The admin screens and Admin → Export mask the secrets.

The encryption key is derived from SETTINGS_ENCRYPTION_KEY, or from BETTER_AUTH_SECRET when SETTINGS_ENCRYPTION_KEY is empty. Secrets saved as plain text by an older build are still read, and are saved again encrypted the first time they are read.

Warning

Keep the key unchanged once secrets are saved. If SETTINGS_ENCRYPTION_KEY changes, or BETTER_AUTH_SECRET changes while it is the fallback, or you first set SETTINGS_ENCRYPTION_KEY after secrets were saved with the fallback, the saved secrets can no longer be read. LinkNest then treats them as not set: the gateway stops working, the log shows [settings] <key> is encrypted but could not be decrypted, and you must enter the secrets again under Admin → Payment settings. Secrets that also come from environment variables are picked up and saved again on their own.

With neither SETTINGS_ENCRYPTION_KEY nor BETTER_AUTH_SECRET set, saving a payment secret fails with "Set SETTINGS_ENCRYPTION_KEY (or BETTER_AUTH_SECRET) before saving payment secrets." Secrets from environment variables still work but are not saved to the database.

Encryption protects a copied database or backup on its own. Anyone who also has the key can read the secrets, so keep the key apart from the backups and restrict database access.

Values that live in code

What Where Value
AI co-pilot runs per page COPILOT_CAP in src/lib/grove/copilot.ts 40, for the life of the page
xAI model used by the co-pilot src/lib/grove/copilot.ts grok-4-1-fast-non-reasoning
Minimum payout request src/lib/grove/earn.ts 1000 cents ($10)
Minimum tip or product price at checkout src/lib/payments/earn-checkout.ts 100 cents ($1)
Themes, typefaces and which ones are free src/lib/grove/themes.ts, src/routes/dashboard/appearance.tsx 18 themes (first 6 free), 9 typefaces (3 free)
Insight rules src/lib/grove/insights.ts See Analytics
Hosts that can never be claimed as custom domains src/lib/grove/domain-format.ts localhost, *.vercel.app, *.localhost and a few product hosts
Broadcast rate limit src/lib/grove/broadcasts.ts One broadcast per page every five minutes
Subscribe field limit assertPublicFormLimit in src/lib/http/rate-limit.ts 5 per visitor IP per page every 10 minutes, and 300 per page per hour
Message form limit (notes and booking requests) src/lib/http/rate-limit.ts 5 per visitor IP per page every 10 minutes, and 100 per page per hour
Password and email unlock limit src/lib/http/rate-limit.ts 20 per visitor IP per page every 10 minutes, and 600 per page per hour
Link shortener limit createShortLink in src/lib/grove/tools.ts 30 new short links per account per hour

The form and shortener limits are fixed windows counted in the rate_limits table (migration 0016). A visitor over a limit gets "Too many tries. Wait a few minutes and try again."; a creator over the shortener limit gets "You've made a lot of short links. Try again in an hour." If the database cannot be reached, the limits are skipped rather than blocking the page. The visitor IP comes from the proxy's headers; see Client IP for rate limits.