Project: Capacitor Mobile

project-capacitor-mobile Project Page

active project-page

Project: Capacitor Mobile

Canonical arch: labels for board items. Per convention-architecture-ids, these map to architecture diagram nodes and are used on board items for traceability.
Label Component Description
<code>arch:cap-core</code> Capacitor Core cap init, cap sync, capacitor.config.ts — the bridge between SvelteKit and native
<code>arch:ios-shell</code> iOS Native Shell Xcode project, Info.plist, WKWebView, AppDelegate.swift
<code>arch:signing</code> Code Signing Fastlane match, certificates, provisioning profiles (dev + distribution)
<code>arch:mac-agent</code> Mac CI Agent Woodpecker agent on MacBook M1, platform=darwin label routing
<code>arch:ios-pipeline</code> iOS Build Pipeline .woodpecker/ios.yml — npm ci → build → cap sync → xcodebuild → fastlane pilot
<code>arch:testflight</code> TestFlight Beta distribution via Fastlane pilot, iteration loop with Lucas
<code>arch:appstore</code> App Store Connect Metadata, assets, review submission, Fastlane deliver
<code>arch:keycloak-ios</code> Keycloak iOS Auth capacitor://localhost redirect URI, platform detection, silent SSO handling

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
<code>cap-setup</code> Dev agent I want to add Capacitor to an existing SvelteKit SPA <code>npx cap add ios</code> works, Xcode project generated, platform detection in keycloak.js
<code>cap-build</code> Dev agent I want CI to build an iOS binary and upload to TestFlight automatically Push to Forgejo → Mac agent → xcodebuild → Fastlane → TestFlight
<code>cap-appstore</code> Lucas I want to submit an app to the App Store with correct metadata and pass review App approved, live on App Store
<code>iterate</code> Lucas 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
<code>promote</code> Dev agent I want to scaffold a playground prototype as a SvelteKit + Capacitor app HTML/CSS from playground used directly as SvelteKit templates + app.css
<code>deploy</code> Dev agent I want to deploy the SvelteKit app to the cluster with full GitOps pal-e-services onboarding + pal-e-deployments overlay + ArgoCD syncing
<code>cap-camera</code> App user I want to use the native camera from within the app @capacitor/camera opens iOS camera, returns photo, web fallback works
<code>cap-gps</code> App user I want the app to detect my location automatically @capacitor/geolocation returns lat/lng, permission prompt handled
<code>cap-push</code> App user I want to receive push notifications @capacitor/push-notifications delivers to lock screen
<code>analytics</code> Lucas I want to see how many people use my app and what they do Umami (web) + App Store Connect (iOS) + custom event tracking
<code>dual-channel</code> App 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
<code>monetize</code> Lucas 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:X labels 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 SPA Configuration

Full convention documented in convention-sveltekit-spa. Summary of SPA requirements for Capacitor apps:
Setting Value Why
Adapter <code>@sveltejs/adapter-static</code> with <code>fallback: 'index.html'</code> SPA mode — all routes resolve to index.html, SvelteKit router handles navigation client-side
SSR <code>false</code> No server-side rendering — everything runs in browser. Required for Capacitor.
bundleStrategy <code>'single'</code> (recommended) Capacitor local server uses HTTP/1 — single bundle avoids waterfall
Auth <code>keycloak-js</code> + PKCE (public client) No server to store secrets. In-memory tokens only. See <code>convention-sveltekit-spa</code> for full pattern.
Data fetching Client-side <code>fetch()</code> with Bearer token No <code>+page.server.ts</code>. API wrapper module (<code>src/lib/api.js</code>) with <code>keycloak.updateToken(30)</code> before every call.
CSS Global <code>app.css</code>, no Tailwind Per <code>convention-frontend-css</code>. Design tokens as CSS custom properties.
Dockerfile <code>nginx:alpine</code> serving static <code>build/</code> <code>try_files $uri $uri/ /index.html</code> for SPA fallback. Cache headers on hashed assets.
Env vars <code>VITE_KEYCLOAK_URL</code>, <code>VITE_KEYCLOAK_REALM</code>, <code>VITE_KEYCLOAK_CLIENT_ID</code>, <code>VITE_API_URL</code> <code>VITE_</code> 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:5173 in Keycloak redirect URIs ✓ (already added), (2) http://localhost in API CORS origins ✓ (already added), (3) API accessible at https://mcd-tracker.tail5b443a.ts.net (already deployed).
Capacitor requires SvelteKit to run as a Single Page Application (no SSR). Configuration:
  • adapter-static instead of adapter-node
  • ssr: false in svelte.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 with adapter-static. Decision: use keycloak-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 <code>@capacitor/camera</code> AVFoundation <code>&lt;input type="file" capture&gt;</code> Yes — receipt photos
Geolocation <code>@capacitor/geolocation</code> CLLocationManager <code>navigator.geolocation</code> Yes — auto-detect McDonald's
Push Notifications <code>@capacitor/push-notifications</code> APNs n/a Future — slot reopen alerts
Clipboard <code>@capacitor/clipboard</code> UIPasteboard <code>navigator.clipboard</code> Yes — copy survey code
Haptics <code>@capacitor/haptics</code> UIImpactFeedbackGenerator n/a Nice-to-have
Filesystem <code>@capacitor/filesystem</code> Native file system IndexedDB Maybe — offline receipt cache
App <code>@capacitor/app</code> UIApplication lifecycle n/a Yes — deep links, state restoration

iOS Build Pipeline

App 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.plist permission 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 /events tracks 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 <code>mcd-tracker-app</code> Approved — CSS consolidated, @-comment specs Merged (PR #5, #6) 5173
westside <code>westside-app</code> Approved — 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 unchanged
  • convention-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
<em>empty</em>