Capacitor Mobile
Notes
Board 1
-
Capacitor Mobile
board-capacitor-mobileCapacitor Mobile
Project Page 1
-
Project: Capacitor Mobile
project-capacitor-mobileProject: Capacitor Mobile
Canonical
arch:labels for board items. Perconvention-architecture-ids, these map to architecture diagram nodes and are used on board items for traceability.Label Component Description arch:cap-coreCapacitor Core cap init, cap sync, capacitor.config.ts — the bridge between SvelteKit and native arch:ios-shelliOS Native Shell Xcode project, Info.plist, WKWebView, AppDelegate.swift arch:signingCode Signing Fastlane match, certificates, provisioning profiles (dev + distribution) arch:mac-agentMac CI Agent Woodpecker agent on MacBook M1, platform=darwin label routing arch:ios-pipelineiOS Build Pipeline .woodpecker/ios.yml — npm ci → build → cap sync → xcodebuild → fastlane pilot arch:testflightTestFlight Beta distribution via Fastlane pilot, iteration loop with Lucas arch:appstoreApp Store Connect Metadata, assets, review submission, Fastlane deliver arch:keycloak-iosKeycloak iOS Auth capacitor://localhost redirect URI, platform detection, silent SSO handling The production readiness practice for mobile apps. Take a proven playground design (HTML/CSS, zero infra) and walk it through every step to the App Store: SvelteKit scaffold, Capacitor native plugins, cluster deployment, Mac CI pipeline, TestFlight iteration, and App Store submission.
At the playground stage, an app has nothing: no SvelteKit repo, no k8s namespace, no CI, no ArgoCD, no Capacitor. Just HTML/CSS files served by nginx. This project owns the entire transformation — every piece of infrastructure, every configuration, every Apple requirement — so that individual app projects follow a proven path instead of figuring it out from scratch.
Three practice projects, one pipeline:
- Frontend Playground — proves the look (seconds: save → refresh)
- Capacitor Mobile — proves the functionality and ships to production (minutes to days: build → deploy → TestFlight → App Store)
- pal-e-platform — provides the infrastructure (cluster, CI, GitOps, observability)
The native mobile development practice. Bridge proven web design (from the playground) to native mobile functionality via Capacitor. Own the complexity of iOS builds, native plugins, App Store submission, and the TestFlight iteration loop — so that individual app projects (mcd-tracker, future apps) don't have to.
Relationship to Frontend Playground: Playground proves the look. This project proves the functionality. Playground iterates in seconds (save → refresh). This project iterates in minutes (build → sign → TestFlight → install on phone). Two cadences, two practices, complementary.
User Stories
Key Role Story Success Metric cap-setupDev agent I want to add Capacitor to an existing SvelteKit SPA npx cap add iosworks, Xcode project generated, platform detection in keycloak.jscap-buildDev agent I want CI to build an iOS binary and upload to TestFlight automatically Push to Forgejo → Mac agent → xcodebuild → Fastlane → TestFlight cap-appstoreLucas I want to submit an app to the App Store with correct metadata and pass review App approved, live on App Store iterateLucas I want to test the app on my phone via TestFlight and iterate on iOS-specific issues App installs on iPhone, full user flow works, Lucas approves promoteDev agent I want to scaffold a playground prototype as a SvelteKit + Capacitor app HTML/CSS from playground used directly as SvelteKit templates + app.css deployDev agent I want to deploy the SvelteKit app to the cluster with full GitOps pal-e-services onboarding + pal-e-deployments overlay + ArgoCD syncing cap-cameraApp user I want to use the native camera from within the app @capacitor/camera opens iOS camera, returns photo, web fallback works cap-gpsApp user I want the app to detect my location automatically @capacitor/geolocation returns lat/lng, permission prompt handled cap-pushApp user I want to receive push notifications @capacitor/push-notifications delivers to lock screen analyticsLucas I want to see how many people use my app and what they do Umami (web) + App Store Connect (iOS) + custom event tracking dual-channelApp user I want to use the app on web OR iOS, whichever I prefer Same app at web URL and on App Store, analytics tracks both monetizeLucas I want to monetize when I have ~100 active users Stripe (web) + Apple IAP (iOS), RevenueCat unified Plan
Plans are obsolete per
convention-kanban-over-plans. The board IS the work decomposition tool. Existing plan references (plan-wkq phases 22-24, plan-pal-e-platform phases 28-30) are historical context only.Board
board-capacitor-mobile— Continuous kanban. Tracks the web-to-App-Store transformation pipeline. Cross-repo: items reference issues on consumer app repos (westside-app, mcd-tracker-app) and platform infra repos (pal-e-platform).consumer:Xlabels identify which project each item serves.Status
See
board-capacitor-mobile. The board is the source of truth for current state — not this paragraph. Key blocker: Apple Developer enrollment ($99,blocker:external). Infrastructure is ready (Mac agent connected, subnet router deployed, Salt managing config).Milestones
None yet. First milestone will be: westside-app running on Lucas's iPhone via TestFlight.
Architecture
Architecture Component IDs
How Capacitor Works
SvelteKit App (SPA mode, adapter-static) ↓ npx cap sync Capacitor iOS Shell (WKWebView) ├── Your SvelteKit app runs inside WKWebView ├── @capacitor/camera → native AVFoundation ├── @capacitor/geolocation → native CLLocationManager ├── @capacitor/push-notifications → native APNs └── @capacitor/filesystem → native file system Xcode Project (auto-generated by Capacitor) ├── ios/App/App/ │ ├── Info.plist (permissions, entitlements) │ ├── AppDelegate.swift (minimal, Capacitor-managed) │ └── public/ (your built SvelteKit SPA) └── ios/App.xcodeproj (opened by Xcode for signing + build)SvelteKit SPA Configuration
Full convention documented in
convention-sveltekit-spa. Summary of SPA requirements for Capacitor apps:Setting Value Why Adapter @sveltejs/adapter-staticwithfallback: 'index.html'SPA mode — all routes resolve to index.html, SvelteKit router handles navigation client-side SSR falseNo server-side rendering — everything runs in browser. Required for Capacitor. bundleStrategy 'single'(recommended)Capacitor local server uses HTTP/1 — single bundle avoids waterfall Auth keycloak-js+ PKCE (public client)No server to store secrets. In-memory tokens only. See convention-sveltekit-spafor full pattern.Data fetching Client-side fetch()with Bearer tokenNo +page.server.ts. API wrapper module (src/lib/api.js) withkeycloak.updateToken(30)before every call.CSS Global app.css, no TailwindPer convention-frontend-css. Design tokens as CSS custom properties.Dockerfile nginx:alpineserving staticbuild/try_files $uri $uri/ /index.htmlfor SPA fallback. Cache headers on hashed assets.Env vars VITE_KEYCLOAK_URL,VITE_KEYCLOAK_REALM,VITE_KEYCLOAK_CLIENT_ID,VITE_API_URLVITE_prefix required — Vite only exposes prefixed vars to client-side code.Local Development Workflow (CRITICAL — test before deploy)
Three iteration speeds. Never skip level 2.
Speed Tool What it catches Iteration time 1. Playground Save HTML → refresh browser Design, layout, UX flow. No auth, no API. Seconds 2. Local dev npm run dev → http://localhost:5173 Auth flow, CORS, API shape, data binding, error states. Real keycloak-js, real API. Seconds 3. Cluster deploy Push → CI → Harbor → ArgoCD Production infra: ports, image names, ingress, TLS. Only after level 2 validates. Minutes Lesson (2026-03-16): Phase 7 skipped level 2 entirely. Six production hotfixes in a row — wrong port, wrong registry, wrong realm, check-sso redirect, no CORS, client not public. Every single one would have been caught in seconds with
npm run dev. The rule: if it doesn't work on localhost, it doesn't get pushed to prod. Local dev requires: (1)http://localhost:5173in Keycloak redirect URIs ✓ (already added), (2)http://localhostin API CORS origins ✓ (already added), (3) API accessible athttps://mcd-tracker.tail5b443a.ts.net(already deployed).Capacitor requires SvelteKit to run as a Single Page Application (no SSR). Configuration:
adapter-staticinstead ofadapter-nodessr: falseinsvelte.config.js(or per-page)bundleStrategy: 'single'recommended (Capacitor local server uses HTTP/1)- API calls go to the remote backend URL (not relative paths — no server-side in SPA mode)
Authentication (keycloak-js — decided 2026-03-16)
SPA mode means no server-side auth. Auth.js (used by westside-app) requires
adapter-node+ SSR for server-side token exchange — incompatible withadapter-static. Decision: usekeycloak-js(official Keycloak JavaScript adapter) for client-side OIDC with PKCE. Rejected: Auth.js (requires SSR), oidc-spa (warns against SvelteKit), keycloak-ionic (deprecated), @byteowls/capacitor-oauth2 (native-only, would need separate web auth = two code paths), BFF pattern (adds server back, defeats SPA purpose).Concern Approach Keycloak client type Public client (Client authentication OFF — can't store secrets client-side) PKCE Enabled by default in keycloak-js (SHA256) Token storage In-memory only (never localStorage). Per Keycloak docs: "tokens should never be persisted to prevent hijacking attacks." Token refresh keycloak.updateToken(30) before API calls. On Capacitor appStateChange, refresh on resume. Silent check-SSO Hidden iframe on web. Full check on app resume for iOS. API calls Bearer token: fetch(url, { headers: { Authorization: `Bearer ${keycloak.token}` } }) Redirect URIs Web: https://mcd-tracker-app.tail5b443a.ts.net/* — iOS: capacitor://localhost/* Web Origins https://mcd-tracker-app.tail5b443a.ts.net, capacitor://localhost, http://localhost SvelteKit pattern Initialize in +layout.svelte (onMount). Auth guard blocks rendering until resolved. Data loading via client-side fetch(), not +page.server.ts. Platform detection Capacitor.isNativePlatform() sets redirectUri. Web: window.location.origin. iOS: capacitor://localhost/ Plugin Catalog
Plugin npm Package Native API (iOS) Web Fallback mcd-tracker uses Camera @capacitor/cameraAVFoundation <input type="file" capture>Yes — receipt photos Geolocation @capacitor/geolocationCLLocationManager navigator.geolocationYes — auto-detect McDonald's Push Notifications @capacitor/push-notificationsAPNs n/a Future — slot reopen alerts Clipboard @capacitor/clipboardUIPasteboard navigator.clipboardYes — copy survey code Haptics @capacitor/hapticsUIImpactFeedbackGenerator n/a Nice-to-have Filesystem @capacitor/filesystemNative file system IndexedDB Maybe — offline receipt cache App @capacitor/appUIApplication lifecycle n/a Yes — deep links, state restoration iOS Build Pipeline
Developer pushes code to Forgejo ↓ Woodpecker webhook Mac Woodpecker Agent (local backend, platform=darwin) ├── npm install ├── npm run build (SvelteKit → static) ├── npx cap sync (copy build to ios/App/public/) ├── xcodebuild archive ├── xcodebuild -exportArchive → .ipa ├── fastlane pilot upload → TestFlight └── (manual) promote TestFlight → App StoreApp Store Requirements
- Apple Developer Program ($99/yr)
- Signing certificates + provisioning profiles (Fastlane
match) - App icons (1024x1024 + all required sizes)
- Launch screen / splash screen
- Privacy policy URL
Info.plistpermission descriptions: camera usage, location usage- App Store Connect metadata: description, keywords, screenshots
- App review: usually 24-48 hours
Analytics & User Tracking
Self-hosted, privacy-respecting analytics. No Google Analytics, no third-party trackers. Three layers:
Layer Tool What it tracks Self-hosted When to add Web analytics Umami or Plausible Page views, referrers, devices, web vs iOS split Yes (k8s deployment) After App Store launch Product events Custom API endpoint (POST /events) App-specific actions: scanned receipt, redeemed code, slot checked Yes (same Postgres) When 10+ users App Store metrics App Store Connect (Apple) Downloads, impressions, conversion rate, crashes, ratings Apple-hosted (free) Automatic on publish Monetization Stripe (web) + Apple IAP (iOS) + RevenueCat (unified) Subscriptions, revenue, churn Stripe self-managed When ~100 active users Dual-Channel Distribution
Every Capacitor app ships to both web and iOS from one codebase. Standard for modern apps:
- Web — deployed via ArgoCD, accessible at public URL. SEO-indexable. No install required. Catches Android users + desktop users.
- iOS — Capacitor wraps SvelteKit SPA in WKWebView. Native camera/GPS. App Store distribution. Push notifications.
- Analytics must track both channels — Umami embeds in the SvelteKit app (works on both), App Store Connect covers iOS-specific metrics.
POST /eventstracks user actions regardless of channel.
Repos
This project doesn't own repos — it owns knowledge. Capacitor integration lives inside each consumer's app repo:
Consumer Repo Playground Docker Compose Dev Port mcd-tracker mcd-tracker-appApproved — CSS consolidated, @-comment specs Merged (PR #5, #6) 5173 westside westside-appApproved — playground complete Not yet created 5174 Key Conventions
sop-frontend-experiment— design in playground FIRST (locked before Capacitor touches it)convention-frontend-css— CSS carries through playground → SvelteKit → Capacitor unchangedconvention-sveltekit-spa— adapter-static, keycloak-js PKCE, client-side fetch, nginx static serving- SOP: Native Mobile Integration — TBD, created when Phase 7 begins
- SOP: App Store Submission — TBD, created when Phase 10 begins
Inbox
Slug Summary Discovered empty