The Problem: Everything Worked, Except on iPhones
I recently created a full-stack application and deployed it on Vercel and Render. The build was successful and the app worked as expected on desktop browsers and Android. However, on iPhones the application failed across Safari and other browsers. The failure was mainly observed when trying to authenticate into the system. The credentials entered during registration were added to the database but the client was unable to enter the system.
Understanding the architecture of the app
The application followed a typical client-server architecture, with the frontend and backend deployed independently.
The frontend was hosted on Vercel, while the backend was deployed on Render.
When a user interacted with the application using their browser, the browser requests the frontend application from Vercel and executes the Javascript. When an API call was needed, the browser sent an HTTP request to the backend using its publicly accessible Render URL. The backend processed these requests, interacted with the database when necessary, and returned responses to the browser.
The Investigation: What Was Actually Going Wrong?
The first intuition was to add in logs for execution tracing. This helped me identify the following issue:
Auth debug: {
origin: 'https://ai-job-tracker-interview-assistant.vercel.app',
cookieReceived: false,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.7 Mobile/15E148 Safari/604.1'
}
I found that the backend did receive the request routed via an iPhone client but did not receive the authentication cookie. This helped me narrow down the problem to cookie transmission rather than JWT generation.
Although CORS was configured to allow credentialed requests, cookie transmission was still subject to browser cookie policies and the cookie's own attributes. This made it important to distinguish between permission to make a cross-origin request and permission to send cookies with that request.
Since the frontend and backend were hosted on different registrable domains, the authentication cookie was being used in a cross-site context. This led me to investigate Safari's Intelligent Tracking Prevention (ITP), which applies restrictions to cross-site tracking and third-party cookies. I suspected that browser privacy restrictions were interfering with the transmission of the authentication cookie.
The Solution: Setting Up Vercel as a Reverse Proxy
I introduced Vercel as a reverse proxy for API requests which now dealt with both serving frontend and also handling API routing through reverse proxy rewrites which can be configured in next.config.mjs in your frontend directory:
async rewrites() {
if (!process.env.BACKEND_URL) return [];
return [
{ source: "/api/:path*", destination: ${process.env.BACKEND_URL}/:path* },
];
}
This configured Vercel to forward all requests under '/api'* to backend and changed the frontend API base URL to '/api'.
So a /api/users request on the frontend gets forwarded to BACKEND_URL/users on Render. (BACKEND_URL must be configured in Vercel's environment variables for the relevant deployment environment)
The /api prefix is stripped by your destination pattern.
Why did this solve the issue?
This setup ensures that the browser communicates with a single origin throughout the API authentication flow, even though the frontend and backend are hosted by two different providers.
The browser now sends API requests to Vercel, which forwards them to the Render backend. This changes the browser-facing API communication from cross-origin to same-origin, while the communication between Vercel and Render happens server-side.
As a result, the browser no longer needs to transmit authentication cookies directly to a third-party backend domain.
Verification: Did the Fix Actually Work?
Upon making the above changes, the backend started receiving the authentication cookie thus fixing the cookie transmission issue.
Auth debug: {
origin: 'https://ai-job-tracker-interview-assistant.vercel.app',
cookieReceived: true,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_7 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.7 Mobile/15E148 Safari/604.1'
}
However I went further to check whether the requests were being proxied through Vercel by inspecting the network tab which verified that the browser requests /api/... on the Vercel origin
What I learned:
The issue taught me a few things that go beyond simply getting the application to work.
1. Not every frontend issue originates in the frontend.
Initially, the problem appeared to be specific to iPhones. However, debugging it required looking beyond the frontend code and understanding how the browser communicated with the backend. It reinforced the importance of investigating the entire request lifecycle rather than immediately assuming the problem lies in the component or application logic.
2. Understanding browser security policies matters.
Working through this issue gave me a better understanding of the same-origin policy, CORS, and how browsers handle cross-origin requests. I also learned how introducing a reverse proxy can change the way the browser communicates with a separately hosted backend.
3. Deployment architecture is part of application development.
Having the frontend and backend deployed independently introduces considerations that might not be apparent during local development. Understanding how requests travel between deployed services is just as important as understanding how the frontend and backend work individually.
4. Configuring CORS and cookies to allow cross-site transmission does not guarantee the flow to work.
Studying about security features on different devices, I learnt that the modern browsers enforce security and privacy policies that go beyond CORS. These policies may intervene with the traditional mechanisms to protect privacy of their users. Thus having an understanding of such policies will help us in making applications which work well across different devices.
5. Debugging is as much about eliminating possibilities as finding solutions.
Rather than jumping straight to a fix, I had to investigate the behavior across different devices and browsers, examine network requests, and narrow down the underlying cause. This reinforced the value of systematic debugging over trial-and-error configuration changes.
Most importantly, I learned to look at a full-stack application as a system of interconnected components rather than treating the frontend, backend, and deployment infrastructure as isolated pieces.
Few alternative solutions I considered:
1. Using a custom domain.
This would place both services under the same registrable domain. Although they would remain different origins and still require appropriate CORS configuration, they would generally be considered same-site for cookie purposes (assuming HTTPS is used for both).
This could help avoid certain third-party cookie restrictions associated with cross-site requests.
Why I did not choose this:
- Doesn't provide the same-origin API communication achieved by the Vercel rewrite approach.
- Requires careful cookie configuration.
2. Using an API gateway.
This sits between client and backend services and handles request routing. It provides centralized request routing and is considered useful especially when the application grows to support multiple backend services.
Why I did not choose this:
- Additional infrastructure and configuration
- Potentially unnecessary for a small application
3. Why not just disable browser security?
Disabling browser security features or relying on users to change browser settings is not a viable solution.
The application should adapt its architecture to browser security policies rather than asking users to weaken their browser's privacy protections.
Why I chose Vercel rewrites over them:
Since my frontend was already deployed on Vercel, this approach required minimal infrastructure changes and allowed me to retain Render as my backend hosting provider.
It also avoided introducing an additional API gateway or maintaining a separate proxy server.
For the scale and requirements of my application, this was a practical solution that addressed the issue without unnecessarily complicating the deployment architecture.
Conclusion
What started as an unexpected iPhone compatibility issue turned into a valuable learning experience in debugging, browser networking, and deployment architecture.
Implementing a reverse proxy through Vercel resolved the issue by changing how API requests were routed between the frontend and backend.
More than the fix itself, this experience helped me understand the importance of looking beyond application code when debugging production issues. Sometimes, the problem isn't in what your application does, but in how its different components communicate.
I'm continuing to explore these aspects of full-stack development and document the problems I encounter and the solutions I implement along the way.
Thanks for reading!


Top comments (0)