Express.js, secure out of the box

REST API, sign-in with a second factor, password reset by email, account self-service and user management in one project. The groundwork is secured and tested, so your code starts with the actual job.

This response came with 10 security headers
  • content-security-policy default-src 'self';base-uri 'none';script-src 'self';script-src-attr 'none';style-src 'self';img-src 'self';font-src 'self';connect-src 'self';manifest-src 'self';media-src 'none';worker-src 'none';form-action 'self';frame-ancestors 'none';object-src 'none'
  • strict-transport-security max-age=63072000; includeSubDomains; preload
  • cross-origin-opener-policy same-origin
  • cross-origin-embedder-policy require-corp
  • cross-origin-resource-policy same-origin
  • x-frame-options DENY
  • x-content-type-options nosniff
  • referrer-policy no-referrer
  • permissions-policy accelerometer=(), camera=(), display-capture=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=(), browsing-topics=()
  • cache-control no-store

The path of a request

Before your code sees a request, it passes seven stations. Scroll along.

  1. 01

    Arrival

    Every request gets an ID that shows up in each log line and on error pages. Anything larger than 100 KB is rejected before the server reads it.

    X-Request-Id: 7c1e…
  2. 02

    Headers

    A Content Security Policy without inline scripts, HSTS, COOP, COEP and a Permissions-Policy that blocks camera, microphone and location.

    Content-Security-Policy: default-src 'self'
  3. 03

    Rate limit

    300 requests per 15 minutes and IP, 10 failed attempts on sign-in and password forms. With RATE_LIMIT_STORE=database several instances share the counters.

    RateLimit-Policy: 300;w=900
  4. 04

    Origin

    Forms carry a CSRF token that is masked anew each time it is rendered. Posts from other sites already fail the Fetch Metadata check.

    Sec-Fetch-Site: same-origin
  5. 05

    Input

    Joi checks every field against a schema and rejects unknown ones. MongoDB operators and prototype keys are stripped first.

    400 "email" must be a valid email
  6. 06

    Identity

    Session cookie or JWT, the role comes from the database, with a second factor if the user turned it on. After a password change, old tokens and sessions stop working.

    Authorization: Bearer eyJhbGciOiJIUzI1NiJ9…
  7. 07

    Response

    Errors reveal no internals, only an error ID. Duration and status end up as a Prometheus metric.

    200 OK

What is inside

API and browser

JSON API under /v1 with access and refresh tokens, plus server-rendered pages with session sign-in. OpenAPI docs at /docs.

Complete sign-in

Sign-up, email confirmation, forgotten password, two-factor with an authenticator app and recovery codes.

Accounts users control

Profile, password, signed-in devices, activity log, data export and deletion, as the GDPR requires.

Administration

Find users, assign roles and delete accounts, in the browser and through the API with the same rights.

Two languages

Interface and emails in German and English. Another language is one more file.

Ready for production

Docker, health checks, Prometheus metrics, structured logs with request IDs and a load test for every endpoint.

Running locally in three commands

Node.js 22 is enough: the data goes to SQLite until DATABASE_URL names MongoDB, PostgreSQL, MySQL or another server. Setup writes a .env with random keys.

  1. npm run setupCreate .env with your own keys
  2. npm run create-adminCreate the first administrator
  3. npm run devStart the server, restart on changes