Research report accompanying the feature request in AmpersandTarski/Ampersand.
Date: 2026-08-13. Codebase facts verified against AmpersandTarski/prototype-framework at commit 0438d0a4 (v2.6.0 line, Angular 17 frontend, PHP 8.3 backend). External facts carry source links; the state of the art is as of mid-2026.
What does it take to deploy the frontend of an Ampersand-generated application on an iPhone and an Android phone as an app, with the app talking over the public internet to the backend API?
Today the prototype framework produces one artifact: a Docker image in which Apache serves the Angular bundle and the PHP API from the same document root, on the same origin. This report examines (a) which assumptions in the current framework tie the frontend to that single origin, (b) which packaging routes exist for putting the Angular frontend on a phone, and (c) which changes each route requires in the framework and in the deployment of the backend.
The build pipeline copies the Angular build output and the PHP entry point into one directory (generate.sh):
rm -rf html/*
cp -r backend/public/ html/
cp -r frontend/dist/prototype-frontend/* html/One Apache serves both, with an SPA rewrite fallback to index.html. Every coupling mechanism below silently relies on that shared origin. A mobile app breaks each of them, so they are worth listing precisely.
flowchart LR
subgraph today["Today: one container, one origin"]
B[Browser] -->|"GET /"| A[Apache]
A --> SPA["Angular bundle (html/)"]
B -->|"api/v1/… + PHPSESSID cookie"| A
A --> PHP["PHP API (html/api/v1)"]
end
subgraph target["Target: app on the phone, API on the internet"]
APP["App (WebView/PWA)<br/>origin: capacitor://localhost"] -->|"https://api.example.org/api/v1/…<br/>CORS + token/cookie"| API["Backend API<br/>(container, public HTTPS)"]
end
today ~~~ target
The frontend has no notion of a backend address. The environment files contain only a production flag (frontend/src/environments/environment.ts); there is no apiUrl anywhere. A global interceptor prepends a relative prefix to every request (frontend/src/app/backend/http-interceptors/backend-interceptor.ts:16-22):
const apiReq = req.clone({ url: `api/v1/${req.url}` });which the browser resolves against <base href="/"> — i.e. against whatever origin served the SPA. All services, including the generated backend.service.ts (resource/SESSION/1/<Ifc>), pass bare paths. The backend does know its own URL (global.serverURL, env var AMPERSAND_SERVER_URL, in backend/src/Ampersand/Misc/defaultSettings.yaml), but that value never reaches the frontend.
Consequence: a packaged app, which is served from capacitor://localhost (iOS) or https://localhost (Android) rather than from the backend host, has no way to find its API. A configurable base URL (build-time token or runtime config JSON) is a precondition for every route in §3.
The only authentication mechanism is the PHPSESSID cookie. Facts from backend/bootstrap/framework.php:63-74:
session.cookie_httponly = 1;cookie_secureonly when the request already came in over HTTPS.session.cookie_samesiteis never set — anywhere in the repo. Modern browsers and WKWebView then apply Lax by default, which means the cookie is not sent on cross-site requests.session.use_strict_mode = 0(deliberate, for multi-container deployments: the client may propose a session ID).
The frontend nowhere sets withCredentials: true, uses no Authorization header, no tokens, no CSRF token (verified by grep over frontend/src). Login requires the SIAM extension module (session.loginEnabled, default false) and runs as a plain PATCH on an Ampersand interface; there is no OAuth/OIDC/JWT code in the repository. Since v2.5.1 a SessionBootstrapInterceptor serializes the very first API call precisely because the whole session model hangs on the cookie jar of the browser.
Consequence: from a phone app the session cookie is a cross-site cookie. It is only sent at all with SameSite=None; Secure, and WKWebView (iOS) treats third-party cookies more strictly than Safari and is documented to drop or not persist them (capacitor#1373). Cookie-based sessions are the single largest obstacle; §4.3 lists the options.
Exactly one CORS header exists in the codebase: Access-Control-Allow-Origin: * on the OpenAPI spec endpoint, which is disabled in production (backend/src/Ampersand/Controller/OpenApiController.php). Nothing else: no CORS middleware, no OPTIONS/preflight route (the Slim router registers none), no Header set in the Apache configs. A cross-origin PATCH resource/... with credentials would die in the preflight before reaching PHP.
The compiler generates per-interface components, the routing table and a typed backend.service.ts into frontend/src/app/generated/, and assets/interfaces.json is copied out of backend/generics/ into the bundle at build time (frontend/angular.json:24-28). The backend verifies a model checksum on every request and warns when the generated model changed (VerifyChecksumMiddleware).
Consequence: a mobile app is a per-Ampersand-application artifact, exactly like today's Docker image. The framework can deliver the tooling and templates, but each Ampersand project ships its own app to the stores. Moreover, a binary in a user's pocket can lag behind a redeployed backend — the checksum mechanism will flag the drift, and an update strategy is needed (§4.5).
Smaller items that assume a full browser on the same origin (all verified in the code):
- Population export builds a
data:URI<a download>and clicks it (population.service.ts:21-39) — inert inside a native WebView; needs a Filesystem/Share plugin. - Recovery paths use
window.location.assign('/')andwindow.location.reload()(error and logging interceptors); menu/role state lives insessionStorage. - File upload posts
FormDatatoadmin/import; file serving streams fromGET /api/v1/file/{path}— both fine cross-origin once CORS works, but the download UX needs native handling. - Deep-link refresh currently works because of the Apache SPA rewrite; in a packaged app the router/shell must take that over.
- No service worker, no web manifest, no
@angular/service-worker: the PWA groundwork is absent but greenfield. - No WebSocket/SSE anywhere — all traffic is request/response HTTP. That is good news: no long-lived-connection complications.
Add @angular/pwa (service worker + web manifest + icons) and serve the prototype over HTTPS; users install from the browser ("Add to Home Screen" on iOS, real install prompt on Android). No app store, no signing, no per-platform build.
Status 2026: Android/Chrome support is complete, including push. On iOS, home-screen web apps run on WebKit (Apple's February 2024 EU removal was reversed on 1 March 2024; Apple DMA page). Web Push works on iOS 16.4+ only for installed home-screen apps (WebKit blog); iOS 18.4 added Declarative Web Push (WebKit). Safari's 7-day storage eviction does not apply to installed home-screen apps (Apple forums).
Key property for Ampersand: if the PWA is served by the prototype itself, the same-origin architecture survives intact — no CORS, no cookie surgery, no base-URL work. The framework changes reduce to manifest + service worker + HTTPS guidance. Limitations: no store presence, iOS push only after home-screen install, no biometrics/deep native integration.
Capacitor wraps the unmodified Angular build output (webDir → dist/) in a native iOS/Android project with a plugin bridge. Current major: Capacitor 8 (Dec 2025, announcement); MIT-licensed; maintained by the Ionic team under OutSystems, which committed to the open-source stack when sunsetting Ionic's commercial products in Feb 2025 (announcement). Cordova plugins mostly still run on it; Angular 17 with the webpack builder drops in without rewrite.
The webview serves the bundled app from a synthetic origin — capacitor://localhost on iOS, https://localhost on Android (config reference) — so every API call is cross-origin. The framework-side work is §4.1–4.3 in full. For the cookie problem Capacitor offers escape hatches: the CapacitorHttp plugin routes fetch/XHR through the native HTTP stack, bypassing webview CORS and cookie policy (docs), and CapacitorCookies patches document.cookie to the native store (docs). Pointing server.url at the remote prototype (making the app same-origin again) is explicitly marked not intended for production in the Capacitor docs, and produces exactly the "repackaged website" profile the stores reject.
Store acceptance is a real gate, not a formality: Apple guideline 4.2 Minimum Functionality rejects apps that don't rise above a repackaged website, and Google Play's webview/spam policy does the same. The accepted pattern is: bundle the web assets in the binary (no remote loading) and add native value — push notifications (APNs/FCM via @capacitor/push-notifications), deep links (Universal Links / App Links with verification files served by the backend), biometric login, offline behavior.
Remote update of the web assets (to soften the model-drift problem of §2.4) is established practice within Apple's interpreted-code carve-out (guideline 2.5.2 / DPLA 3.3.1(B)); tooling: Capgo or Capawesome Cloud — Ionic Appflow sunsets 31 Dec 2027.
A TWA (Bubblewrap/PWABuilder) packages the PWA of route A for the Play Store, running in full Chrome with Digital Asset Links verification. Cheap add-on once route A exists; no iOS equivalent, and the same Play minimum-functionality bar applies.
- Cordova: not formally dead, but plugin ecosystem archiving is underway (deprecation policy), the commercial ecosystem has withdrawn, and Capacitor is its designated successor. No reason to start here in 2026.
- NativeScript / Flutter / native rewrite: NativeScript is alive (9.0, Nov 2025) but renders native UI — every generated Angular template and every BOX component would need a NativeScript counterpart. That discards the framework's largest asset, the generated+shared component library. A rewrite route only makes sense if the web frontend is abandoned, which contradicts the framework's purpose.
| A: PWA | B: Capacitor | C: TWA (Android) | |
|---|---|---|---|
| Store presence | no | App Store + Play | Play only |
| Framework changes needed | manifest + service worker + HTTPS | §4.1–4.6 (base URL, CORS, auth, packaging, updates) | those of A + Play packaging |
| Cookie/session problem | none (same origin) | central problem | none (real Chrome, real origin) |
| Push on iOS | after home-screen install (16.4+) | full (APNs) | n/a |
| Native APIs (camera, biometrics, files) | limited | full via plugins | limited |
| Per-project distribution cost | zero | signing, review, store fees, API-level treadmill | low |
| Model-drift handling | deploy = update (it's a website) | store release or OTA web-asset update | deploy = update |
The numbers indicate dependency order, not priority. W1–W3 are backend/frontend contract work that also benefits non-mobile deployments (e.g. serving the SPA from a CDN, or third-party API clients); W4–W7 are the mobile-specific layers.
W1 — Configurable API base URL (frontend). Introduce an injectable base URL: default '' (today's relative behavior, zero impact on existing deployments), overridable at build time or via a runtime config.json asset. Adapt BackendInterceptor and audit the handful of absolute-path assumptions (/assets/, window.location.assign('/'), Monaco's location.origin worker loader).
W2 — CORS middleware (backend). A Slim middleware answering OPTIONS preflights and emitting Access-Control-Allow-Origin (exact origin echo from an allow-list — * is invalid with credentials), Access-Control-Allow-Credentials: true, allowed methods/headers. Configurable in project.yaml (e.g. api.corsOrigins: [capacitor://localhost, https://localhost]), off by default. Roughly the shape of the existing middlewares (VerifyChecksumMiddleware et al.).
W3 — Cross-site authentication (backend + frontend). Three levels, increasing in effort and robustness:
- Cookie hardening: set
session.cookie_samesite = None+ forceSecurewhen CORS mode is on; frontend sendswithCredentials: true. Works for PWA-on-other-origin and partially for Capacitor, but stays hostage to WKWebView third-party-cookie policy and to future browser cookie tightening. - Native HTTP in the shell: enable CapacitorHttp so requests leave via the native stack with a native cookie jar; combine with 1. Confines the fix to the Capacitor route.
- Token-based session (recommended endpoint): accept the session ID / an opaque or JWT token via the
Authorizationheader as an alternative to the cookie.use_strict_mode=0means the backend already accepts client-proposed session IDs, so a header-based session carrier is a modest, backwards-compatible extension ofInitAmpersandAppMiddleware/Session. For real login flows, pair with SIAM/OIDC using Authorization Code + PKCE in the system browser per RFC 8252, tokens in Keychain/Keystore.
W4 — Packaging.
- PWA:
ng add @angular/pwa, manifest + icons (per project: name/icons come from the Ampersand model or project config), service-worker caching policy (careful:interfaces.jsonand the API must not be cached stale), documentation for HTTPS deployment. - Capacitor: a template/scaffold (analogous to
.templates/) that per project generatescapacitor.config.ts(appId, appName,webDir: dist/prototype-frontend, CapacitorHttp on), plus theios//android/project generation steps in or next togenerate.sh.
W5 — Model-version and update strategy. Define what happens when the backend redeploys with a changed model while binaries are in the field: minimally a friendly "app update required" screen keyed off the existing checksum mechanism; optionally OTA web-asset updates (Capgo/Capawesome) so that model changes that don't need new native plugins bypass store review.
W6 — Replace browser-only idioms behind an abstraction. Download (population export, FILEOBJECT views) via a platform service: DOM anchor on web, Filesystem/Share plugin in the shell. Same for "reload app" recovery paths.
W7 — Distribution pipeline and store compliance (documentation + CI templates). Apple Developer Program USD 99/yr (organizations need D-U-N-S), Play one-time USD 25; personal Play accounts need a 12-tester/14-day closed test before production (policy); Play target-API treadmill (API 36 by 31 Aug 2026, policy); ATS requires proper public HTTPS with valid certificates (Apple). CI: GitHub Actions + fastlane is the standard shape. Each Ampersand project owns its store listing; the framework supplies the pipeline template and the guideline-4.2 checklist (push, deep links, offline shell as the native value-add).
Phase the work; each phase is independently useful.
- Phase 1 — PWA (route A). Smallest change set (W4-PWA plus HTTPS docs; W1–W3 not required when the prototype serves its own PWA), immediately gives "app on the home screen" on both platforms, and improves the ordinary web experience (offline shell, faster loads). This phase alone answers the original question for many use cases.
- Phase 2 — decouple the origin (W1–W3). Base URL, CORS,
SameSite/token auth. This is the structural investment; it also unlocks CDN hosting of the SPA and third-party API clients, independent of mobile. - Phase 3 — Capacitor shell (route B: W4–W7). For projects that need store presence, push notifications, biometrics or native file handling. Enter it knowing the store gates: bundled assets, native value-add for guideline 4.2 / Play policy, signing and review cycles per project.
Cookie-based sessions are the pivot: option W3.3 (Authorization-header session carrier) removes the WebView cookie fragility at its root and is small on the backend, so it deserves to be the default design for phase 2 rather than the cookie-hardening patch.
Codebase (commit 0438d0a4): generate.sh, frontend/src/app/backend/http-interceptors/*, frontend/src/environments/*, frontend/angular.json, frontend/package.json, backend/bootstrap/framework.php, backend/src/Ampersand/Misc/defaultSettings.yaml, backend/src/Ampersand/Session.php, backend/src/Ampersand/Controller/OpenApiController.php, backend/src/Ampersand/API/Middleware/*, docker/apache/000-default.conf, apache-conf/.htaccess, frontend/src/app/admin/population/population.service.ts, frontend/src/app/generated/*.
External: Capacitor 8 announcement · Capacitor support policy · Capacitor config (origins, server.url) · CapacitorHttp · CapacitorCookies · Ionic CORS guide · Ionic commercial sunset · Cordova deprecation policy · WebKit: Web Push on iOS · WebKit: Declarative Web Push · Apple: DMA and apps in the EU · Apple App Review Guidelines (2.5.2, 4.2, 4.7) · Apple: ATS · Google Play webview policy · Play closed-testing requirement · Play target API level · RFC 8252 (OAuth for native apps) · Bubblewrap · Angular service workers · Capacitor deep links · Capacitor push notifications.