SOP: Capacitor Mobile Lifecycle
SOP: Capacitor Mobile Lifecycle
Standard operating procedure for the playground → Capacitor app → production pipeline. Every mobile project follows this lifecycle. No exceptions.
Pipeline Overview
Rule: Never skip gates. Never parallelize across gates. Parallel work within a gate is fine.
Stage 1: Playground (Design + Spec)
Input Contract
A playground repo ready for promotion has EXACTLY:
app.css— ONE CSS file (entire design system)app.js— ONE JS file (shared interactions, if needed)*.html— HTML files referencing only app.css and app.js- Zero inline
<style>blocks - Zero inline
<script>blocks (except minimal DOM wiring) - Every HTML file has an @-comment spec header (see format below)
@-Comment Spec Format
Every playground HTML file MUST have this comment block after
<body>:
Complexity Scale
- low — fetch + render, no writes (landing, history list, auth redirects)
- medium — form submit + navigation, single API write (save, filter/sort)
- high — multi-step state, optimistic updates, device APIs (scan flow, redeem)
Why @-prefix
Greppable.
grep '@gaps' *.html shows every page with backend gaps. grep '@complexity high' *.html shows hard pages. grep '@api' *.html shows all API surface area.Gate 1: Phone Approval
Lucas reviews every page on phone. No promotion until approved. Design is taste-driven, not rules-driven.
Stage 2: Promotion to Capacitor App
Mechanical Copy
app.css→src/app.css(literal copy)- Each
*.htmlbody →+page.sveltetemplate (copy + data bindings) - @-comment specs guide the Svelte implementation (API calls, state, interactivity)
Rules
- NO scoped Svelte
<style>blocks — all CSS stays in global app.css - If new CSS is needed, add to playground app.css FIRST, then copy to app
- The playground is always the CSS source of truth
Stage 3: Local Validation (Vite on Host)
Architecture
Setup (per project)
Step 1:
Step 2:
Step 3: Open
cd ~/project-app && npm install (one-time)Step 2:
npm run dev -- --host (starts Vite, connects to prod API)Step 3: Open
https://capacitor-dev.tail5b443a.ts.net/project-name/ on phonevite.config.js must include server: { allowedHosts: true } for the funnel to work. The app's api.js and keycloak.js use import.meta.env.VITE_* with production URL fallbacks — no env vars needed for local dev.Port Convention
Each project runs Vite on a unique port so multiple dev servers can run simultaneously.
| Project | Vite Port | Funnel Path | Production URL |
|---|---|---|---|
| mcd-tracker | 5173 | /mcd-tracker/ | mcd-tracker-app.tail5b443a.ts.net |
| westside | 5174 | /westside/ | westsidekingsandqueens.tail5b443a.ts.net |
| (next) | 5175 | /project-name/ | project.tail5b443a.ts.net |
Docker Compose (offline fallback only)
Docker Compose exists in the app repos for fully offline, self-contained dev if needed (e.g. on a machine without k3s). It is NOT the primary local dev path on archbox. On the k3s host, Docker Compose fights nftables, Tailscale DNS, and k3s networking. Use Vite on host instead. If you must use Docker Compose, see the known gotchas in the repo's docker-compose.yml comments.
- nftables: Docker bridge CIDR. k3s host nftables drops Docker bridge traffic (172.16.0.0/12) by default. Fix: add
172.16.0.0/12tofirewall:allowed_cidrsinsalt/pillar/firewall.sls, runsalt-call state.apply firewall. Done once per host. (PR #97, pal-e-platform) - API build:
network: host. Docker builds can't resolve DNS through Tailscale's MagicDNS (100.100.100.100 is only reachable from the host network namespace). Fix: usebuild: { context: ../project-api, network: host }in docker-compose.yml. - Node image:
node:22notnode:22-alpine. Alpine's npm crashes with 'Exit handler never called' on large installs. Use the full Debian-based image. - First
docker compose upis slow. npm install downloads all dependencies into a named volume. Subsequent runs are fast (volume persists). Be patient on first run (~2-5 minutes).
Dev URL (Phone Access)
capacitor-dev.tail5b443a.ts.net — k8s nginx pod with Tailscale funnel, routing paths to host Vite ports. Deployed via pal-e-deployments/overlays/capacitor-dev/prod/. To add a new project: add a location /project-name/ block to the configmap and update the landing page HTML.
NEVER use the main archbox hostname for dev funnels. The dev hub gets its own Tailscale hostname — same way playground and every other service does. Implementation: k3s pod with nginx reverse-proxy + Tailscale funnel operator, routing paths to host ports.
Gate 2: Output Contract
The app is validated when:
npm run dev -- --hoststarts Vite instantly- Landing page renders at
localhost:PORT capacitor-dev.tail5b443a.ts.net/project-name/accessible from phone- Sign in via production Keycloak works
- Home page fetches from production API and renders real data
- Full flow works end-to-end (auth → data → interactivity)
- Hot reload works (change .svelte → browser updates in <1s)
npm run buildproduces static output- Dockerfile builds and serves the SPA
- THEN push to prod (CI → Harbor → ArgoCD)
Stage 4: Production Deploy
Push → CI (Woodpecker) → Harbor → ArgoCD (via pal-e-deployments kustomize overlay). Only after Gate 2 passes.
Gate 3: Production Smoke Test
- Web app loads at production URL without errors
- Auth flow (Keycloak) works end-to-end
- API calls return expected data (not 502/503)
- Mobile viewport renders correctly on phone browser
Stage 5: iOS Build Pipeline
Build the native iOS app from the Capacitor project. Only after Gate 3 passes.
Prerequisites
- Apple Developer account enrolled ($99/year)
- Xcode installed on Mac CI agent (mac-agent Salt state)
- App ID + provisioning profile created in Apple Developer portal
- Signing certificate (distribution) installed in Keychain
Build Steps
npm run build— production SvelteKit buildnpx cap sync ios— sync web assets to native iOS project- Open
ios/App/App.xcworkspacein Xcode (orxcodebuildon CI) - Archive with release configuration and distribution signing
- Export IPA via
xcodebuild -exportArchiveor Xcode Organizer
CI Automation
Mac CI agent (provisioned via
salt/states/mac-agent) runs Xcode builds. Woodpecker pipeline triggers on merge to main with platform: darwin agent filter. IPA artifact is uploaded to Harbor OCI registry or MinIO for distribution.Gate 4: TestFlight Internal Build
- IPA uploaded to App Store Connect via
altoolor Transporter - TestFlight internal testing group receives the build
- Lucas installs and validates on physical device
- No crashes in TestFlight crash reports
- All native capabilities (camera, push notifications, etc.) work if applicable
Stage 6: App Store Submission
Submit the validated TestFlight build to the App Store for public review. Only after Gate 4 passes.
Submission Checklist
- App Store Connect metadata complete (description, screenshots, keywords, categories)
- Privacy policy URL set and accessible
- App icon meets Apple HIG specs (1024x1024 source, all required sizes generated)
- Version number and build number incremented
- Export compliance (encryption) questionnaire answered
- Submit for review from App Store Connect
Gate 5: App Store Review Approved
- Apple review passes (no rejections, or rejections resolved and resubmitted)
- App appears in App Store search results
- Download and install from App Store on fresh device
- Post-launch smoke test: auth, core flows, no regressions
Rejection Handling
If Apple rejects the submission: create a Forgejo issue for each rejection reason, fix in the normal pipeline (branch, PR, merge, redeploy), then re-enter Stage 5 from the build step. Do not skip Gate 3 or Gate 4 on the fix cycle.
Repo Structure (per project)
Related
sop-frontend-experiment— playground creationtodo-capacitor-audit-agent— future automated audit agentfeedback_playground_gate— behavioral memory for gate enforcement- claude-custom #117 — tracking issue