GioJSdocs
On this page

Production Checklist

What to check before a GioJS app takes real traffic - and what you can leave alone because the defaults already got it right.

GioJS starts with production-safe defaults: no error details in responses, security headers on every response, cross-site form posts refused, dev endpoints off, metrics off. Most of this list is about the parts only you can know: your secrets, your proxy, your traffic. Each item links to the page with the details.

Runtime mode

  • Start in production mode. Anything but NODE_ENV=development is production - npm start and a standalone run.mjs default to it, npm run dev does not. The server decides once and starts its workers in the same mode.
  • Check an error page. In production a failed render shows only an error reference (digest); the message and stack go to the log under the same digest. If you see a stack trace in the browser, the server is in development mode. See Error Handling.
  • Use Node 20 or newer, and keep @gio.js/server, @gio.js/core and @gio.js/react on the same version - they are released in lockstep.
  • Check where fonts come from. A [[fonts]] file in public/ (the starter's) is copied at every start and needs no network - make sure it is in the image or standalone folder, since a missing file stops startup. An https:// font url is downloaded into .gio/fonts/ on a fresh host's first start, and a failed download stops startup: allow outbound HTTPS to that URL, persist the folder, or move the file into public/. See Font Optimization.

Secrets and environment

  • Set GIO_SESSION_SECRET (32+ bytes) if you use sessions or require_session guards. Without it, createSessionStorage() throws, so every page and route.ts importing your session module answers 500 (the log names the file and the missing secret under the response's digest), and guards deny every request. The server still starts - check npx gio routes with the production environment for routes marked (failed to load).
  • Set GIO_REVALIDATE_TOKEN (32+ bytes) only if a CMS or script calls POST /_gio/revalidate; without it the endpoint does not exist.
  • Keep secrets out of the browser. Nothing secret is named GIO_PUBLIC_*, getServerSideProps returns only what the page shows (props are sent to the browser), and modules holding secrets import @gio.js/core/server-only.
  • Keep secrets out of git: .env*.local is ignored, and production values live in the host's environment. See Environment Variables.

Security headers and CSP

  • Defaults are on: X-Content-Type-Options, X-Frame-Options: SAMEORIGIN, Referrer-Policy. Add Permissions-Policy and the cross-origin policies your app can live with in [security.headers].
  • Add a Content-Security-Policy. Start with csp_report_only, watch the browser console on every page, then switch to csp. Nonces are fresh per response, cache hits included; nonce your own inline and third-party scripts with cspNonce(). See the Content Security Policy guide.
  • HSTS. GioJS sends Strict-Transport-Security on its own only when it terminates TLS. Behind a TLS proxy or a platform, set [security] hsts = true once the whole site is HTTPS.
toml
[security]
csp_report_only = "default-src 'self'; script-src 'self' 'nonce-{nonce}' 'strict-dynamic'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; object-src 'none'; base-uri 'self'; frame-ancestors 'self'"
hsts = true

[security.headers]
permissions-policy = "camera=(), microphone=(), geolocation=()"

CSRF and origins

  • Same-origin forms and fetch calls just work; cross-site POST/PUT/PATCH/DELETE requests and WebSocket upgrades get a 403 before Node sees them.
  • List other origins of yours that post to this app (an admin subdomain, a marketing site) in [security.csrf] trusted_origins.
  • Exempt cross-site callbacks - OAuth/OIDC response_mode=form_post, SAML ACS, payment-provider returns, webhooks that send an Origin - in [security.csrf] exempt, and make sure each verifies its own signature or state. See CSRF protection.
  • Keep state changes off GET, and session cookies SameSite=Lax (the default) or Strict.

Reverse proxy and client IPs

  • Trust exactly your proxy in [server] trusted_proxies, so rate limits, the metrics allowlist and req.ip see visitors instead of the proxy. Leave it empty when GioJS faces the internet directly.
  • Make GioJS unreachable around the proxy: bind host = "127.0.0.1" or firewall the port.
  • Pass the original Host through - the CSRF and WebSocket checks compare the browser's Origin with it.
  • Decide who sets X-Request-Id: let the proxy set it, or accept_request_id = false if the proxy passes a client's through.
  • Line up keep-alive timeouts with a proxy that pools connections, or it can answer with sporadic 502s; turn off response buffering (nginx proxy_buffering off) so streamed pages stream. See Behind a reverse proxy.

Limits

  • Rate-limit what attackers hammer: login and signup actions, password resets, expensive APIs. [[rate_limits]] run in Rust before routing. See Rate limits.
  • Size request bodies: [server] max_body_bytes (2 MiB by default) caps uploads - raise it for file uploads, and match the proxy's limit (nginx client_max_body_size).
  • Keep the file-descriptor limit (ulimit -n, LimitNOFILE= in systemd) above [server] max_connections (10000). See Connection limits.
  • Allow only the image hosts you use in [[images.remote_patterns]], with a pathname where you can.

Caching

Render workers

  • Pick a worker count. One worker is plenty when most traffic is cache hits and static files. When uncached renders queue up, set [server] workers to a number or "auto".
  • Budget memory per worker - each is a full Node process (often 100-200 MB) - and set container limits for the whole pool.
  • Run one-time jobs once: plugin onStartup runs in every worker; guard migrations and schedulers with process.env.GIO_WORKER_INDEX === '0' or move them out of the server. See Sizing.

Logs, metrics and health

  • JSON logs for a log shipper: GIO_LOG_FORMAT=json (or [logging] format = "json"). Every line of a request carries its request id in both processes. See Observability.
  • Metrics: [metrics] enabled = true with an ip_allowlist or token - without either it answers only this machine, and an allowlist of 0.0.0.0/0 with no token logs a warning at startup. Alert on 5xx rates per route, p95 latency, and a climbing gio_worker_restarts_total.
  • Health checks on /_gio/health. It always answers 200 while the server runs; read nodeReady for "can render right now". [health] details = false keeps the deployment id and worker counts out of it; [health] enabled = false turns it into a 404, so point every probe at a page of your own first. See Health check.
  • Graceful stops: stop with SIGTERM and allow at least 15-20 seconds (requests drain for up to 8, then workers get a few more). Several defaults are shorter: Docker's 10 seconds (use --stop-timeout 20), Fly.io's 5 (kill_timeout = 20) and Railway's 0 ("drainingSeconds": 20) - see Deploying. In systemd use KillMode=mixed.

SEO and URLs

  • Set GIO_SITE_URL (or metadataBase) so canonical and Open Graph URLs, sitemap.xml and robots.txt are absolute. See Metadata & SEO.
  • Never derive URLs or security decisions from req.host - the host header is client-supplied.

Before every release

npm run build            # typecheck (tsc --noEmit) - normal deploys have no other build step
npm test                 # your tests - @gio.js/core/testing has renderPage, callRoute, createTestServer
npx gio build standalone # if you ship a standalone folder or image

Then deploy with one of the deployment recipes, and keep an eye on known limitations when planning features.