Every API call in the app fails. Every curl against production works. That gap is the clue. Your app is calling itself.
Your website works. You wrap it in Capacitor and every API call in the app fails. Login, data, uploads, all of it.
If the app is in App Review, it gets rejected again and again for "cannot sign in". Meanwhile every curl you run against the production login works perfectly.
When a Capacitor app bundles its web files, those pages are not served from your domain anymore. They are served from a local origin inside the phone, such as capacitor://localhost or https://localhost.
So a relative path like /api/login or /.netlify/functions/x resolves to the phone itself. There is nothing there. The call fails.
That is why curling the backend proves nothing. The backend is fine. The app never reaches it.
There are two good fixes. Pick one on purpose. If the app's content is already a live website, Option A is usually simpler. If you need the app to ship fixed, reviewed files, use Option B. Either way, write down which mode each app uses so nobody expects a web deploy to fix a bundled app.
Point the app at your production site with server.url in capacitor.config.json. Relative paths then resolve to your real domain:
{
"server": {
"url": "https://yourdomain.com",
"allowNavigation": ["yourdomain.com"]
}
}
Keep allowNavigation to your own host only. Hosts listed there stay inside the webview with no back button, so phone links, WhatsApp, maps and other sites should open in the OS instead.
A bonus: web deploys reach installed apps instantly, with no new binary. The cost: Apple can reject thin wrappers under Guideline 4.2, so add real native value such as push, camera and splash.
Step 1. Add a small fetch shim that rewrites root relative API paths to your production origin, only inside the native app:
(function () {
var C = window.Capacitor;
if (!(C && C.isNativePlatform && C.isNativePlatform())) return;
var API = 'https://yourdomain.com';
var origFetch = window.fetch;
window.fetch = function (input, init) {
if (typeof input === 'string' && input.charAt(0) === '/') {
input = API + input;
}
return origFetch.call(this, input, init);
};
})();
Load it before any other script makes a request.
Step 2. Allow the app's origin in your CORS rules. Your backend now gets requests from the app origin, so it must answer them:
const ALLOWED = [
'https://yourdomain.com',
'capacitor://localhost',
'https://localhost'
];
const origin = event.headers.origin;
const headers = ALLOWED.includes(origin)
? { 'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Headers': 'Content-Type, Authorization' }
: {};
Step 3. Remember that with bundled files, every JavaScript fix needs a new binary. A fix that is live on the website will not reach people using the installed app until you ship one.
Test the bundled app from the app's own origin. Serve the bundled web folder on http://localhost and drive the real login form with a browser automation tool such as puppeteer. If login works there, it will work in the app. Curling the backend does not count.
server.url is in the wrong mode.viewport-fit=cover, add env(safe-area-inset-*) padding to full screen containers.Every entry follows the same pattern: the symptom you see, the real cause, the fix that worked, and the rule that stops it coming back. App Store, Google Play, Capacitor, Netlify, Supabase, Stripe, n8n and more.
Get Field NotesBuilding a lot? The Builder Bundle has Field Notes plus the launch playbook, the prompt book and the Claude Code skills kit. See the bundle.
Google will not sign anyone in inside an embedded webview.
The upload worked.
You added one private key and now nothing deploys.
What broke and how we fixed it, across app stores, hosting, databases and payments.