Skip to main content

Production Checklist

This page is the pre-launch pass. Whichever platform you deploy to — AWS, Azure, DigitalOcean, Heroku or your own machine — the same set of environment variables governs how the store boots, and the same rate limits govern how it behaves under load.

Read it top to bottom once before your first production start, and again whenever you move the store to a new host or put a CDN in front of it.

Environment variables

EverShop is configured for deployment through environment variables. On a server they normally live in a .env file in the project root (the file evershop install writes) or in your platform's configuration panel — both end up in process.env, which is all EverShop reads.

Two of these have failure modes worth knowing before you set them: EVERSHOP_HOME_URL aborts the boot if it is malformed, and TRUST_PROXY_HOPS silently changes who your rate limits apply to. Both are covered in detail below the table.

VariableDefaultWhat it controls, and what happens if it is wrong
Database
DB_HOSTPostgreSQL hostname. Required.
DB_PORT5432PostgreSQL port. Managed providers often use a non-standard port (DigitalOcean uses 25060).
DB_NAMEDatabase name. Required.
DB_USERDatabase user. Needs CREATE, ALTER and DROP — migrations run on every start.
DB_PASSWORDDatabase password.
DB_SSLMODETLS off

disable turns TLS off explicitly. require, prefer, verify-ca and verify-full all enable TLS with certificate verification. no-verify enables TLS but skips verification — acceptable as a stopgap, not as a permanent setting.



Any unset or unrecognized value means no TLS at all. A typo here does not error; it silently sends your credentials in the clear.

DB_SSLROOTCERT
DB_SSLCERT
DB_SSLKEY
Filesystem paths to a CA bundle / client certificate / client key. Read only in the verifying DB_SSLMODE modes. A path that does not exist throws at startup when the connection pool is created.
Server
PORT3000Port the HTTP server binds to. A non-numeric value falls back to 3000 rather than failing — if your platform assigns a port dynamically, make sure it is exported before the process starts.
NODE_ENVset by the CLIYou do not need to set this. evershop start and evershop build force production; evershop dev forces development. A value you export is overwritten by the CLI for those commands.
EVERSHOP_HOME_URLshop.homeUrl, then localhostValidated at boot — a malformed value is fatal. The store's public base URL, used for every absolute URL the store emits. See below.
TRUST_PROXY_HOPS1Number of reverse proxies in front of the app. Determines request.ip, and therefore which bucket the rate limiter counts a request into. See below.
Tokens and secrets
JWT_ADMIN_SECRET

Signing keys for the token-based REST authentication endpoints (POST /api/user/tokens, /api/user/token/refresh, /api/customer/tokens, /api/customer/token/refresh, /api/user/session/tokens).



There is no default and no boot-time check: the endpoints throw JWT secret for … is not configured the first time they are called. If you use the REST API from a headless frontend or a mobile app, set all four to long, independent random strings. Rotating one invalidates every token signed with it.



Cookie-session logins to the admin panel and the storefront do not use these.

JWT_ADMIN_REFRESH_SECRET
JWT_CUSTOMER_SECRET
JWT_CUSTOMER_REFRESH_SECRET
JWT_ISSUERevershopThe iss claim on issued tokens.
JWT_ADMIN_TOKEN_EXPIRY
JWT_ADMIN_REFRESH_TOKEN_EXPIRY
JWT_CUSTOMER_TOKEN_EXPIRY
JWT_CUSTOMER_REFRESH_TOKEN_EXPIRY
900
54000
1800
108000
Token lifetimes in seconds (15 min / 15 h / 30 min / 30 h).
ORDER_TRACKING_TOKEN_SECRETSigns the anonymous order-tracking links embedded in shipment emails. If unset, those links cannot be generated and the tracking page renders "this link is no longer valid". Set it if guests can place orders.
Images
IMAGE_ALLOWED_HOSTSempty

Comma-separated allowlist of hosts the /images optimization proxy may fetch from. It is a strict allowlist: it blocks SSRF against internal addresses and stops the endpoint being used as a free image proxy for third-party sites.



EverShop logs a warning at boot when this is unset and no cloud storage host can be derived from your configuration. With nothing allowed, only local media, public and theme images are optimized; external images are refused. When S3, Azure or GCS file storage is configured, that provider's host is allowed automatically — you do not need to repeat it here.

Logging
LOG_FILEconsolePath to a log file. When set, log output goes to that file instead of the console — check it before you conclude the app is silent. Ignored in debug mode (evershop dev, or --debug), which always logs to the console.
LOGGER_LEVELwarnMinimum level to record: error, warn, info, http, verbose, debug, silly. The default of warn means informational messages are dropped in production. Debug mode overrides this to silly.
Cloud file storage — only needed for the provider you selected in Admin → Settings. See File Storage.
AWS_BUCKET_NAME
AWS_REGION
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
AWS_S3_ENDPOINT
AWS_S3_FORCE_PATH_STYLE
AWS_S3_BASE_URL
Amazon S3 and S3-compatible services. Omit the key pair to use the AWS SDK's default credential chain (instance role, IRSA, shared config) — the recommended setup on EC2 and EKS. AWS_S3_ENDPOINT plus AWS_S3_FORCE_PATH_STYLE cover R2, MinIO and Spaces; AWS_S3_BASE_URL serves files through a CDN instead of the bucket host.
GCS_BUCKET_NAME
GCS_BASE_URL
Google Cloud Storage. Credentials come from Application Default Credentials (an attached service account, or GOOGLE_APPLICATION_CREDENTIALS) unless you paste a service-account key in the admin settings.
AZURE_STORAGE_CONNECTION_STRING
AZURE_STORAGE_CONTAINER_NAME
AZURE_STORAGE_BASE_URL
container: imagesAzure Blob Storage. The container is created on first use if it does not exist.
info

Anything set through an environment variable or config/<env>.json wins over the matching admin setting, and the admin form shows those fields as read-only. That is deliberate: it lets infrastructure pin credentials that a store operator must not be able to change from the browser.

Installer variables

evershop install also reads ADMIN_FULLNAME, ADMIN_EMAIL and ADMIN_PASSWORD, and skips the matching prompt when one is present. Useful for scripting an unattended first install. They have no effect on a running store.

EVERSHOP_HOME_URL — validated, and fatal when wrong

This is the recommended way to set your store's public base URL. It overrides the shop.homeUrl configuration key, so you can change the domain without editing a config file or rebuilding.

EVERSHOP_HOME_URL="https://your-store.com"

Everything the store emits as an absolute URL is derived from it:

  • links in transactional emails (order confirmations, shipment notifications, password resets)
  • <loc> entries in sitemap.xml and the absolute Sitemap: line in the generated robots.txt
  • canonical tags and hreflang alternates on multi-language stores

Left unset, EverShop falls back to shop.homeUrl and then to http://localhost:$PORT. That fallback is the classic production bug: the store works fine in a browser, and every order confirmation emails a localhost link to the customer.

A malformed value stops the boot

The value is validated during module bootstrap. It must parse as a URL and use the http or https protocol. your-store.com (no scheme) and ftp://your-store.com both throw, and the process exits before it starts listening.

An unset or empty value is allowed — it just falls back. Only a set but invalid value is fatal. If a deploy suddenly fails to come up right after you touched this variable, that is where to look.

Set the full origin, with the scheme and without a trailing slash or path. Update it when you move from a platform-assigned hostname to your custom domain, and again if you switch from http to https.

TRUST_PROXY_HOPS — who the rate limiter sees

Every deployment in this section runs behind at least one proxy. EverShop only learns the real client address from the X-Forwarded-For header, and it only trusts as much of that header as you tell it to.

TRUST_PROXY_HOPS sets Express's trust proxy to a hop count. That determines request.ip, and request.ip is the key the rate limiter buckets on.

ValueTopologyFailure mode when this is the wrong choice
0The Node process is directly internet-facing. X-Forwarded-For is not trusted at all.Correct only with no proxy. Behind one, every request is attributed to the proxy's address — one bucket for the entire internet.
1 (default)One reverse proxy or load balancer: nginx, an ALB, the Heroku router, the App Service front end, the App Platform load balancer.The common case, and the reason this is the default.
2 or moreA chain: CDN → load balancer → app. Cloudflare or CloudFront in front of nginx is two.

Set too low and the address you see is the last proxy's, not the visitor's — all traffic collapses into a single bucket and a normal traffic spike returns 429 to everybody at once.



Set too high and the app trusts an entry in X-Forwarded-For that the client supplied. A client can then present a fresh fake IP on every request and never hit a limit at all.

Count the hops that actually terminate and re-forward the connection, and set the variable to exactly that number. An unset, empty, negative or non-integer value falls back to 1.

Built-in rate limits

EverShop ships a per-client-IP rate limiter, mounted early in the middleware stack — before session lookup and before any database work — so a flood is shed before it consumes a connection from the pool.

TierApplies toLimit
PagesEverything not matched by another tier — storefront and admin pages.300 requests per minute per IP (~5/second)
API/api and everything under /api/.120 requests per minute per IP
Auth

POST to these paths only:
/customer/login
/admin/user/login
/api/customers (registration)
/api/customers/reset-password
/api/customers/password

8 requests per 15 minutes per IP

A leading locale prefix is stripped before matching, so /fr/customer/login lands in the auth tier just like /customer/login.

Exempt paths

These never count against any limit:

  • Static assets — any path ending in a known asset extension: .js, .mjs, .cjs, .css, .map, .png, .jpg, .jpeg, .gif, .svg, .webp, .avif, .ico, .woff, .woff2, .ttf, .eot, .txt, .xml, .json. (This is why /robots.txt and /sitemap.xml are also exempt.)
  • Hot module replacement/__webpack_hmr… and any .hot-update path.
  • /backend/ — the admin development bundle.
  • /health and /healthz — reserved for uptime probes, so a monitor polling every few seconds never exhausts a bucket.
caution

/health and /healthz are exempt from rate limiting, but EverShop core does not define them as routes — a request to either returns a 404 unless you add the route yourself in an extension. If your platform requires a health-check endpoint, point it at one of these two paths and implement it, so the probe traffic stays exempt.

The 429 contract

When a limit is exceeded, the response is 429 Too Many Requests with:

  • a Retry-After header, in seconds — 60 for the page and API tiers, 900 for the auth tier
  • standard RateLimit-* headers on every response, so a well-behaved client can back off before it is throttled (the legacy X-RateLimit-* headers are not sent)
  • a JSON body for /api/** paths, matching EverShop's normal error envelope:
{
"error": {
"status": 429,
"message": "Too many requests. Please slow down and try again later."
}
}
  • a plain-text body for page requests

What you cannot change

These limits are hardcoded and not operator-configurable. There is no config key and no environment variable for the windows, the thresholds or the tiers. They are deliberately generous — sized so that an office or a mobile carrier sharing one NAT address still browses comfortably — and exist as a capacity safety net rather than as a tunable policy.

The limiter is skipped when NODE_ENV=test, which keeps integration suites deterministic. That is not an escape hatch for production: evershop start forces NODE_ENV=production, so a running store always has the limiter active.

A per-IP limit is not DDoS protection

Per-IP limits stop a single abusive source. A distributed flood arrives from thousands of addresses and each one stays under the threshold. If that is in your threat model, put a WAF or CDN with edge rate limiting in front of the store — and remember to raise TRUST_PROXY_HOPS when you do.

Build and start

Two commands, in this order:

npm run build
npm run start

evershop build compiles the production bundles for every route. It takes no flags — asset minification is always on for a production build and cannot be disabled. Run it on every deploy, after npm install, and re-run it whenever you change a theme, add an extension or upgrade EverShop.

evershop start boots the server in production mode. It sets NODE_ENV=production itself, loads .env, runs module bootstrap, and binds to $PORT (default 3000).

Migrations run on start

Database migrations are applied automatically during startup, after bootstrap and before the server accepts requests. There is no separate migrate command to run and nothing to schedule.

The practical consequences:

  • The database user in DB_USER needs DDL privileges — CREATE, ALTER, DROP — permanently, not just at install time.
  • Take a backup before starting a new version. Migrations are not reversible; rolling the application back does not roll the schema back.
  • On a multi-instance deployment, be aware that every instance runs migrations at boot. Roll instances one at a time rather than restarting the whole fleet at once.
  • The very first start on an empty database also creates the schema, so a fresh deploy has a longer cold start than subsequent ones.

Creating the first administrator is a separate, explicit step — evershop install does it locally, and on a server you run:

npm run user:create -- --email "admin@example.com" --name "Admin" --password "a-strong-password"

Final pass before launch

  • EVERSHOP_HOME_URL is set to the real public origin, with https:// and no trailing slash — and a test email actually links to it.
  • TRUST_PROXY_HOPS matches the real number of proxies. Confirm it by checking that different clients produce different addresses in your logs.
  • DB_SSLMODE is one of the verifying modes, not left unset.
  • The four JWT secrets are set to independent random strings if you use the REST API; ORDER_TRACKING_TOKEN_SECRET is set if guests can order.
  • IMAGE_ALLOWED_HOSTS is set, or cloud file storage is configured — check the boot log for the warning.
  • LOG_FILE points somewhere you will actually look, and log rotation is configured for it.
  • Uploaded media is on cloud storage, or on a volume that survives a redeploy. See File Storage.
  • Automated database backups are on, and you have restored one at least once.
  • No hand-written public/robots.txt or public/sitemap.xml is shadowing the generated ones. See Static File Serving.
  • Store settings — currency, timezone, units, languages, tax — are set in Admin → Settings. See Store Settings.
  • Demo seed data has not been run against the production database.
  • npm run build completed without errors on the exact commit you are deploying.