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.
| Variable | Default | What it controls, and what happens if it is wrong |
|---|---|---|
| Database | ||
DB_HOST | — | PostgreSQL hostname. Required. |
DB_PORT | 5432 | PostgreSQL port. Managed providers often use a non-standard port (DigitalOcean uses 25060). |
DB_NAME | — | Database name. Required. |
DB_USER | — | Database user. Needs CREATE, ALTER and DROP — migrations run on every start. |
DB_PASSWORD | — | Database password. |
DB_SSLMODE | TLS off |
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_SSLROOTCERTDB_SSLCERTDB_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 | ||
PORT | 3000 | Port 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_ENV | set by the CLI | You 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_URL | shop.homeUrl, then localhost | Validated 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_HOPS | 1 | Number 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 ( There is no default and no boot-time check: the endpoints throw 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_ISSUER | evershop | The iss claim on issued tokens. |
JWT_ADMIN_TOKEN_EXPIRYJWT_ADMIN_REFRESH_TOKEN_EXPIRYJWT_CUSTOMER_TOKEN_EXPIRYJWT_CUSTOMER_REFRESH_TOKEN_EXPIRY | 900540001800108000 | Token lifetimes in seconds (15 min / 15 h / 30 min / 30 h). |
ORDER_TRACKING_TOKEN_SECRET | — | Signs 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_HOSTS | empty | Comma-separated allowlist of hosts the 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 |
| Logging | ||
LOG_FILE | console | Path 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_LEVEL | warn | Minimum 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_NAMEAWS_REGIONAWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEYAWS_S3_ENDPOINTAWS_S3_FORCE_PATH_STYLEAWS_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_NAMEGCS_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_STRINGAZURE_STORAGE_CONTAINER_NAMEAZURE_STORAGE_BASE_URL | container: images | Azure Blob Storage. The container is created on first use if it does not exist. |
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.
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 insitemap.xmland the absoluteSitemap:line in the generatedrobots.txt- canonical tags and
hreflangalternates 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.
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.
| Value | Topology | Failure mode when this is the wrong choice |
|---|---|---|
0 | The 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 more | A 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 Set too high and the app trusts an entry in |
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.
| Tier | Applies to | Limit |
|---|---|---|
| Pages | Everything 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 |
| 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.txtand/sitemap.xmlare also exempt.) - Hot module replacement —
/__webpack_hmr…and any.hot-updatepath. /backend/— the admin development bundle./healthand/healthz— reserved for uptime probes, so a monitor polling every few seconds never exhausts a bucket.
/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-Afterheader, in seconds —60for the page and API tiers,900for the auth tier - standard
RateLimit-*headers on every response, so a well-behaved client can back off before it is throttled (the legacyX-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.
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_USERneeds 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_URLis set to the real public origin, withhttps://and no trailing slash — and a test email actually links to it. -
TRUST_PROXY_HOPSmatches the real number of proxies. Confirm it by checking that different clients produce different addresses in your logs. -
DB_SSLMODEis 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_SECRETis set if guests can order. -
IMAGE_ALLOWED_HOSTSis set, or cloud file storage is configured — check the boot log for the warning. -
LOG_FILEpoints 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.txtorpublic/sitemap.xmlis 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 buildcompleted without errors on the exact commit you are deploying.