We had a nice setup: designers built marketing and feature pages in Framer, and we embedded them into our product. It cut a lot of development time.
Then we adopted a new analytics tool, and that setup quietly stopped working for it. This post walks through why, the options we weighed, and how we ended up with a dedicated Nginx reverse proxy — after validating the idea in Express first.
The new requirement: behavior analytics
If you run a product, you know how valuable it is to see where users click and how they move through a page.
We already used GTM and Hotjar, but we wanted more precise behavioral data, so we adopted Beusable, a tool that visualizes clicks, mouse movement, and scroll flow.
The catch: it only works inside iframes under strict conditions.
- The tracked page must be same-origin with the host page.
- The embedded page must not send CSP headers that block its script.
The problem: Framer's hosting
Framer hosts every project at project-name.framer.com. You can embed that page in an iframe, but it's a different domain — so it's not same-origin, and the analytics tool can't capture any events inside it.
Missing data for the pages we were actively iterating on meant weaker decisions about how to improve them.
Options we considered
1. A subdomain
Idea: point something like framer.example.com at Framer.
Problem: browsers treat subdomains as different origins too.
💡 Same-origin policy: if the protocol, host, or port differs, the browser treats it as a different origin and restricts access.
example.comandframer.example.comare separate origins.
2. Self-host Framer's export
Idea: export HTML/CSS from Framer and serve it ourselves.
Problem: designers edit these pages often. Every change would need a manual re-export, which is hard to automate and expensive to maintain.
3. A reverse proxy
A reverse proxy serves external content through our own domain, which solves the same-origin problem. After talking it through with our backend engineers, we went this way and compared three ways to build it:
(a) Load balancer rules at the infra layer. Every new proxy route would need an infra engineer. Slow, and hard to scale as a team practice.
(b) Middleware in our existing Express server. Our React app already ran behind Express for SEO injection, so a proxy middleware would work. But Express was already serving static files and acting as an API server. Adding proxying would pile on complexity and blur responsibilities further.
(c) A dedicated Nginx proxy. ✅ This is what we chose.
Why Nginx
Nginx is a lightweight, fast, flexible web server and reverse proxy — good at static files, load distribution, and proxying.
Pros
- Drops easily into our existing Docker-based deploy pipeline.
- Pulls a cross-cutting concern out of Express, so responsibilities are clearer.
- Security headers (HSTS, X-Frame-Options, X-Content-Type-Options) are a few lines of config.
- Bot blocking, IP restrictions, and rate limiting are available when needed.
- Gzip and proxy caching come almost for free.
Cons
- One more piece of config to maintain. We judged the benefit worth the added complexity.
Validate fast, then refactor
We wanted two things at once: keep the service stable and improve the architecture. So we didn't jump straight to Nginx.
- First, we added reverse-proxy logic directly in Express. That let us confirm the analytics tool worked through a proxy without touching infrastructure.
- Once that was proven, we refactored step by step and moved the proxy into Nginx.
Prove the effect quickly, then clean up the structure.
The new container layout
Before: the container forwarded external traffic to port 80, where Express answered everything directly.
After:
- Nginx listens on port 80 inside the container.
- Express moved to an internal port (e.g. 3000).
- Nginx routes by path: proxy to Express, serve static files directly, or proxy to Framer.
Serving static files directly:
location /static/ {
alias /path/to/static/;
expires 1d;
add_header Cache-Control "public, max-age=86400, s-maxage=86400";
gzip_static on;
try_files $uri =404;
}
Proxying Framer pages so they're same-origin:
location /service/framer/ {
proxy_pass https://your-project.framer.website/;
proxy_set_header Host your-project.framer.website;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_server_name on;
proxy_intercept_errors off;
}
Security headers:
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "upgrade-insecure-requests" always;
Results
- Analytics works. Framer pages are now served same-origin, so the heatmap tool tracks them.
- Security: HTTPS, CSP, and HSTS headers live in one place.
- Performance: static files are served by Nginx with caching and gzip.
- Maintainability: each layer has one job, so problems can be debugged independently.
What started as "make the analytics tool work" ended as a cleaner, more secure, more observable architecture.
I'm Henry, a frontend engineer focused on React, Next.js, TypeScript and Playwright E2E testing. This post is adapted from a write-up I originally published in Korean. I'm open to frontend roles abroad — let's connect on LinkedIn or see my work on GitHub.


Top comments (0)