Security headers for a self-hosted Framer site

Which security headers to add once a Framer site is self-hosted as code, how to write a Content Security Policy that allows Custom Code, and where to set them.

Published
Reading time
6 min
Deframer cover: “Headers you now control.” Beside it, four tiles, CSP, HSTS, Frames and Referrer, with CSP selected
On this page7 sections

Once a Framer site is self-hosted as code, the security headers are yours to set. Start with Strict-Transport-Security, Referrer-Policy, X-Frame-Options (or frame-ancestors), Permissions-Policy and the X-Content-Type-Options: nosniff header a converted Next.js project already sends. Then add a Content Security Policy in report-only mode, list the domains your Custom Code scripts actually load from, and enforce it once the reports are clean. On Framer, its hosting decides these; after converting, you decide them in next.config.ts, your host's config or your reverse proxy.

Deframer is an independent tool and is not affiliated with, endorsed or sponsored by Framer. This post covers sites you've converted from your own Framer project, or one whose owner gave permission.

What the converted project already sets

A Next.js project from Deframer ships with a small, safe baseline in next.config.ts:

  • poweredByHeader: false, so responses don't advertise the framework.
  • X-Content-Type-Options: nosniff on every path, so browsers don't guess content types.

Everything else is left to you, because the right values depend on your domain, your host and the third-party scripts your site uses. That last part matters: Framer sites often carry analytics, chat widgets or tag managers in Site Settings, then Custom Code, and the converted project keeps that code as it was, running at the same points in the page. A strict policy that ignores it will break it.

The headers worth setting

Header

Suggested value

What it does

Strict-Transport-Security

max-age=31536000; includeSubDomains

Browsers only use HTTPS for your domain

Content-Security-Policy

See below

Limits where scripts, styles, images and frames load from

X-Frame-Options

DENY or SAMEORIGIN

Stops other sites framing yours (clickjacking)

Referrer-Policy

strict-origin-when-cross-origin

Sends only your origin to other sites

Permissions-Policy

camera=(), microphone=(), geolocation=()

Turns off browser features you don't use

X-Content-Type-Options

nosniff

Already set in Next.js projects

Cross-Origin-Opener-Policy

same-origin

Isolates your window from popups you open

A few cautions:

  • HSTS is sticky. Once browsers have seen it, they refuse plain HTTP for max-age. Start with a short value such as max-age=300, confirm every subdomain works over HTTPS, then raise it. Only add includeSubDomains if every subdomain has HTTPS. Don't add preload unless you've read the preload list's requirements.
  • X-Frame-Options: DENY breaks any place where you deliberately embed your own site in an iframe. Use SAMEORIGIN or the CSP's frame-ancestors if you do.
  • Cross-Origin-Opener-Policy: same-origin can interfere with payment or login popups that talk back to the opener. Leave it out if you use those.

Writing a Content Security Policy for a converted site

A CSP lists, per type of resource, where the browser may load it from. The hard part on a converted Framer site is scripts.

What the page loads

Go through these sources before writing a policy:

  1. The site itself ('self'): pages, scripts, styles, and assets if you chose "Assets in the project".
  2. Framer's CDN (https://framerusercontent.com) if you kept the default "Assets on Framer's CDN": images, fonts and videos load from there.
  3. Custom Code: every script tag and every domain those scripts load from. Tag managers load more scripts from more domains.
  4. Embeds and code components: maps, videos, calendars, and anything in src/code or src/vendor that loads from a third party.
  5. Inline scripts and styles: the framework's own inline bootstrapping scripts, Custom Code written inline, and inline styles that animations set at runtime.

A starting policy

This is a report-only starting point for a Next.js project with assets on Framer's CDN and a Google Tag Manager snippet in Custom Code. Adjust the domains to what your site really uses.

Code
default-src 'self';
script-src 'self' 'unsafe-inline' https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: blob: https://framerusercontent.com https://www.googletagmanager.com;
font-src 'self' data: https://framerusercontent.com;
media-src 'self' https://framerusercontent.com;
connect-src 'self' https://www.google-analytics.com https://*.google-analytics.com;
frame-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
upgrade-insecure-requests

Why 'unsafe-inline' for scripts and styles? Custom Code is often inline <script> blocks, the framework adds inline scripts, and animations set inline styles. A nonce-based policy is stricter, but it requires dynamic rendering and wiring a nonce into every inline script, including the ones from Custom Code. Even with 'unsafe-inline', the policy still blocks scripts from domains you didn't list, which is where most real-world injection lands. Treat a nonce-based policy as a later improvement, not a launch requirement.

form-action 'self' works for a Next.js, TanStack Start, Astro or React Router project, whose forms post to the project's own API routes. On a Vite static site, forms post to a form service; add that service's domain to connect-src.

Roll it out in report-only mode

  1. Send the policy as Content-Security-Policy-Report-Only first. Nothing is blocked; violations show in the browser console.
  2. Click through every page, open menus, play videos, submit forms, accept the cookie banner. Note each reported violation.
  3. Add the legitimate sources; remove scripts you no longer want.
  4. Switch the header name to Content-Security-Policy and test again.

Where to set the headers

In Next.js

Extend the existing headers() in next.config.ts:

ts
async headers() {
  const csp = [
    "default-src 'self'",
    "script-src 'self' 'unsafe-inline' https://www.googletagmanager.com",
    "style-src 'self' 'unsafe-inline'",
    "img-src 'self' data: blob: https://framerusercontent.com",
    "font-src 'self' data: https://framerusercontent.com",
    "frame-ancestors 'none'",
    "base-uri 'self'",
    "object-src 'none'",
  ].join("; ");
  return [
    {
      source: "/:path*",
      headers: [
        { key: "X-Content-Type-Options", value: "nosniff" },
        { key: "Content-Security-Policy-Report-Only", value: csp },
        { key: "Strict-Transport-Security", value: "max-age=300" },
        { key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
        { key: "X-Frame-Options", value: "DENY" },
        { key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
      ],
    },
  ];
},

Keep the existing nosniff entry when you edit it.

On a static host

  • Netlify and Cloudflare Pages read a _headers file in the output folder; create public/_headers so the build copies it into dist/. Netlify also accepts headers in netlify.toml.
  • Vercel reads headers in vercel.json; a Vite project's vercel.json already exists, so add a headers array to it.
  • CloudFront uses a response headers policy attached to the distribution. See S3 and CloudFront.

In a reverse proxy

With Caddy in front of a container (self-hosting with Docker):

Code
example.com {
  header {
    Strict-Transport-Security "max-age=300"
    Referrer-Policy "strict-origin-when-cross-origin"
    X-Frame-Options "DENY"
    Permissions-Policy "camera=(), microphone=(), geolocation=()"
  }
  reverse_proxy site:3000
}

Set each header in one place only. If both the app and the proxy send a CSP, browsers enforce both, and the result is the stricter intersection, which is confusing to debug.

Check the result

sh
curl -sI https://example.com | grep -iE "strict-transport|content-security|referrer|x-frame|permissions|x-content"

Online scanners such as Mozilla's HTTP Observatory grade the headers and explain each finding. Treat the grade as a checklist, not a goal: a perfect score with a broken chat widget isn't a win.

Headers are one layer

Headers protect visitors' browsers. They don't stop someone copying public pages, and they don't secure your server. Also keep:

  • Dependencies updated (npm audit, and your host's automatic updates).
  • Form webhooks secret, in environment variables, never in the repository.
  • The project in Git, so you can rebuild from scratch (back up a Framer site).

If your worry is other people converting your site, that's a different problem with different tools; what website copy protection can and cannot do explains them.

FAQ

Does Framer set security headers for me?

Framer's hosting controls the headers on a Framer-hosted site. Once you self-host the converted code, you choose them. Check your live Framer site with curl -sI if you want to match its current behavior first.

Will a Content Security Policy break my Custom Code?

It can, if a script loads from a domain the policy doesn't list. Roll out in report-only mode, add each legitimate domain, then enforce.

Why does the example allow 'unsafe-inline'?

Converted sites often include inline Custom Code, and the framework and animations use inline scripts and styles. Domain restrictions still block most injected third-party scripts. A nonce-based policy is stricter but takes more work.

Should I enable HSTS preload?

Only after HSTS has run with a long max-age and includeSubDomains without problems, and you're sure every subdomain will always have HTTPS. Removal from the preload list is slow.

Where should I put headers if I use Cloudflare in front of my host?

Either works; pick one. Setting them in the app or host config keeps them in Git with the rest of the site.

Keep reading

All posts