CORS & Same Origin
A website running at one domain cannot read responses from a website running at another domain. This is the Same Origin Policy, one of the most important security rules in browsers, and one of the most commonly misunderstood.
This page explains what same origin means, when CORS lets you bend the rule on purpose, and how Dashify configures the gates.
What "same origin" means
Two URLs are the same origin if all three of the following match exactly:
- Scheme (
httpsvshttp). - Host (
dashify.example.comvsapp.dashify.com). - Port (the
:8080part, if any).
https://app.example.com/foo and https://app.example.com/bar are same origin. https://app.example.com and http://app.example.com are not (different scheme). https://app.example.com and https://api.example.com are not (different host).
Why the policy exists
Without same origin, any website you visit could secretly fetch data from every other website you are logged into. You visit evil.example.com. While you are there, evil.example.com's JavaScript fetches your bank's account page (your bank cookie is attached, the page loads). Now evil.example.com reads your balance, your transaction history, your account number, all of it. Game over.
Same Origin Policy stops this by saying: a page can make requests to other origins, but the browser will not let JavaScript read the response. So evil.example.com can fire off a fetch to your bank, but the response is invisible to it. The bank stays private.
The tradeoff
Same Origin is brutal. But sometimes you legitimately want one origin to talk to another. Dashify itself is a perfect example: in production the browser app might live at app.dashify.example and the API might live at api.dashify.example. Different hosts. Same Origin would forbid the front from talking to the back.
The escape hatch is CORS, Cross Origin Resource Sharing. CORS is the protocol by which a server can tell the browser, "yes, I trust requests from that other origin, let it read my responses."
How CORS works
Every cross origin request goes through one of two flows.
Simple requests (basic GETs, simple POSTs with form data, no custom headers) just go through. The browser includes an Origin: https://app.dashify.example header on the request. The server's response includes an Access-Control-Allow-Origin header that either matches the requesting origin or doesn't. If it matches, the browser lets JavaScript see the response. If not, the response is fetched but kept hidden, JavaScript sees an opaque error.
Preflighted requests (anything with a custom header, a non-trivial content type like JSON, or an HTTP method like PUT or DELETE) go through a two step dance. Before the real request, the browser sends an OPTIONS preflight asking "can I send this kind of request?" The server responds with what is allowed. If the real request fits, the browser proceeds; otherwise it never even tries.
The preflight is what makes CORS expensive, it doubles the round-trips for non-simple requests. Servers can mitigate this with Access-Control-Max-Age to let the browser cache preflight responses.
How Dashify configures CORS
The Dashify API has a strict CORS configuration. Two rules:
Allowed origins are explicit. The API has an environment variable listing the exact origins that may call it, typically the production browser app's origin and a development host. There is no wildcard. There is no "allow *." If your origin is not on the list, you are blocked at the CORS layer regardless of what other credentials you present.
Credentials are allowed only with explicit origins. Setting Access-Control-Allow-Origin: * and Access-Control-Allow-Credentials: true is forbidden by the spec, the browser refuses requests configured that way. This is good, it forces the API to name its allowed origins specifically when cookies are involved, removing entire classes of accidents.
The Dashify config explicitly enumerates origins, and Allow-Credentials is true only for those origins (so the cookie is sent and the response is readable).
What about the same origin proxy trick?
In local development, the browser app at localhost:3000 talks to the API at localhost:6001. Different ports = different origins. CORS would apply.
Dashify side-steps the issue entirely: Vite's dev server proxies API requests under /api/* and /socket.io/* to the API server. From the browser's perspective, the requests go to localhost:3000 (same origin). From the API's perspective, the requests come from the proxy on the same machine. No CORS preflights. Faster.
In production, the architecture is similar, a reverse proxy (nginx, Traefik, your CDN) often serves both the front-end and the API on the same hostname under different paths. Same origin by construction; no CORS at all.
What CORS does not protect
A common confusion: CORS is not a server side security boundary. It is a browser mechanism that prevents other websites from reading responses. It does not stop a determined attacker from making requests directly to the API with curl, Postman, or a script, those tools simply ignore CORS because they are not browsers.
For protection against non-browser attacks, you need authentication (which Dashify has), rate limiting (also has), and authorisation checks (also has). CORS is one layer in the cake, not the whole cake.
Common CORS mistakes (avoided)
- Reflecting
Originblindly intoAccess-Control-Allow-Origin. This effectively whitelists every origin and defeats the policy. Dashify's CORS config comparesOriginagainst an explicit list and only echoes it back if it matches. - Using
Allow-Origin: *with credentials. Browsers refuse this combination, but not all libraries handle it cleanly. Dashify never sets the wildcard when credentials are enabled. - Forgetting that the OPTIONS preflight is hit first. Some logging stacks miss preflights because they are short and don't carry a body. Dashify logs both and Pino includes the request method.
Key takeaways
- Same Origin Policy prevents one website from reading another website's data, the foundation of web security.
- CORS is the protocol that lets a server explicitly grant cross origin access where it is intentional.
- Dashify's CORS config uses an explicit allowlist of origins, never the wildcard, and supports credentials only for named origins.
- In dev, Vite's proxy sidesteps CORS by making the front and back appear on the same origin.
- CORS is a browser mechanism, not a server side security boundary, non-browser callers ignore it.