# Bundled Capacitor app — deploy & CORS notes How to make the bundled app SPA (`BUILD_TARGET=capacitor`, see the "Dual build target" recipe in `recipes.md`) actually work against production. The app runs on origin `https://localhost` and calls `https://app.logship.de` cross-origin with token auth (`Authorization: Bearer` + `X-Logship-*` headers, marker `X-Logship-App: 1`). ## 1. Deploy the Nuxt app — that's the whole "server deploy" The new server code is part of the normal Nitro build, so a standard deploy of the Nuxt app to `:3001` ships it. No special step. - `server/middleware/00.app-auth.ts` — emits CORS for the app origin, answers the `OPTIONS` preflight, and maps the app's Bearer token + `X-Logship-*` headers into the cookies the existing routes read. No-op for web traffic. - `server/api/app-auth/{login,token-refresh}.post.ts` — the app's auth endpoints. ## 2. nginx — NO CHANGE NEEDED ✅ Verified against the current production directive: - `location /` → `proxy_pass http://192.168.12.72:3001` catches every `/api/app-auth/*`, `/api/mobile/*`, `/api/materials/*`, … call. It forwards `OPTIONS` and all request headers (our headers are hyphenated — `X-Logship-App`, `X-Logship-Organization-Id` — so nginx does NOT drop them; no `underscores_in_headers` needed), does not short-circuit `OPTIONS`, and adds no CORS of its own. So the in-app middleware fully handles CORS + preflight. **Leave it as is.** - `location /api/v1` → iDempiere `:8443` is unaffected — none of our Nuxt routes use `/api/v1`. - `client_max_body_size 100M` (global) covers image/voice uploads; `large_client_header_buffers 4 32k` covers the base64 `X-Logship-User` header. Both already fine. Do **not** add `add_header Access-Control-*` or an `if ($request_method = OPTIONS) { return 204; }` to nginx — that would double the CORS headers (browser rejects) or answer the preflight WITHOUT the CORS headers. Let Nitro handle it. ## 3. Strapi — the ONE change you need: allow the app origin Image uploads (product galleries, RMA/receipt attachments in return/receive/product/movement) post to `…/media-api/upload`, which your `location /media-api` block proxies to Strapi (`:1337/api`). nginx forwards the preflight, but **Strapi's own CORS must allow `https://localhost`**. On the Strapi server (`192.168.12.72:1337`), edit `config/middlewares.{js,ts}` — keep the rest of the default array, only adjust the `strapi::cors` entry: ```js { name: 'strapi::cors', config: { origin: [ 'https://app.logship.de', // existing web 'https://localhost', // Capacitor Android (androidScheme: 'https') 'capacitor://localhost', // older Capacitor / iOS scheme 'http://localhost', // dev fallback ], methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'], headers: ['Content-Type', 'Authorization', 'Origin', 'Accept'], keepHeaderOnError: true, }, }, ``` Restart Strapi after the change. (If `config.public.strapi` / `STRAPIPUBLIC` points directly at the Strapi host rather than `https://app.logship.de`, the same Strapi CORS change still applies; only the URL the app hits differs.) ## 4. Verify (run from a machine that can reach app.logship.de) ```bash # Nuxt API preflight — expect: 204 + "Access-Control-Allow-Origin: https://localhost" curl -i -X OPTIONS https://app.logship.de/api/app-auth/login \ -H 'Origin: https://localhost' \ -H 'Access-Control-Request-Method: POST' \ -H 'Access-Control-Request-Headers: authorization,content-type,x-logship-app' # Strapi upload preflight (after the Strapi CORS change) — expect: 2xx + ACAO: https://localhost curl -i -X OPTIONS https://app.logship.de/media-api/upload \ -H 'Origin: https://localhost' \ -H 'Access-Control-Request-Method: POST' \ -H 'Access-Control-Request-Headers: authorization,content-type' # A real login (server-deployed) — expect a body with token + refresh_token curl -i -X POST https://app.logship.de/api/app-auth/login \ -H 'Origin: https://localhost' -H 'X-Logship-App: 1' -H 'Content-Type: application/json' \ -d '{"userName":"","password":"
"}' ``` If `app-auth/login` returns a `token` but no `refresh_token`, the inner `Set-Cookie` did not surface through Nitro's internal `$fetch.raw` (see the recipe gotcha) — refresh then falls back to re-login. ## 5. Ship the APK One command does the whole release (bump → build:app → cap:sync:app → assembleDebug → stage → upload): ```bash scripts/release-app.sh # auto-bumps the patch (e.g. 1.0.2 -> 1.0.3) scripts/release-app.sh 1.1.0 # explicit versionName scripts/release-app.sh --no-upload # build + stage only ``` It bumps `android/app/build.gradle` + `android/update-files/version.json` together (versionName must strictly increase or the in-app updater won't offer it), builds the bundled APK, stages it into `android/update-files/logship-mobile.apk`, and hands off to `scripts/upload-mobile-update.sh`. Remember to commit the version bump afterwards.