Your app is a new origin
On your website, the page at https://example.com calling https://example.com/api is a same-origin request: no CORS, cookies flow freely. In the app, the same code runs at https://appassets.androidplatform.net/web/, so every request to your API is cross-origin, and the browser enforces three rules on it:
- The API must answer with an
Access-Control-Allow-Originheader that permits the app's origin, or the response is hidden from your code. - Requests with JSON bodies or custom headers are preceded by an
OPTIONSpreflight the API must also answer. - Cookies are only sent cross-site if they are marked
SameSite=None; Secureand the request opts in withcredentials: 'include'.
Public APIs that already send Access-Control-Allow-Origin: * (most weather, maps and open-data APIs; Supabase; Firebase's REST endpoints) work with no changes.
Allowing the origin
app.use(cors({
origin: ['https://example.com', 'https://appassets.androidplatform.net'],
allowedHeaders: ['Content-Type', 'Authorization'],
}))
const ALLOWED = new Set(['https://example.com', 'https://appassets.androidplatform.net'])
function cors(req, res) {
const o = req.headers.get('Origin')
if (ALLOWED.has(o)) {
res.headers.set('Access-Control-Allow-Origin', o)
res.headers.set('Access-Control-Allow-Headers', 'Content-Type, Authorization')
res.headers.set('Vary', 'Origin')
}
return res
}
'allowed_origins' => ['https://example.com', 'https://appassets.androidplatform.net'],
if ($http_origin = "https://appassets.androidplatform.net") {
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Headers "Content-Type, Authorization" always;
}
Every ZIP-built app shares this origin. Allowing it means any such app could call your API from a user's phone — the same as any website could with *. CORS is a browser courtesy, not access control; your API still needs real authentication.
Tokens beat cookies
Session cookies set by your API are third-party cookies from the app's point of view. The app accepts them, but most frameworks issue SameSite=Lax session cookies, which the browser will not send on cross-site fetch. Rather than loosening your cookie policy, authenticate the app with a bearer token:
const token = localStorage.getItem('token')
const res = await fetch('https://api.example.com/orders', {
headers: { Authorization: `Bearer ${token}` },
})
Store short-lived access tokens with a refresh flow; localStorage in the app is private to it and survives updates.
Sign-in methods that work
| Method | In the app |
|---|---|
| Email + password, your own backend | ✓ |
| Magic link / one-time code by email | ✓ (code entry is smoother than links, which open the mail app's browser) |
| Phone number + SMS code | ✓ (invisible reCAPTCHA needs the app's hostname allowed in your provider's settings) |
| Google sign-in (OAuth popup or redirect) | ✗ Google rejects OAuth requests from embedded WebViews (disallowed_useragent) |
| Facebook, Apple, GitHub OAuth | Unreliable — providers increasingly block or degrade WebView sign-in |
With Firebase Authentication, add appassets.androidplatform.net to Authorized domains and use email/password, email link or phone providers in the app build. If Google sign-in is essential, it needs a native integration rather than a web page.
HTTP, mixed content and certificates
The app is permissive about plain http:// APIs and mixed content, so a test server on your LAN works. Do not ship that way: traffic is readable on any shared Wi-Fi. Self-signed certificates are rejected — use a real certificate (Let's Encrypt, or a tunnel such as Cloudflare Tunnel for testing).
Secrets
Everything in the ZIP can be read by unzipping the APK. Publishable keys are fine. Anything that grants write access, billing or admin rights belongs on a server; the app calls that server with the user's token.