HSTS looks simple: send one header, force HTTPS, move on.

That’s exactly why people mess it up.

With AdonisJS, the mistakes usually aren’t about syntax. They’re about where the app sits in production, how TLS is terminated, whether subdomains are ready, and whether you’ve accidentally made local development annoying for everyone on the team.

Here are the HSTS mistakes I see most often in AdonisJS apps, plus the fixes that actually work.

First: what HSTS should look like

The header is usually this:

Strict-Transport-Security: max-age=31536000; includeSubDomains

Or if you’re going all-in on preload:

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

A year (31536000) is the normal production value. If you’re still testing, start lower.

In AdonisJS, you’ll usually set this either:

  • in app middleware
  • at the reverse proxy or load balancer
  • or both, though I prefer one clear source of truth

A simple middleware example:

// start/kernel.ts
import server from '@adonisjs/core/services/server'

server.use([
  () => import('#middleware/hsts_middleware'),
])
// app/middleware/hsts_middleware.ts
import type { HttpContext } from '@adonisjs/core/http'

export default class HstsMiddleware {
  async handle({ request, response }: HttpContext, next: () => Promise<void>) {
    await next()

    if (request.secure()) {
      response.header(
        'Strict-Transport-Security',
        'max-age=31536000; includeSubDomains'
      )
    }
  }
}

That request.secure() check matters more than people think.

Mistake 1: Sending HSTS on HTTP responses

This is probably the most common mistake.

If your app sends Strict-Transport-Security over plain HTTP, browsers ignore it. HSTS is only honored when delivered over HTTPS.

A lot of developers add the header globally and assume they’re done:

response.header(
  'Strict-Transport-Security',
  'max-age=31536000; includeSubDomains'
)

That’s sloppy. If your app is behind a proxy and AdonisJS doesn’t correctly detect HTTPS, you may think HSTS is active when it isn’t.

Fix

Only set HSTS when the request is actually secure:

import type { HttpContext } from '@adonisjs/core/http'

export default class HstsMiddleware {
  async handle({ request, response }: HttpContext, next: () => Promise<void>) {
    await next()

    if (!request.secure()) {
      return
    }

    response.header(
      'Strict-Transport-Security',
      'max-age=31536000; includeSubDomains'
    )
  }
}

Then verify the live response with your browser dev tools or a header scanner like Headertest.

Mistake 2: Forgetting that your reverse proxy terminates TLS

This one bites AdonisJS deployments behind Nginx, Caddy, Traefik, Cloud load balancers, and container platforms.

The browser connects via HTTPS to the proxy, but the proxy forwards traffic to AdonisJS over HTTP. If AdonisJS doesn’t trust the proxy headers, request.secure() may return false.

That means:

  • redirects may be wrong
  • secure cookies may break
  • HSTS may never be set

Fix

Make sure AdonisJS is configured to trust your proxy setup so forwarded protocol headers are respected.

Your exact setup depends on your AdonisJS version and deployment model, but the core idea is the same: trust X-Forwarded-Proto only from infrastructure you control.

Then test this route:

// start/routes.ts
import router from '@adonisjs/core/services/router'

router.get('/debug-request', async ({ request }) => {
  return {
    secure: request.secure(),
    protocol: request.protocol(),
    url: request.completeUrl(),
  }
})

If you hit that route through your public HTTPS URL and secure is false, your proxy config is wrong.

I’ve seen teams waste hours debugging HSTS when the real problem was “the app thinks everything is HTTP.”

Mistake 3: Enabling includeSubDomains before your subdomains are ready

includeSubDomains is great. It’s also the fastest way to break an old admin panel, forgotten staging host, or random customer vanity subdomain.

Once a browser sees:

Strict-Transport-Security: max-age=31536000; includeSubDomains

it will force HTTPS for every subdomain under that parent domain for the duration of max-age.

That includes things like:

  • api.example.com
  • admin.example.com
  • old.example.com
  • staging.example.com

If even one of them doesn’t support HTTPS correctly, users will hit hard failures.

Fix

Audit subdomains before enabling includeSubDomains.

A safer rollout:

  1. Start with the apex domain only
  2. Use a short max-age
  3. Confirm every required subdomain serves valid HTTPS
  4. Then add includeSubDomains
  5. Only consider preload after everything is boring and stable

Example staged rollout:

response.header('Strict-Transport-Security', 'max-age=300')

Then later:

response.header(
  'Strict-Transport-Security',
  'max-age=31536000; includeSubDomains'
)

Boring is good here. HSTS is not where you want surprises.

Mistake 4: Jumping straight to preload

A lot of people see preload and think “extra secure, why not.”

Because preload is sticky, that’s why.

If you submit a domain for HSTS preload, browsers can hardcode HTTPS behavior before the user even visits your site. That’s powerful, but it’s also operationally unforgiving.

Preload requires:

  • max-age of at least 1 year
  • includeSubDomains
  • preload
  • valid HTTPS across the whole domain tree you care about

Fix

Don’t use preload until you’re sure you can support it long-term.

If you do want it, the header looks like this:

response.header(
  'Strict-Transport-Security',
  'max-age=31536000; includeSubDomains; preload'
)

I’d treat preload like a deployment contract, not a security checkbox.

Mistake 5: Setting a huge max-age on day one

Technically, a year is normal. Operationally, a year is a long time if you got something wrong.

If this is your first HSTS rollout and you immediately ship:

Strict-Transport-Security: max-age=31536000; includeSubDomains

you’ve removed your margin for error.

Fix

Ramp up in stages.

A practical rollout:

// Phase 1: 5 minutes
response.header('Strict-Transport-Security', 'max-age=300')
// Phase 2: 1 day
response.header('Strict-Transport-Security', 'max-age=86400')
// Phase 3: 1 year
response.header(
  'Strict-Transport-Security',
  'max-age=31536000; includeSubDomains'
)

That gives you time to catch weird subdomain or proxy issues before they become support tickets.

Mistake 6: Enabling HSTS in local development

Don’t do this to your team.

If you serve a local domain over HTTPS with HSTS and later need to access it over HTTP, browsers may keep forcing HTTPS. That leads to confusing “why is localhost broken?” debugging.

HSTS is a production policy. Treat it like one.

Fix

Gate the header by environment.

import env from '#start/env'
import type { HttpContext } from '@adonisjs/core/http'

export default class HstsMiddleware {
  async handle({ request, response }: HttpContext, next: () => Promise<void>) {
    await next()

    if (env.get('NODE_ENV') !== 'production') {
      return
    }

    if (!request.secure()) {
      return
    }

    response.header(
      'Strict-Transport-Security',
      'max-age=31536000; includeSubDomains'
    )
  }
}

That keeps local and preview environments sane.

Mistake 7: Assuming redirects replace HSTS

I still see apps that do a clean HTTP-to-HTTPS redirect and stop there.

Redirects help first contact. HSTS helps every visit after that.

Without HSTS, the browser may still begin with an HTTP request next time. That leaves room for downgrade attacks and sloppy network behavior.

Fix

Use both:

  • redirect HTTP to HTTPS
  • send HSTS on HTTPS responses

A simple route-level redirect isn’t enough. You want this enforced consistently at the edge or in global middleware.

Mistake 8: Setting HSTS in multiple layers with conflicting values

Maybe Nginx sets:

Strict-Transport-Security: max-age=31536000

And AdonisJS sets:

Strict-Transport-Security: max-age=300; includeSubDomains

Now you’ve created ambiguity and future confusion. Depending on the setup, one may overwrite the other, or duplicate headers may appear.

Fix

Pick one layer as the source of truth.

My preference:

  • set HSTS at the reverse proxy if every app behind it should have the same policy
  • set HSTS in AdonisJS if policy differs per app or environment

Just don’t split ownership unless you like debugging config drift.

Mistake 9: Forgetting API and asset subdomains

Teams usually think about the main app and maybe www. Then six months later they realize:

  • API clients hit api.example.com
  • assets load from cdn.example.com
  • uploads come from files.example.com

If you use includeSubDomains, those hosts need proper HTTPS too. If you don’t use it, you may still want HSTS on those hosts individually.

Fix

Inventory every public hostname. Not just the “website.”

This is where a header scan and a manual host list help. Check each production hostname and confirm:

  • valid certificate
  • HTTPS redirect behavior
  • HSTS presence
  • no mixed content dependencies

A clean AdonisJS HSTS middleware example

Here’s a version I’d actually ship:

// app/middleware/hsts_middleware.ts
import env from '#start/env'
import type { HttpContext } from '@adonisjs/core/http'

export default class HstsMiddleware {
  async handle({ request, response }: HttpContext, next: () => Promise<void>) {
    await next()

    if (env.get('NODE_ENV') !== 'production') {
      return
    }

    if (!request.secure()) {
      return
    }

    response.header(
      'Strict-Transport-Security',
      'max-age=31536000; includeSubDomains'
    )
  }
}

And register it globally so it applies consistently.

If you’re unsure whether it’s live, inspect the response headers directly or run a scan with Headertest.

The practical rule

HSTS is easy to enable and easy to get wrong.

For AdonisJS, the real checklist is:

  • app correctly detects HTTPS behind proxies
  • HSTS only sent on secure production traffic
  • subdomains audited before includeSubDomains
  • preload treated as permanent-ish
  • rollout staged with sane max-age values
  • one config owner, not three

That’s the difference between “we added the header” and “we deployed HSTS without breaking anything.”

If you want the framework details for your exact version, check the official AdonisJS documentation for middleware, request handling, and proxy configuration.