| name | ship |
| description | Ship a Lovable app to iOS TestFlight and Google Play. Registers all third-party services, wraps the app in Capacitor, and submits builds — no app store knowledge required. Triggered by: "ship this app", "publish this Lovable app", "wrap in Capacitor", "get this on TestFlight", "submit to App Store", "deploy to Play Store", "turn this into a native app", "put this on the app store".
|
| tools | ["Bash","Read","Write","Edit","Glob","Grep","WebFetch","mcp__Claude_in_Chrome__navigate","mcp__Claude_in_Chrome__computer","mcp__Claude_in_Chrome__find","mcp__Claude_in_Chrome__form_input","mcp__Claude_in_Chrome__read_page","mcp__Claude_in_Chrome__get_page_text","mcp__Claude_in_Chrome__tabs_context_mcp","mcp__Claude_in_Chrome__tabs_create_mcp"] |
Ship a Lovable App
Wrap a Lovable/GitHub web app as a native iOS + Android app and submit to TestFlight and Google Play. This skill uses frozen, battle-tested templates — no code is regenerated from scratch.
🎯 Operating Philosophy — read this BEFORE doing anything
You (Claude) do everything. The user only logs in.
The user is a Lovable user who has never touched Xcode, the Apple Developer Portal, Google Play Console, git, or a terminal. They know how to type their email, click "Sign in with Google", and tap "Approve" in a push notification on their phone. That is the entire skillset you can assume.
What "the user only logs in" means in practice
| ❌ Don't make the user… | ✅ Instead, you do this |
|---|
| Paste a GitHub repo URL | Open github.com/Mark-Taikai?tab=repositories in Chrome via the MCP extension. Ask them to log in if needed. You read the repo list and ask which one is the Lovable app — by name, not URL. |
| Generate a Personal Access Token and paste it | Drive their logged-in Chrome session to github.com/settings/tokens/new?...&scopes=repo,workflow. You click "Generate token", you read the token off the page via get_page_text, you use it. Never make them copy-paste. |
| Paste a Lovable URL | Open lovable.dev in Chrome. Ask them to log in. You read their project list, find the match for the GitHub repo they picked, read the live preview URL off the project page. |
| Type their Apple Team ID | Open developer.apple.com/account in Chrome. Once they're logged in, you read the Team ID from the membership page. |
| Type their Apple Developer email | Same — read it from the account page after login. |
| Type their Google Play account | Open play.google.com/console. Once logged in, you read the account info from the page. |
| Find and copy an API key from a dashboard | You navigate the dashboard, you find the key, you copy it via JS injection or get_page_text. The user never sees a copy-paste step. |
| Click through ASC API key download → paste contents | You click "Generate", you download the .p8, you read it via shell, you base64-encode it for GitHub Secrets. The user only logs in. |
What the user IS responsible for (the entire list)
- Logging in to GitHub, Lovable, Apple Developer, Google Play, RevenueCat, OneSignal — one tab at a time as you open them.
- Approving 2FA / push notifications on their phone when a provider asks.
- Confirming destructive actions ("Yes, create the app", "Yes, charge me $99/year for Apple Developer") that the provider's UI requires a human click on.
- Providing one-time information that only they know: the app's display name, what category it falls under, the bundle ID format they want, what the icon should look like.
That's the entire list. Anything else you make them do is a bug in this skill — file an issue.
Concrete consequences for the workflow below
- Step 1 is no longer "ask for repo URL." It's "open GitHub in Chrome, log user in, list their repos."
- Step 2's question count drops to 4 from 9. You can read most of
01-questions.md from logged-in browser sessions.
- GitHub authentication for
git push is not a PAT-paste flow. You drive the user's Chrome session to generate the PAT autonomously and read it off the page.
- Lovable URL discovery is via the user's logged-in lovable.dev session, not by asking.
When in doubt, ask yourself: "can I read this from a page the user is logged into?" If yes, open the page and read it. Don't ask.
v2.0 architecture (default): apps ship with bundled dist/ web
assets inside the .ipa, plus @capgo/capacitor-updater pulling OTA
bundles from your own Supabase Storage bucket. This avoids Apple's
Guideline 4.2 ("Minimum Functionality") risk that the v1.x web-shell
pattern triggered, and prevents bad live-site deploys from bricking
installed apps. Read references/11-bundled-ota.md for the full
architecture before starting. Migrating an app from v1.x?
Read references/12-migration-guide.md first.
🚦 Two-path rule — pick the right architecture for the app's stack
There are two valid native architectures and one forbidden combination. The choice is determined by the app's stack, not by what's convenient:
✅ Path A — Bundled-dist + Capacitor Updater (v2.0 default)
For apps with: Vite SPA + raw Supabase OAuth (i.e. @supabase/supabase-js directly).
webDir: 'dist', no server.url in capacitor.config.ts
- Native Google/Apple Sign-In via the edge-function flow (refs 07 + 08)
- OTA via
@capgo/capacitor-updater from Supabase Storage
- Standard v2.0 templates
✅ Path B — server.url + Lovable Cloud Auth (for TanStack Lovable apps)
For apps with: TanStack Start (Cloudflare Workers backend) + @lovable.dev/cloud-auth-js.
server.url: 'https://app.lovable.app' in capacitor.config.ts — the WebView loads the live Lovable URL
- Native Google/Apple Sign-In via
lovable.auth.signInWithOAuth() — this works inside a WebView because Lovable Cloud Auth manages the redirect via its own URL scheme handler, not Safari's. See ref 14-lovable-cloud-auth.md for the wrapper.
- OTA is automatic — every Lovable redeploy updates the live URL the WebView loads
- Apple 4.2 risk mitigated by ensuring the app has substantial native features (haptics, push, IAP) demonstrating native value beyond the WebView
🚫 FORBIDDEN — server.url + raw Supabase OAuth (the v1.x regression)
For apps with: any stack + @supabase/supabase-js auth.
This combination is what caused weeks of debugging. The failure mode:
- App opens Google sign-in inside the Capacitor WebView
- Google's consent page completes
- The OAuth redirect (to
https://app.lovable.app/auth/callback) leaves the WebView and opens Safari instead — because Capacitor's WebView doesn't claim the URL scheme
- User authenticates in Safari; session lands in Safari's cookies
- WebView keeps showing the login page; user is logged out forever as far as the app knows
There is no clean fix at the WebView layer for raw Supabase OAuth. The only working flows are Path A (native SDK + edge function code exchange) or Path B (Lovable Cloud Auth which handles the redirect itself).
When to STOP and ask the user: if the app has @supabase/supabase-js directly AND won't static-export (e.g. SSR-heavy TanStack with no static preset), surface this as a blocker. The options are:
- Refactor to Lovable Cloud Auth (
@lovable.dev/cloud-auth-js) — then Path B works
- Refactor server-only code to client fetches — then Path A works
- Ship without native sign-in (email/password / magic link only) and use server.url — Apple 4.2 risk remains but native sign-in isn't broken
Do NOT silently choose the forbidden combination. If a previous Claude session in this project suggested it, that was wrong — flag it and pick A or B.
⚡ ALWAYS START HERE — Pre-flight (mandatory)
Read references/00-preflight.md and run every check before doing anything else.
Fast path decision after pre-flight:
- Client is a returning client (existing org in memory) AND repo already has all 5 GitHub secrets → use
references/09-returning-client-fast-path.md (skip Steps 1–4 below entirely)
- New client or missing secrets → continue with standard workflow below
Standard Workflow (new clients / new secret setup only)
Step 1: Find the Repo (Chrome-driven, no URL asks)
Do not ask the user for a repo URL. Find it autonomously.
-
Open GitHub in Chrome via the MCP extension (tabs_create_mcp → navigate to https://github.com/?tab=repositories).
-
Check login state. If the page redirects to a login screen or shows "Sign in", say to the user: "I've opened GitHub — please sign in in the tab I just opened, then say 'logged in' here." Wait for confirmation. Do NOT proceed until logged in.
-
Read the repo list via get_page_text or find for "repository row". Extract repo names.
-
Identify the Lovable app. Lovable repos typically:
- Are recently updated
- Contain a
lovable.app reference somewhere in the README or have lovable in tags
- Have
vite.config.ts + src/App.tsx (React+Vite, which is Lovable's stack)
If multiple candidates: list the top 5 by recent activity and ask the user to pick by name.
If one obvious candidate: pre-select it and confirm with "Shipping {repo-name} — yes?"
-
Get the clone URL from the repo page (https://github.com/<user>/<repo>).
-
Clone via shell (the bash sandbox can clone public repos without auth; for private repos use the autonomous PAT flow described later):
git clone https://github.com/{user}/{repo} /tmp/lovable-to-app-store/{repo} --depth=1
-
Read package.json, src/App.tsx, and index.html from the cloned copy.
-
DETECT THE APP ARCHITECTURE (mandatory — different code paths for each):
Lovable ships TWO architectures as of late April 2026. The plugin handles both, but they require different templates. Detect by checking these signals (in order):
| Signal | Indicates |
|---|
src/routes/ directory exists with file-based routes (e.g. __root.tsx, index.tsx) | TanStack Start |
app.config.ts or tanstack.config.ts at repo root | TanStack Start |
package.json has @tanstack/start or @tanstack/react-start dependency | TanStack Start |
wrangler.toml, wrangler.jsonc, or cloudflare in package.json scripts | TanStack Start on Cloudflare Workers (the newer Lovable default) |
@lovable.dev/cloud-auth-js in deps | TanStack Start (Lovable's new auth lib) |
src/main.tsx exists + index.html exists + vite.config.ts present | Vite SPA (the older Lovable default) |
| None of the above match cleanly | Custom / unknown — ask the user, do NOT guess |
Default precedence: if both TanStack signals AND Vite signals are present (some hybrid configs exist), prefer TanStack — that's the modern default.
What to do after detection:
-
Vite SPA → continue with this skill's standard Steps 2–6. Use references/11-bundled-ota.md for the bundled-OTA architecture and the standard references/templates/capacitor.config.ts, vite.config.prod.ts, sdk-init-snippet.ts. The dist/ folder built by npm run build is what gets bundled into the IPA.
-
TanStack Start → read references/13-tanstack-start.md before doing anything else. The bundled-dist approach still applies, but the build invocation, output directory, and several config files differ. TanStack needs vite build --mode spa (or equivalent vinxi build --preset static-spa) to produce a static bundle suitable for Capacitor. Server-only routes need to be replaced with client-fetch versions at build time. references/13-tanstack-start.md is the canonical guide — DO NOT try to apply the Vite SPA templates directly to a TanStack project. You will produce a broken build.
-
Custom / unknown → stop and ask the user. Show them the signals you detected (or didn't) and ask which path: Vite SPA, TanStack Start on Cloudflare, or Other (tell me what you have). Don't proceed with a guess.
Save the detected architecture to the app's memory file under architecture (values: "vite-spa", "tanstack-start", "tanstack-cloudflare", "custom"). Subsequent update / add-native runs read this field to skip re-detection.
-
Load memory from ~/Documents/Claude/lovable-to-app-store/memory/.
Why we do it this way: the user never sees a github.com/... URL or a "what's your repo?" question. They see a Chrome tab open on github.com → log in once → confirm the repo by name → done. And the plugin self-detects whether the repo is a Vite SPA (older Lovable apps) or TanStack Start (newer Lovable apps) — it doesn't ask the user "what stack are you on" because the user is a Lovable user; they don't know what TanStack is, and they shouldn't have to.
Step 2: Ask Upfront Questions
Read references/01-questions.md. Ask ALL questions at once, never mid-workflow.
Step 3: Register Services
Read references/02-service-registration.md for browser workflows (Apple, Google Play, RevenueCat, OneSignal).
Step 4: Capacitor Setup — USE FROZEN TEMPLATES
Do NOT regenerate from 03-capacitor-setup.md. Copy files from references/templates/ and substitute placeholders. See references/09-returning-client-fast-path.md Step 3 for the exact substitution process.
Step 4b (conditional): Native Sign-In Wiring
If the app uses Google Sign-In or Apple Sign-In, the URL-scheme step in 03-capacitor-setup.md is only half the story. The full architecture — Edge Function, code exchange, signInWithIdToken() — lives in dedicated references:
- Google Sign-In → read
references/07-google-native-signin.md (full edge-function flow, capacitor.config.ts iosClientId/serverClientId/forceCodeForRefreshToken rules, the "drawer never closes" fix, Lovable-doesn't-auto-deploy-edge-functions warning)
- Apple Sign-In → read
references/08-apple-native-signin.md (same code-exchange pattern using bundle ID as client_id, JWT client secret generation, App.entitlements wiring)
Both flows require the Edge Function to be deployed by Lovable explicitly — verify with curl (should return 400, not 404) before testing the build.
Step 5: Build and Submit
MANDATORY pre-archive check (whether building locally or via CI):
run the verification block in references/10-build-gotchas-addendum.md
under "Pre-archive verification checklist". It checks for:
UIMainStoryboardFile = Main in Info.plist (silent black screen if missing)
- No
iosScheme: 'https' in capacitor.config.ts (silently rejected, breaks WebView)
- Capacitor CLI / core / ios all on the same major version
- Node 22+ (required by Capacitor CLI v8.3+)
- Podfile post_install hook present (was wiped by last
cap sync?)
- GoogleSignIn pod >= 7.1.0 (avoids ITMS-91061)
- public/index.html bundled
If any check fails, fix it BEFORE invoking xcodebuild archive. Each of
these has caused multi-hour debug sessions in the wild because the
TestFlight upload looks fine and the symptom (black screen on launch) gives
no logs.
GitHub Actions CI handles archive + upload automatically after push. Read
references/04-build-and-submit.md only if CI fails after 3 runs. Also
consult references/10-build-gotchas-addendum.md for the other gotchas
(ITMS-91061, provisioning-profile invalidation after enabling Sign in with
Apple, and the rest).
Step 6: TestFlight Group + Invite Testers
After altool returns successfully (build uploaded), drive ASC TestFlight UI:
- Open
https://appstoreconnect.apple.com/apps/{app_store_connect_app_id}/testflight/groups in Chrome
- If no Internal Testing group exists yet: click "+ Internal Testing Group" → name it "Internal Testers" (or read the org's default from memory)
- Add the user's Apple Developer account email as the first tester
- Read the resulting TestFlight join link (
testflight.apple.com/join/{code}) from the group's page
- Save the join link + group ID to the app's memory file under
testflight: { group_id, join_link, testers: [...] }
- If the user provided additional tester emails in upfront questions (or via memory's org-level
default_testers list), invite them via the same UI
For subsequent ships of the same app, this group already exists — just add new testers / read the existing join link.
Step 7: Save Memory (everything)
Read references/05-memory-schema.md. Save after every step that produces a new ID or key. Specifically save:
- All Apple IDs (Team ID, App ID, Services ID, ASC app ID)
- All API keys + key paths (ASC, Sign in with Apple, APNs)
- All Google OAuth client IDs (Web, iOS, Android) + iOS reversed scheme
- All RevenueCat keys (iOS + Android)
- OneSignal app ID + REST API key path
- NEW: GitHub Actions run ID of the successful upload (for debugging later runs that fail)
- NEW: Apple Beta App Review submission ID (for tracking review state)
- NEW: TestFlight join link + group ID
- NEW:
architecture field (vite-spa / tanstack-cloudflare / custom) and auth_client (supabase / lovable-cloud)
If Google or Apple Sign-In was wired up, also persist the google_auth / apple_auth (Path A) or lovable_cloud_auth (Path B) blocks per refs 07 / 08 / 14.
Frozen Templates (the whole point)
The references/templates/ directory contains the exact files from the successful Task List deployment. Copy and substitute — never rewrite.
The only things that change per app:
| Variable | Example |
|---|
{{BUNDLE_ID}} | com.yourcompany.gamechime |
{{APP_DISPLAY_NAME}} | GameChime |
{{LOVABLE_URL}} | https://abc123.lovable.app |
{{REVENUECAT_IOS_KEY}} | appl_xxxxxxxxxxxxxx |
{{REVENUECAT_ANDROID_KEY}} | goog_xxxxxxxxxxxxxx |
{{ONESIGNAL_APP_ID}} | xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx |
Files to deploy per app:
references/templates/.github/workflows/ios-testflight.yml → .github/workflows/ (3× {{BUNDLE_ID}})
references/templates/.github/scripts/setup_cert.py → .github/scripts/ (unchanged)
references/templates/capacitor.config.ts → repo root (3 variables)
references/templates/sdk-init-snippet.ts → inject into src/main.tsx (3 variables)
references/templates/info-plist-additions.xml → add entries to ios/App/App/Info.plist
references/templates/vite.config.prod.ts → repo root (no substitutions)
references/templates/index-html-boot-overlay.html → splice into index.html (no substitutions)
references/templates/ota-updater-client.ts → src/lib/ota-updater.ts (2 variables)
references/templates/ota-manifest-edge-function.ts → supabase/functions/ota-manifest/index.ts
references/templates/asc-submit.py → repo root (used by build script + CI; no substitutions)
references/templates/build-local.sh → repo root if user wants local builds (8 variables)
Key Principles
- Pre-flight first. Always run
references/00-preflight.md before any other work.
- Returning client fast path. If a returning client (existing org in memory) already has all 5 GitHub secrets → skip service registration entirely.
- Never regenerate what's frozen. Use the templates. Never retype code from reference docs.
- Validate credentials immediately. A 401 from Apple takes 5 seconds to catch. A failed CI build takes 20 minutes.
- Never ask mid-workflow. All questions happen upfront.
- Save everything. Every ID and key to memory immediately.
- Native sign-in needs Edge Functions. On Lovable-managed Supabase, native Google/Apple Sign-In can't use the native idToken directly — exchange the auth code server-side. See refs 07 and 08.
Reference Files
references/00-preflight.md — Start here every time
references/01-questions.md — Upfront questions (standard workflow)
references/02-service-registration.md — Browser workflows for Apple, Google, RevenueCat, OneSignal
references/03-capacitor-setup.md — Manual Capacitor setup (fallback only)
references/04-build-and-submit.md — Build errors and manual fallback
references/05-memory-schema.md — Memory file format
references/06-ci-signing.md — CI signing deep-dive (reference only)
references/07-google-native-signin.md — Google Sign-In via Supabase Edge Function (April 2026)
references/08-apple-native-signin.md — Apple Sign-In via Supabase Edge Function (April 2026)
references/09-returning-client-fast-path.md — Fast path for returning clients (existing org with all 5 secrets in GitHub)
references/10-build-gotchas-addendum.md — All known silent-failure causes + the pre-archive verification checklist
references/11-bundled-ota.md — v2.0 architecture: bundled dist + Capacitor Updater + Supabase Storage OTA (Vite SPA)
references/12-migration-guide.md — v1.x → v2.0 migration path (existing apps shipped on server.url)
references/13-tanstack-start.md — TanStack Start path: SPA build presets, server-only code handling, @lovable.dev/cloud-auth-js wiring (for newer Lovable apps)
references/templates/ — Frozen template files — copy these, never regenerate
Final Output
✅ {App Name} is live on TestFlight
iOS: TestFlight link → [link or "check App Store Connect"]
Android: [Play Console internal testing link]
Secrets: all 5 set in GitHub ✓
Templates used: frozen (no code regenerated) ✓
Native sign-in: [Google ✓ / Apple ✓ / N/A] — edge function deployed and verified
Memory saved: ~/Documents/Claude/lovable-to-app-store/memory/apps/{bundle-id}.json
OTA updates are automatic — edit in Lovable, users see it on next launch.
To add native features: "add [feature] to {App Name}"