@ramonclaudio/create-vexpo 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (174) hide show
  1. package/README.md +10 -10
  2. package/dist/index.js +8 -7
  3. package/dist/templates/default/.eas/workflows/asc-events.yml +9 -6
  4. package/dist/templates/default/.eas/workflows/deploy-production.yml +28 -15
  5. package/dist/templates/default/.eas/workflows/e2e-tests.yml +3 -2
  6. package/dist/templates/default/.eas/workflows/pr-preview.yml +12 -21
  7. package/dist/templates/default/.eas/workflows/release.yml +3 -7
  8. package/dist/templates/default/.eas/workflows/rollback.yml +54 -28
  9. package/dist/templates/default/.eas/workflows/rollout.yml +27 -33
  10. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +1 -5
  11. package/dist/templates/default/.eas/workflows/testflight.yml +3 -7
  12. package/dist/templates/default/.github/workflows/check.yml +20 -12
  13. package/dist/templates/default/.maestro/launch.yaml +19 -10
  14. package/dist/templates/default/AGENTS.md +25 -8
  15. package/dist/templates/default/DESIGN.md +14 -10
  16. package/dist/templates/default/README.md +83 -78
  17. package/dist/templates/default/SETUP.md +159 -152
  18. package/dist/templates/default/__tests__/convex/_auth-harness.test.ts +112 -0
  19. package/dist/templates/default/__tests__/convex/_harness.ts +132 -0
  20. package/dist/templates/default/__tests__/convex/appAttest.test.ts +172 -0
  21. package/dist/templates/default/__tests__/convex/appAttestStore.test.ts +48 -0
  22. package/dist/templates/default/__tests__/convex/pushTokens-remove.test.ts +106 -0
  23. package/dist/templates/default/__tests__/convex/pushTokens-upsert.test.ts +146 -0
  24. package/dist/templates/default/__tests__/convex/users-deleteAccount.test.ts +140 -0
  25. package/dist/templates/default/__tests__/convex/users-deleteAvatar.test.ts +104 -0
  26. package/dist/templates/default/__tests__/convex/users-getMe.test.ts +98 -0
  27. package/dist/templates/default/__tests__/convex/users-getUser.test.ts +120 -0
  28. package/dist/templates/default/__tests__/convex/users-hardDeleteExpired.test.ts +67 -0
  29. package/dist/templates/default/__tests__/convex/users-restoreAccount.test.ts +96 -0
  30. package/dist/templates/default/__tests__/convex/users-updateAvatar.test.ts +92 -0
  31. package/dist/templates/default/__tests__/convex/users-updateProfile.test.ts +126 -0
  32. package/dist/templates/default/__tests__/convex/webhook.test.ts +31 -0
  33. package/dist/templates/default/__tests__/lib/deep-link.test.ts +51 -6
  34. package/dist/templates/default/__tests__/lib/schemas.test.ts +205 -0
  35. package/dist/templates/default/__tests__/lib/text-style.test.ts +31 -0
  36. package/dist/templates/default/_env.example +7 -7
  37. package/dist/templates/default/_gitattributes +1 -1
  38. package/dist/templates/default/_gitignore +17 -2
  39. package/dist/templates/default/_npmrc +7 -0
  40. package/dist/templates/default/_oxlintrc.json +1 -1
  41. package/dist/templates/default/app-store/accessibility.config.json +20 -0
  42. package/dist/templates/default/app-store/privacy.config.json +27 -0
  43. package/dist/templates/default/app.config.ts +105 -33
  44. package/dist/templates/default/app.json +1 -9
  45. package/dist/templates/default/convex/_generated/api.d.ts +12 -0
  46. package/dist/templates/default/convex/admin.ts +0 -13
  47. package/dist/templates/default/convex/appAttest.ts +467 -0
  48. package/dist/templates/default/convex/appAttestStore.ts +141 -0
  49. package/dist/templates/default/convex/apple.ts +53 -0
  50. package/dist/templates/default/convex/auth.ts +6 -45
  51. package/dist/templates/default/convex/constants.ts +2 -7
  52. package/dist/templates/default/convex/crons.ts +12 -5
  53. package/dist/templates/default/convex/email.ts +4 -24
  54. package/dist/templates/default/convex/env.ts +0 -4
  55. package/dist/templates/default/convex/errors.ts +0 -7
  56. package/dist/templates/default/convex/functions.ts +0 -26
  57. package/dist/templates/default/convex/http.ts +3 -5
  58. package/dist/templates/default/convex/log.ts +2 -25
  59. package/dist/templates/default/convex/pushSender.ts +145 -0
  60. package/dist/templates/default/convex/pushTokens.ts +110 -13
  61. package/dist/templates/default/convex/rateLimit.ts +8 -39
  62. package/dist/templates/default/convex/schema.ts +48 -5
  63. package/dist/templates/default/convex/tsconfig.json +1 -0
  64. package/dist/templates/default/convex/users.ts +143 -61
  65. package/dist/templates/default/convex/validators.ts +1 -38
  66. package/dist/templates/default/convex/webhook.ts +1 -31
  67. package/dist/templates/default/convex.json +1 -2
  68. package/dist/templates/default/metro.config.js +9 -1
  69. package/dist/templates/default/package.json +67 -70
  70. package/dist/templates/default/plugins/README.md +5 -1
  71. package/dist/templates/default/scripts/README.md +9 -9
  72. package/dist/templates/default/scripts/_run.mjs +3 -20
  73. package/dist/templates/default/scripts/clean.ts +81 -69
  74. package/dist/templates/default/scripts/gen-update-cert.mjs +98 -0
  75. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home)/index.tsx +21 -6
  76. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(home,search)/_layout.tsx +9 -8
  77. package/dist/templates/default/{app → src/app}/(app)/(tabs)/(search)/index.tsx +26 -24
  78. package/dist/templates/default/{app → src/app}/(app)/(tabs)/_layout.tsx +3 -4
  79. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/_layout.tsx +10 -6
  80. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/index.tsx +81 -51
  81. package/dist/templates/default/{app → src/app}/(app)/(tabs)/settings/preferences.tsx +72 -12
  82. package/dist/templates/default/src/app/(app)/_layout.tsx +147 -0
  83. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/_layout.tsx +4 -5
  84. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/forgot-password.tsx +15 -9
  85. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/reset-password.tsx +88 -14
  86. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-in.tsx +65 -35
  87. package/dist/templates/default/{app/(auth) → src/app/(app)/auth}/sign-up.tsx +131 -196
  88. package/dist/templates/default/src/app/(app)/debug.tsx +479 -0
  89. package/dist/templates/default/{app → src/app}/(app)/help.tsx +76 -64
  90. package/dist/templates/default/{app → src/app}/(app)/linked.tsx +21 -27
  91. package/dist/templates/default/{app → src/app}/(app)/privacy.tsx +35 -8
  92. package/dist/templates/default/src/app/(app)/profile/change-password.tsx +264 -0
  93. package/dist/templates/default/{app/(app)/profile.tsx → src/app/(app)/profile/index.tsx} +179 -255
  94. package/dist/templates/default/src/app/(app)/restore-account.tsx +192 -0
  95. package/dist/templates/default/src/app/(app)/sessions.tsx +287 -0
  96. package/dist/templates/default/src/app/(app)/welcome.tsx +194 -0
  97. package/dist/templates/default/src/app/+native-intent.tsx +25 -0
  98. package/dist/templates/default/src/app/+not-found.tsx +43 -0
  99. package/dist/templates/default/{app → src/app}/_layout.tsx +28 -37
  100. package/dist/templates/default/src/components/auth/apple-button.tsx +51 -0
  101. package/dist/templates/default/{components → src/components}/auth/otp-verification.tsx +79 -58
  102. package/dist/templates/default/{components → src/components}/auth/password-field.tsx +74 -18
  103. package/dist/templates/default/src/components/auth/segmented-toggle.tsx +71 -0
  104. package/dist/templates/default/src/components/ui/content-unavailable.tsx +81 -0
  105. package/dist/templates/default/src/components/ui/convex-error.tsx +21 -0
  106. package/dist/templates/default/src/components/ui/error-boundary.tsx +89 -0
  107. package/dist/templates/default/{components → src/components}/ui/loading-screen.tsx +5 -4
  108. package/dist/templates/default/{components → src/components}/ui/material.tsx +50 -17
  109. package/dist/templates/default/src/components/ui/offline-banner.tsx +59 -0
  110. package/dist/templates/default/{components → src/components}/ui/prominent-button.tsx +8 -11
  111. package/dist/templates/default/{components → src/components}/ui/skeleton.tsx +31 -13
  112. package/dist/templates/default/src/components/ui/status-text.tsx +64 -0
  113. package/dist/templates/default/src/components/ui/update-banner.tsx +85 -0
  114. package/dist/templates/default/{constants → src/constants}/layout.ts +0 -6
  115. package/dist/templates/default/{constants → src/constants}/theme.ts +49 -64
  116. package/dist/templates/default/{constants → src/constants}/ui.ts +13 -4
  117. package/dist/templates/default/src/hooks/use-debounce.ts +12 -0
  118. package/dist/templates/default/src/hooks/use-deep-link.ts +51 -0
  119. package/dist/templates/default/src/hooks/use-delete-account.ts +35 -0
  120. package/dist/templates/default/src/hooks/use-motion-screen-options.ts +13 -0
  121. package/dist/templates/default/src/hooks/use-network.ts +34 -0
  122. package/dist/templates/default/{hooks → src/hooks}/use-notifications.ts +39 -30
  123. package/dist/templates/default/src/hooks/use-reduce-transparency.ts +30 -0
  124. package/dist/templates/default/{hooks → src/hooks}/use-theme.ts +0 -5
  125. package/dist/templates/default/src/lib/appAttest.ts +78 -0
  126. package/dist/templates/default/src/lib/assets.ts +9 -0
  127. package/dist/templates/default/src/lib/deep-link.ts +82 -0
  128. package/dist/templates/default/{lib → src/lib}/dev-menu.ts +0 -4
  129. package/dist/templates/default/{lib → src/lib}/device.ts +1 -13
  130. package/dist/templates/default/{lib → src/lib}/dynamic-font.ts +13 -10
  131. package/dist/templates/default/src/lib/dynamic-symbol-size.ts +33 -0
  132. package/dist/templates/default/src/lib/masks.ts +21 -0
  133. package/dist/templates/default/src/lib/native-state.ts +20 -0
  134. package/dist/templates/default/{lib → src/lib}/notifications.ts +7 -45
  135. package/dist/templates/default/{lib → src/lib}/preferences.ts +0 -2
  136. package/dist/templates/default/{lib → src/lib}/schemas.ts +19 -16
  137. package/dist/templates/default/src/lib/text-style.ts +20 -0
  138. package/dist/templates/default/{lib → src/lib}/updates.ts +0 -7
  139. package/dist/templates/default/store.config.json +1 -1
  140. package/dist/templates/default/tsconfig.json +3 -1
  141. package/dist/templates/default/vitest.config.ts +8 -1
  142. package/package.json +5 -5
  143. package/dist/templates/default/app/(app)/_layout.tsx +0 -73
  144. package/dist/templates/default/app/(app)/debug.tsx +0 -389
  145. package/dist/templates/default/app/(app)/sessions.tsx +0 -191
  146. package/dist/templates/default/app/(app)/welcome.tsx +0 -140
  147. package/dist/templates/default/app/+native-intent.tsx +0 -14
  148. package/dist/templates/default/app/+not-found.tsx +0 -51
  149. package/dist/templates/default/bun.lock +0 -1860
  150. package/dist/templates/default/components/auth/segmented-toggle.tsx +0 -47
  151. package/dist/templates/default/components/ui/convex-error.tsx +0 -32
  152. package/dist/templates/default/components/ui/error-boundary.tsx +0 -57
  153. package/dist/templates/default/components/ui/offline-banner.tsx +0 -58
  154. package/dist/templates/default/components/ui/status-text.tsx +0 -49
  155. package/dist/templates/default/components/ui/update-banner.tsx +0 -82
  156. package/dist/templates/default/fingerprint.config.js +0 -9
  157. package/dist/templates/default/hooks/use-debounce.ts +0 -20
  158. package/dist/templates/default/hooks/use-deep-link.ts +0 -43
  159. package/dist/templates/default/hooks/use-network.ts +0 -11
  160. package/dist/templates/default/lib/assets.ts +0 -17
  161. package/dist/templates/default/lib/deep-link.ts +0 -71
  162. package/dist/templates/default/patches/PR-368.patch +0 -91
  163. package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
  164. /package/dist/templates/default/{hooks → src/hooks}/use-navigation-tracking.ts +0 -0
  165. /package/dist/templates/default/{hooks → src/hooks}/use-onboarding.ts +0 -0
  166. /package/dist/templates/default/{hooks → src/hooks}/use-reduced-motion.ts +0 -0
  167. /package/dist/templates/default/{hooks → src/hooks}/use-updates.ts +0 -0
  168. /package/dist/templates/default/{lib → src/lib}/a11y.ts +0 -0
  169. /package/dist/templates/default/{lib → src/lib}/app.ts +0 -0
  170. /package/dist/templates/default/{lib → src/lib}/auth-client.ts +0 -0
  171. /package/dist/templates/default/{lib → src/lib}/convex-auth.tsx +0 -0
  172. /package/dist/templates/default/{lib → src/lib}/env.ts +0 -0
  173. /package/dist/templates/default/{lib → src/lib}/haptics.ts +0 -0
  174. /package/dist/templates/default/{lib → src/lib}/storage.ts +0 -0
@@ -1,16 +1,16 @@
1
1
  # Setup reference
2
2
 
3
- Long-form companion to the README. Walks every phase `bunx vexpo full` runs, the prompts you'll see, env-var alternatives for non-interactive runs (CI), recovery paths, and what state ends up where.
3
+ Long-form companion to the README. Walks every phase `npx vexpo full` runs, the prompts you'll see, env-var alternatives for non-interactive runs (CI), recovery paths, and what state ends up where.
4
4
 
5
- The orchestrator is the published [`vexpo` CLI](https://www.npmjs.com/package/vexpo) (run via `bunx vexpo lite` (dev) or `bunx vexpo full` (TestFlight-ready)). `package.json` exposes every phase as a `bunx vexpo <phase-name>` shortcut. State lives in `.setup-state.json` (gitignored), `.env.local` (gitignored), Convex deployment env (server-side), and EAS project env (per-environment, with secret-visibility entries powering the JWT rotation cron).
5
+ The orchestrator is the published [`vexpo` CLI](https://www.npmjs.com/package/vexpo) (run via `npx vexpo lite` (dev) or `npx vexpo full` (TestFlight-ready)). `package.json` exposes every phase as a `npx vexpo <phase-name>` shortcut. State lives in `.setup-state.json` (gitignored), `.env.local` (gitignored), Convex deployment env (server-side), and EAS project env (per-environment, with secret-visibility entries powering the JWT rotation cron).
6
6
 
7
7
  ## TL;DR
8
8
 
9
9
  ```bash
10
10
  git clone <repo-url> my-app
11
11
  cd my-app
12
- bun install
13
- bunx vexpo full
12
+ npm install
13
+ npx vexpo full
14
14
  ```
15
15
 
16
16
  Plan ~30 minutes if your accounts already exist, ~60-90 if you're enrolling in the Apple Developer Program for the first time.
@@ -19,13 +19,13 @@ Plan ~30 minutes if your accounts already exist, ~60-90 if you're enrolling in t
19
19
 
20
20
  vexpo has three entry points:
21
21
 
22
- | Mode | Command | When |
23
- | ----------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------- |
24
- | Lite (dev) | `bunx vexpo lite` | Greenfield, dev-mode shortcut. Provisions Convex + Better Auth only. ~60 seconds to the iOS Simulator. |
25
- | Full (TestFlight) | `bunx vexpo full` | Greenfield, production setup. Walks signups, provisions Resend + Apple + EAS, signs JWTs, rebrands. |
26
- | Env sync | `bunx vexpo env push` | You already have all values in `.env.local` + `.env.prod`: just push them to Convex env and EAS env. No signups. |
22
+ | Mode | Command | When |
23
+ | ----------------- | -------------------- | ---------------------------------------------------------------------------------------------------------------- |
24
+ | Lite (dev) | `npx vexpo lite` | Greenfield, dev-mode shortcut. Provisions Convex + Better Auth only. ~60 seconds to the iOS Simulator. |
25
+ | Full (TestFlight) | `npx vexpo full` | Greenfield, production setup. Walks signups, provisions Resend + Apple + EAS, signs JWTs, rebrands. |
26
+ | Env sync | `npx vexpo env push` | You already have all values in `.env.local` + `.env.prod`: just push them to Convex env and EAS env. No signups. |
27
27
 
28
- `bunx vexpo env push` reads `.env.local` (dev) and `.env.prod` or `.env.production` (prod), classifies each key by destination, and pushes to Convex env and EAS env. Per-file confirmation, fingerprint diff on overwrites, no provisioning. Secret-visibility EAS env vars (rotation cron) need `eas env:create --visibility secret` and the command prints the exact invocations when it sees those keys.
28
+ `npx vexpo env push` reads `.env.local` (dev) and `.env.prod` or `.env.production` (prod), classifies each key by destination, and pushes to Convex env and EAS env. Per-file confirmation, fingerprint diff on overwrites, no provisioning. Secret-visibility EAS env vars (rotation cron) need `eas env:create --visibility secret` and the command prints the exact invocations when it sees those keys.
29
29
 
30
30
  Pick `env push` when:
31
31
 
@@ -40,10 +40,10 @@ Pick `lite` for the 60-second simulator path with no Apple Developer account or
40
40
 
41
41
  All modes accept `--dry-run`. Print every action the script would take, then exit without touching anything.
42
42
 
43
- | Command | Output |
44
- | ------------------------------- | ----------------------------------------------------------------------------------------------------- |
45
- | `bunx vexpo full --dry-run` | One block per phase: action (`run`/`skip (cached)`/`run (interactive)`), summary list of what it does |
46
- | `bunx vexpo env push --dry-run` | Per-source-file plan: every key, every destination, with `create`/`update`/`noop` status + diff |
43
+ | Command | Output |
44
+ | ------------------------------ | ----------------------------------------------------------------------------------------------------- |
45
+ | `npx vexpo full --dry-run` | One block per phase: action (`run`/`skip (cached)`/`run (interactive)`), summary list of what it does |
46
+ | `npx vexpo env push --dry-run` | Per-source-file plan: every key, every destination, with `create`/`update`/`noop` status + diff |
47
47
 
48
48
  Use it to:
49
49
 
@@ -54,24 +54,24 @@ Use it to:
54
54
 
55
55
  `--dry-run` does not hit the network for verification, doesn't prompt for credentials, doesn't write state. It only reads what already exists locally and prints the plan.
56
56
 
57
- ## What `bunx vexpo full` does
58
-
59
- The orchestrator runs the following phases in order, skipping any that are cached fresh in `.setup-state.json`. Each phase is also runnable standalone via `bunx vexpo <phase-name>`.
60
-
61
- | Phase | Command | Layer | What it does |
62
- | ----- | --------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------- |
63
- | 0 | `bunx vexpo accounts` | meta | Apple Developer / Expo / Convex / Resend signup confirmation |
64
- | 1 | `bunx vexpo rebrand` | ours | Replace template defaults (interactive, only if forking) |
65
- | 2 | `bunx vexpo convex` | ours | Provision Convex deployment, write `.env.local` |
66
- | 3 | `bunx vexpo better-auth` | ours | Generate `BETTER_AUTH_SECRET`, push `SITE_URL`, `APP_NAME` |
67
- | 4 | `bunx vexpo resend` | ours | Resend sending key + webhook (manual: DNS records at registrar) |
68
- | 5 | `bunx vexpo review-account` | ours | Seed App Review demo account on Convex |
69
- | 6 | `bunx vexpo full` (EAS phase) | eas | Thin wrapper: `eas init` + `eas env:push` from `.env.local` |
70
- | 7 | `bunx vexpo apple asc-key` | ours | Validate ASC API key against ASC `/v1/apps` (no upload) |
71
- | 7.5 | `bunx vexpo apple credentials` | ours | Wraps `eas credentials -p ios`. Pre-passes cached ASC creds, EAS auto-generates dist cert + profile + push key |
72
- | 8 | `bunx vexpo apple services-id` | ours | Attach SIWA capability via ASC API (manual: create the Services ID itself) |
73
- | 9 | `bunx vexpo apple jwt` | ours | Sign SIWA ES256 client_secret JWT, push to Convex env |
74
- | 10 | `bunx vexpo apple eas-rotation-secrets` | ours | Push the 5 EAS production secrets the JWT rotation cron needs |
57
+ ## What `npx vexpo full` does
58
+
59
+ The orchestrator runs the following phases in order, skipping any that are cached fresh in `.setup-state.json`. Each phase is also runnable standalone via `npx vexpo <phase-name>`.
60
+
61
+ | Phase | Command | Layer | What it does |
62
+ | ----- | -------------------------------------- | ----- | -------------------------------------------------------------------------------------------------------------- |
63
+ | 0 | `npx vexpo accounts` | meta | Apple Developer / Expo / Convex / Resend signup confirmation |
64
+ | 1 | `npx vexpo rebrand` | ours | Replace template defaults (interactive, only if forking) |
65
+ | 2 | `npx vexpo convex` | ours | Provision Convex deployment, write `.env.local` |
66
+ | 3 | `npx vexpo better-auth` | ours | Generate `BETTER_AUTH_SECRET`, push `SITE_URL`, `APP_NAME` |
67
+ | 4 | `npx vexpo resend` | ours | Resend sending key + webhook (manual: DNS records at registrar) |
68
+ | 5 | `npx vexpo review-account` | ours | Seed App Review demo account on Convex |
69
+ | 6 | `npx vexpo full` (EAS phase) | eas | Thin wrapper: `eas init` + `eas env:push` from `.env.local` |
70
+ | 7 | `npx vexpo apple asc-key` | ours | Validate ASC API key against ASC `/v1/apps` (no upload) |
71
+ | 7.5 | `npx vexpo apple credentials` | ours | Wraps `eas credentials -p ios`. Pre-passes cached ASC creds, EAS auto-generates dist cert + profile + push key |
72
+ | 8 | `npx vexpo apple services-id` | ours | Attach SIWA capability via ASC API (manual: create the Services ID itself) |
73
+ | 9 | `npx vexpo apple jwt` | ours | Sign SIWA ES256 client_secret JWT, push to Convex env |
74
+ | 10 | `npx vexpo apple eas-rotation-secrets` | ours | Push the 5 EAS production secrets the JWT rotation cron needs |
75
75
 
76
76
  Phases marked "manual" pause the CLI while you do something a Resend dashboard or Apple Developer portal can't be automated through. The CLI prints exact instructions and waits for you to press Enter.
77
77
 
@@ -85,7 +85,7 @@ eas submit -p ios --profile production # auto-creates the App Store record on
85
85
 
86
86
  We don't reinvent any of those, `eas-cli` owns the iOS platform layer end-to-end. The EAS init phase is a thin wrapper that does `eas init` + `eas env:push` because the orchestrator wants one entry point for the env mirror. You can run those two commands directly and skip our wrapper entirely.
87
87
 
88
- ## Phase 0: Accounts (`bunx vexpo accounts`)
88
+ ## Phase 0: Accounts (`npx vexpo accounts`)
89
89
 
90
90
  Splits "things you bring" from "things vexpo signs you up for":
91
91
 
@@ -100,25 +100,25 @@ For both, the script asks "do you have this?" and prints links if you don't. If
100
100
 
101
101
  ### Instant signups (we walk you through)
102
102
 
103
- | Account | Validation |
104
- | ------- | ----------------------------------------------------------- |
105
- | Convex | `~/.convex/config.json` exists after `bunx convex login` |
106
- | Expo | `bunx eas whoami` returns a username after `bunx eas login` |
107
- | Resend | `RESEND_FULL_ACCESS_KEY` env probes 200 on `/api-keys` |
103
+ | Account | Validation |
104
+ | ------- | --------------------------------------------------------- |
105
+ | Convex | `~/.convex/config.json` exists after `npx convex login` |
106
+ | Expo | `npx eas whoami` returns a username after `npx eas login` |
107
+ | Resend | `RESEND_FULL_ACCESS_KEY` env probes 200 on `/api-keys` |
108
108
 
109
109
  Each opens the signup page (free-tier accounts, instant), then runs the corresponding CLI login. For Resend, paste a full-access key into the env once: `export RESEND_FULL_ACCESS_KEY=re_...` (the script also prompts interactively if absent). The key is never persisted by vexpo, it's used to provision a scoped sending key + webhook, then forgotten.
110
110
 
111
111
  ### What you'll be prompted for in later phases
112
112
 
113
- | Phase | What it needs from you |
114
- | -------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
115
- | `bunx vexpo apple asc-key` | App Store Connect API key (issuer ID, key ID, .p8), created at [appstoreconnect.apple.com/access/integrations/api](https://appstoreconnect.apple.com/access/integrations/api) |
116
- | `bunx vexpo apple jwt` | Sign In with Apple key (key ID, .p8), created at [developer.apple.com/account/resources/authkeys/list](https://developer.apple.com/account/resources/authkeys/list) |
117
- | DNS records | Added by you at your registrar after `bunx vexpo resend`. Resend's dashboard shows them and verifies. We don't automate this. |
113
+ | Phase | What it needs from you |
114
+ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
115
+ | `npx vexpo apple asc-key` | App Store Connect API key (issuer ID, key ID, .p8), created at [appstoreconnect.apple.com/access/integrations/api](https://appstoreconnect.apple.com/access/integrations/api) |
116
+ | `npx vexpo apple jwt` | Sign In with Apple key (key ID, .p8), created at [developer.apple.com/account/resources/authkeys/list](https://developer.apple.com/account/resources/authkeys/list) |
117
+ | DNS records | Added by you at your registrar after `npx vexpo resend`. Resend's dashboard shows them and verifies. We don't automate this. |
118
118
 
119
- Skip the whole phase: `bunx vexpo full # (accounts walk only runs with --new)`.
119
+ Skip the whole phase: `npx vexpo full # (accounts walk only runs with --new)`.
120
120
 
121
- ## Phase 1: Rebrand (`bunx vexpo rebrand`)
121
+ ## Phase 1: Rebrand (`npx vexpo rebrand`)
122
122
 
123
123
  Interactive wizard for forks. Detects template defaults like `com.example.vexpo`, `slug: "vexpo"`, `scheme: "vexpo"`. Prompts for:
124
124
 
@@ -140,9 +140,9 @@ Edits:
140
140
 
141
141
  Backups land in `.rebrand-backup/<timestamp>/` before any write. Idempotent: re-runs detect "already rebranded" via state and skip unless `--force` is passed.
142
142
 
143
- Skip: `bunx vexpo full --skip-rebrand`. Or pre-detect by reading from `.env.local` (`EXPO_PUBLIC_APP_BUNDLE_ID`). If it differs from `com.example.vexpo`, the orchestrator marks the step `cached`.
143
+ Skip: `npx vexpo full --skip-rebrand`. Or pre-detect by reading from `.env.local` (`EXPO_PUBLIC_APP_BUNDLE_ID`). If it differs from `com.example.vexpo`, the orchestrator marks the step `cached`.
144
144
 
145
- ## Phase 2: Convex (`bunx vexpo convex`)
145
+ ## Phase 2: Convex (`npx vexpo convex`)
146
146
 
147
147
  Provisions a fresh Convex deployment (or connects to an existing one). Writes:
148
148
 
@@ -151,15 +151,15 @@ Provisions a fresh Convex deployment (or connects to an existing one). Writes:
151
151
 
152
152
  Prompts for the iOS bundle ID (reverse DNS, e.g. `com.yourname.myapp`) and your 10-character Apple Team ID (in Apple Developer → Membership). Both can be provided non-interactively via `EXPO_PUBLIC_APP_BUNDLE_ID` and `EXPO_PUBLIC_APPLE_TEAM_ID` env vars.
153
153
 
154
- `--fresh` wipes `.env.local` and reprovisions a brand new deployment. `--local` runs against `bunx convex dev --local` (self-hosted backend).
154
+ `--fresh` wipes `.env.local` and reprovisions a brand new deployment. `--local` runs against `npx convex dev --local` (self-hosted backend).
155
155
 
156
- ## Phase 3: Better Auth (`bunx vexpo better-auth`)
156
+ ## Phase 3: Better Auth (`npx vexpo better-auth`)
157
157
 
158
158
  Generates a 32-byte base64 `BETTER_AUTH_SECRET`. Sets `SITE_URL`, `APP_NAME` on Convex. No prompts, no env required.
159
159
 
160
160
  If `BETTER_AUTH_SECRET` is already set on Convex, the script preserves it.
161
161
 
162
- ## Phase 4: Resend (`bunx vexpo resend`)
162
+ ## Phase 4: Resend (`npx vexpo resend`)
163
163
 
164
164
  Prompts once for a Resend full-access key (or reads `RESEND_FULL_ACCESS_KEY`). Picks a verified domain (or the first one if there's only one). Creates:
165
165
 
@@ -180,7 +180,7 @@ This is the one thing that gates real email sending. Until the domain is verifie
180
180
 
181
181
  The webhook subscribes to 9 events: `email.sent`, `email.delivered`, `email.delivery_delayed`, `email.bounced`, `email.complained`, `email.failed`, `email.suppressed`, `email.opened`, `email.clicked`. The 4 actionable failure events (`bounced`, `complained`, `suppressed`, `failed`) tell you when a user's address is dead. `convex/email.ts` logs them with `console.warn` and you extend `handleEmailEvent` to flag the user account if you want to stop retrying. `opened` and `clicked` only fire when per-email tracking is enabled (we don't enable it by default, toggle on individual sends if you want it).
182
182
 
183
- `bunx vexpo doctor` confirms the webhook subscription includes all 4 actionable events. If you ever drop one accidentally, re-run `bunx vexpo resend` to refresh.
183
+ `npx vexpo doctor` confirms the webhook subscription includes all 4 actionable events. If you ever drop one accidentally, re-run `npx vexpo resend` to refresh.
184
184
 
185
185
  ### Sign In with Apple + Hide My Email (Apple Private Email Relay)
186
186
 
@@ -193,7 +193,7 @@ Resend authenticates via SPF + DKIM by default, which is what Apple wants. So on
193
193
 
194
194
  Apple imposes a 100/day limit per relay address, but that's a per-user cap, not a per-app one.
195
195
 
196
- ## Phase 5: Review account (`bunx vexpo review-account`)
196
+ ## Phase 5: Review account (`npx vexpo review-account`)
197
197
 
198
198
  Reads `apple.review.demoUsername` / `demoPassword` from `store.config.json`. Creates the user via Better Auth's signup flow, then flips `emailVerified: true` directly via the adapter so Apple's reviewer doesn't see an OTP prompt.
199
199
 
@@ -201,13 +201,18 @@ Pass `--email` / `--password` to override the values from `store.config.json`. S
201
201
 
202
202
  ## Phase 6: EAS (auto, no standalone command, runs as part of `vexpo full`)
203
203
 
204
- Runs `eas init` (creates the project, or links to an existing one) and writes `extra.eas.projectId` to `app.json`. Mirrors every `EXPO_PUBLIC_*` from `.env.local` to EAS env across `production`, `preview`, and `development` environments using `bunx eas env:create --visibility plaintext`.
205
-
206
- Pass `--skip-init` to only mirror env, `--skip-env` to only init.
204
+ Runs `eas init` (creates the project, or links to an existing one) and writes `extra.eas.projectId` to `app.json`. Mirrors every `EXPO_PUBLIC_*` from `.env.local` to the EAS `development` environment using `npx eas env:create --visibility plaintext`. Prod and preview values come from `.env.prod` via `vexpo env push`, which routes to `["production", "preview"]`.
207
205
 
208
206
  After this, `expo prebuild` and `eas build` both find the right project + env. The `extra.eas.projectId` write also enables `app.config.ts → updates.url`.
209
207
 
210
- ## Phase 7: ASC API key (`bunx vexpo apple asc-key`)
208
+ The `fingerprint` runtime version policy hashes the resolved config, so `projectId` must resolve to the same value on your machine and on the EAS worker. Otherwise `extra.eas`/`updates.url`/`updates.enabled` diverge and the build fails at `CONFIGURE_EXPO_UPDATES`. Two ways to get that parity:
209
+
210
+ - **Commit `app.json`** (simplest, for a real app): after `eas init` writes `extra.eas.projectId`, commit `app.json`. EAS uploads it, so the worker reads the same projectId you do. No env var, nothing else.
211
+ - **Plaintext `EAS_PROJECT_ID` env var**: for a fork that keeps `app.json` a stub, or CI that can't commit a projectId (this is how this template repo itself builds). `npx eas env:create --name EAS_PROJECT_ID --value <uuid> --visibility plaintext --environment development --environment preview --environment production`. Plaintext vars are injected before the worker evaluates `app.config.ts`, so it resolves the same projectId. Don't `.gitignore` `app.json`, that strips the worker's copy and forces this path.
212
+
213
+ Either way the template needs no `fingerprint.config.js` and no `sourceSkips`.
214
+
215
+ ## Phase 7: ASC API key (`npx vexpo apple asc-key`)
211
216
 
212
217
  The App Store Connect API key is needed for `eas submit` (and for vexpo's Phase 8 Services ID provisioning). The first key has to be created in the ASC web UI, there's no bootstrap path because you can't authenticate the API without already having a key.
213
218
 
@@ -222,13 +227,13 @@ Then prompts for issuer ID, key ID, and `.p8` path. Validates by signing an ES25
222
227
 
223
228
  Records `{issuerId, keyId, p8Path, validatedAt}` in `.setup-state.json`. The .p8 file itself stays where you put it, vexpo never copies it.
224
229
 
225
- Env-var skip: `APPLE_ASC_ISSUER_ID=... APPLE_ASC_KEY_ID=... APPLE_ASC_P8_PATH=/path/to/AuthKey_X.p8 bunx vexpo apple asc-key`.
230
+ Env-var skip: `APPLE_ASC_ISSUER_ID=... APPLE_ASC_KEY_ID=... APPLE_ASC_P8_PATH=/path/to/AuthKey_X.p8 npx vexpo apple asc-key`.
226
231
 
227
- Re-validate cached creds without re-prompting: `bunx vexpo apple asc-key --revalidate`.
232
+ Re-validate cached creds without re-prompting: `npx vexpo apple asc-key --revalidate`.
228
233
 
229
- ## Phase 7.5: EAS iOS credentials (`bunx vexpo apple credentials`)
234
+ ## Phase 7.5: EAS iOS credentials (`npx vexpo apple credentials`)
230
235
 
231
- `bunx vexpo apple credentials` wraps the eas-cli wizard. With our env-var pre-passing (`EXPO_ASC_API_KEY_PATH`, `EXPO_ASC_KEY_ID`, `EXPO_ASC_ISSUER_ID`), the wizard skips Apple Developer login entirely. You walk through ~6 Y/n prompts (each "Generate new" or "Use existing"), each takes 1-2 seconds. Apple's API does the actual work server-side.
236
+ `npx vexpo apple credentials` wraps the eas-cli wizard. With our env-var pre-passing (`EXPO_ASC_API_KEY_PATH`, `EXPO_ASC_KEY_ID`, `EXPO_ASC_ISSUER_ID`), the wizard skips Apple Developer login entirely. You walk through ~6 Y/n prompts (each "Generate new" or "Use existing"), each takes 1-2 seconds. Apple's API does the actual work server-side.
232
237
 
233
238
  What the wizard sets up:
234
239
 
@@ -239,11 +244,11 @@ What the wizard sets up:
239
244
 
240
245
  All credentials are stored encrypted on EAS infrastructure. Subsequent `eas build` + `eas submit` runs are non-interactive.
241
246
 
242
- Standalone: `bunx vexpo apple credentials [-e <profile>]`.
247
+ Standalone: `npx vexpo apple credentials [-e <profile>]`.
243
248
 
244
- Bypass entirely: `bunx eas credentials -p ios` runs the wizard directly. The vexpo wrapper just pre-passes the cached ASC creds.
249
+ Bypass entirely: `npx eas credentials -p ios` runs the wizard directly. The vexpo wrapper just pre-passes the cached ASC creds.
245
250
 
246
- ## Phase 8: Sign In with Apple Services ID (`bunx vexpo apple services-id`)
251
+ ## Phase 8: Sign In with Apple Services ID (`npx vexpo apple services-id`)
247
252
 
248
253
  The Services ID is a separate `BundleId` resource (with `platform: "SERVICES"`) used by your backend to identify the OAuth client to Apple. EAS doesn't manage these. The regular bundle ID it provisions is for the iOS app itself, not the OAuth backend.
249
254
 
@@ -282,7 +287,7 @@ Apple may ask you to upload an `apple-developer-domain-association.txt` to verif
282
287
 
283
288
  After saving, return to the CLI and press Enter. The CLI re-lists, finds the Services ID, attaches the capability via API, and continues to Phase 9.
284
289
 
285
- ## Phase 9: Apple Sign In JWT (`bunx vexpo apple jwt`)
290
+ ## Phase 9: Apple Sign In JWT (`npx vexpo apple jwt`)
286
291
 
287
292
  Signs an ES256 `client_secret` JWT (180-day expiry, Apple's max) from a Sign In with Apple `.p8` file. Writes:
288
293
 
@@ -299,15 +304,15 @@ Prompts:
299
304
 
300
305
  Records `{servicesId, teamId, keyId, p8Path, signedAt, expiresAt}` in state.
301
306
 
302
- Env-var skip: `APPLE_SERVICES_ID=... APPLE_TEAM_ID=... APPLE_KEY_ID=... APPLE_P8_PATH=/path/to/AuthKey_X.p8 bunx vexpo apple jwt`.
307
+ Env-var skip: `APPLE_SERVICES_ID=... APPLE_TEAM_ID=... APPLE_KEY_ID=... APPLE_P8_PATH=/path/to/AuthKey_X.p8 npx vexpo apple jwt`.
303
308
 
304
- Rotate without re-prompting IDs: `bunx vexpo apple jwt --rotate`.
309
+ Rotate without re-prompting IDs: `npx vexpo apple jwt --rotate`.
305
310
 
306
311
  ### JWT rotation
307
312
 
308
- Apple caps `client_secret` JWTs at 180 days. The `.eas/workflows/rotate-apple-jwt.yml` cron fires every 90 days, signs a fresh JWT, and pushes it to your prod Convex deployment. Runs on EAS infrastructure with all secrets read from EAS env (production, secret visibility), no GitHub repo secrets needed. Set up once, never think about it again. Manual fallback: `bunx vexpo apple jwt --rotate`.
313
+ Apple caps `client_secret` JWTs at 180 days. The `.eas/workflows/rotate-apple-jwt.yml` cron fires every 90 days, signs a fresh JWT, and pushes it to your prod Convex deployment. Runs on EAS infrastructure with all secrets read from EAS env (production, secret visibility), no GitHub repo secrets needed. Set up once, never think about it again. Manual fallback: `npx vexpo apple jwt --rotate`.
309
314
 
310
- ## Phase 10: EAS rotation secrets (`bunx vexpo apple eas-rotation-secrets`)
315
+ ## Phase 10: EAS rotation secrets (`npx vexpo apple eas-rotation-secrets`)
311
316
 
312
317
  Pushes the 5 EAS production secrets the rotation cron needs. The orchestrator runs this last. It's also a standalone command.
313
318
 
@@ -322,8 +327,8 @@ Pushes the 5 EAS production secrets the rotation cron needs. The orchestrator ru
322
327
  The 4 Apple secrets get pulled automatically. `CONVEX_DEPLOY_KEY` is prompted because the CLI can't generate Convex deploy keys, you create it once in the Convex dashboard and paste it back.
323
328
 
324
329
  ```bash
325
- bunx vexpo apple eas-rotation-secrets # interactive
326
- bunx vexpo apple eas-rotation-secrets --force # overwrite existing values
330
+ npx vexpo apple eas-rotation-secrets # interactive
331
+ npx vexpo apple eas-rotation-secrets --force # overwrite existing values
327
332
  ```
328
333
 
329
334
  If you'd rather run the raw `eas env:create` calls yourself:
@@ -336,37 +341,37 @@ eas env:create --name APPLE_SERVICES_ID --value <value> --
336
341
  eas env:create --name CONVEX_DEPLOY_KEY --value <prod-deploy-key> --environment production --visibility secret
337
342
  ```
338
343
 
339
- `bunx vexpo doctor --channel prod` lists which of the 5 are present (names appear, values stay opaque since they're secret visibility).
344
+ `npx vexpo doctor --channel prod` lists which of the 5 are present (names appear, values stay opaque since they're secret visibility).
340
345
 
341
- ## What `bunx vexpo full` does NOT do
346
+ ## What `npx vexpo full` does NOT do
342
347
 
343
348
  These are explicit non-goals, EAS or third parties already handle them well:
344
349
 
345
- - **iOS distribution cert / provisioning profile / push notification key (.p8)** are EAS-owned. We wrap the `eas credentials -p ios` wizard via `bunx vexpo apple credentials` so the orchestrator records that it ran, but eas-cli does the work.
350
+ - **iOS distribution cert / provisioning profile / push notification key (.p8)** are EAS-owned. We wrap the `eas credentials -p ios` wizard via `npx vexpo apple credentials` so the orchestrator records that it ran, but eas-cli does the work.
346
351
  - **iOS bundle ID for the app**, EAS auto-creates on first `eas credentials -p ios` if it doesn't exist.
347
352
  - **iOS capability sync**, EAS auto-syncs from `ios.entitlements` (which `app.config.ts` populates from `usesAppleSignIn: true`, `expo-notifications`, `associatedDomains`, etc.) on every `eas build`.
348
353
  - **App Store Connect app record**, `eas submit` auto-creates on first run from `app.config.ts → name` + `package.json → name`.
349
354
  - **Apple Developer account creation**, manual signup, $99/yr, identity verification, 2FA.
350
355
 
351
- ## Lite-mode env sync (`bunx vexpo env push`)
356
+ ## Lite-mode env sync (`npx vexpo env push`)
352
357
 
353
358
  ```bash
354
- bunx vexpo env push # interactive (per-file confirm)
355
- bunx vexpo env push --force # overwrite without prompting
356
- bunx vexpo env push --dry-run # show plan, don't apply
357
- bunx vexpo env push --local-file foo # override .env.local path
358
- bunx vexpo env push --prod-file foo # override .env.prod path
359
+ npx vexpo env push # interactive (per-file confirm)
360
+ npx vexpo env push --force # overwrite without prompting
361
+ npx vexpo env push --dry-run # show plan, don't apply
362
+ npx vexpo env push --local-file foo # override .env.local path
363
+ npx vexpo env push --prod-file foo # override .env.prod path
359
364
  ```
360
365
 
361
366
  Lite mode reads source files and pushes values to remote destinations. Zero provisioning, no API calls beyond `convex env set --from-file` and `eas env:push --path`.
362
367
 
363
368
  ### Source files
364
369
 
365
- | File | Channel | Default destinations |
366
- | ----------------- | ------- | ------------------------------------------------------- |
367
- | `.env.local` | dev | Convex dev env, EAS development env |
368
- | `.env.prod` | prod | Convex prod env, EAS production+preview, GitHub secrets |
369
- | `.env.production` | prod | (used if `.env.prod` is absent) |
370
+ | File | Channel | Default destinations |
371
+ | ----------------- | ------- | --------------------------------------- |
372
+ | `.env.local` | dev | Convex dev env, EAS development env |
373
+ | `.env.prod` | prod | Convex prod env, EAS production+preview |
374
+ | `.env.production` | prod | (used if `.env.prod` is absent) |
370
375
 
371
376
  Override paths with `--local-file` / `--prod-file`. Both files are optional, lite mode runs with whatever it finds.
372
377
 
@@ -374,21 +379,23 @@ Override paths with `--local-file` / `--prod-file`. Both files are optional, lit
374
379
 
375
380
  Each known env-var has a fixed routing in `vexpo`'s env-files module ([source](https://github.com/ramonclaudio/vexpo/blob/main/packages/vexpo/src/lib/env-files.ts)):
376
381
 
377
- | Source key | Convex (dev) | Convex (prod) | EAS env | GitHub secret |
378
- | --------------------------------------- | ----------------- | ----------------- | --------------------- | ------------------------------- |
379
- | `EXPO_PUBLIC_*` | n/a | n/a | dev (or prod+preview) | n/a |
380
- | `BETTER_AUTH_SECRET` | dev | prod | n/a | n/a |
381
- | `RESEND_API_KEY` | dev | prod | n/a | n/a |
382
- | `RESEND_WEBHOOK_SECRET` | dev | prod | n/a | n/a |
383
- | `RESEND_TEST_MODE`, `EMAIL_FROM` | dev | prod | n/a | n/a |
384
- | `APP_NAME`, `SITE_URL`, `APP_BUNDLE_ID` | dev | prod | n/a | n/a |
385
- | `APPLE_CLIENT_ID` | dev | prod | n/a | n/a |
386
- | `APPLE_CLIENT_SECRET` | dev | prod | n/a | n/a |
387
- | `APPLE_TEAM_ID` | dev | prod | n/a | prod only |
388
- | `APPLE_KEY_ID` | dev | prod | n/a | prod only |
389
- | `APPLE_SERVICES_ID` | `APPLE_CLIENT_ID` | `APPLE_CLIENT_ID` | n/a | `APPLE_SERVICES_ID` (prod only) |
390
- | `APPLE_P8_PRIVATE_KEY` | n/a | n/a | n/a | prod only |
391
- | `CONVEX_DEPLOY_KEY` | n/a | n/a | n/a | prod only |
382
+ | Source key | Convex (dev) | Convex (prod) | EAS env |
383
+ | --------------------------------------- | ----------------- | ----------------- | --------------------- |
384
+ | `EXPO_PUBLIC_*` | n/a | n/a | dev (or prod+preview) |
385
+ | `BETTER_AUTH_SECRET` | dev | prod | n/a |
386
+ | `RESEND_API_KEY` | dev | prod | n/a |
387
+ | `RESEND_WEBHOOK_SECRET` | dev | prod | n/a |
388
+ | `RESEND_TEST_MODE`, `EMAIL_FROM` | dev | prod | n/a |
389
+ | `APP_NAME`, `SITE_URL`, `APP_BUNDLE_ID` | dev | prod | n/a |
390
+ | `APPLE_CLIENT_ID` | dev | prod | n/a |
391
+ | `APPLE_CLIENT_SECRET` | dev | prod | n/a |
392
+ | `APPLE_TEAM_ID` | dev | prod | n/a |
393
+ | `APPLE_KEY_ID` | dev | prod | n/a |
394
+ | `APPLE_SERVICES_ID` | `APPLE_CLIENT_ID` | `APPLE_CLIENT_ID` | n/a |
395
+ | `APPLE_P8_PRIVATE_KEY` | n/a | n/a | n/a |
396
+ | `CONVEX_DEPLOY_KEY` | n/a | n/a | n/a |
397
+
398
+ The five rotation-cron secrets (`APPLE_TEAM_ID`, `APPLE_KEY_ID`, `APPLE_SERVICES_ID`, `APPLE_P8_PRIVATE_KEY`, `CONVEX_DEPLOY_KEY`) live in EAS env at `secret` visibility but `vexpo env push` does not write them — run `npx vexpo apple eas-rotation-secrets` once to push the full set.
392
399
 
393
400
  Notes:
394
401
 
@@ -415,7 +422,7 @@ Secret-visibility EAS env vars (`APPLE_P8_PRIVATE_KEY`, `CONVEX_DEPLOY_KEY`) are
415
422
  ### When NOT to use lite mode
416
423
 
417
424
  - You don't have all the values yet. Lite mode doesn't generate `BETTER_AUTH_SECRET`, doesn't sign Apple JWTs, doesn't create Resend keys. Use full mode for first setup.
418
- - You haven't run `eas init` yet. Lite mode pushes to EAS env but won't init the project, run full mode or `bunx eas init && bunx eas env:push --path .env.local` once first.
425
+ - You haven't run `eas init` yet. Lite mode pushes to EAS env but won't init the project, run full mode or `npx eas init && npx eas env:push --path .env.local` once first.
419
426
 
420
427
  ### Example flow
421
428
 
@@ -423,27 +430,27 @@ Move a working app to a new machine:
423
430
 
424
431
  ```bash
425
432
  # On the old machine:
426
- bunx convex env list > /tmp/dev-env
427
- bunx convex env list --prod > /tmp/prod-env
433
+ npx convex env list > /tmp/dev-env
434
+ npx convex env list --prod > /tmp/prod-env
428
435
  # Edit each into .env.local / .env.prod with the values you want carried over.
429
436
 
430
437
  # On the new machine:
431
438
  git clone <repo>
432
439
  cd <repo>
433
- bun install
434
- bunx vexpo env push # syncs from those files
435
- bunx eas credentials -p ios # re-uploads cert / profile / keys
436
- bun run convex:dev
437
- bun run ios
440
+ npm install
441
+ npx vexpo env push # syncs from those files
442
+ npx eas credentials -p ios # re-uploads cert / profile / keys
443
+ npm run convex:dev
444
+ npm run ios
438
445
  ```
439
446
 
440
- ## Verification (`bunx vexpo doctor`)
447
+ ## Verification (`npx vexpo doctor`)
441
448
 
442
449
  ```bash
443
- bunx vexpo doctor # verify dev (default)
444
- bunx vexpo doctor --channel prod # verify prod (Convex --prod env)
445
- bunx vexpo doctor --json # machine-readable output
446
- bunx vexpo doctor --strict # exit non-zero on warnings
450
+ npx vexpo doctor # verify dev (default)
451
+ npx vexpo doctor --channel prod # verify prod (Convex --prod env)
452
+ npx vexpo doctor --json # machine-readable output
453
+ npx vexpo doctor --strict # exit non-zero on warnings
447
454
  ```
448
455
 
449
456
  Runs a battery of checks that auth-test each credential and cross-reference the values across `.env.local`, Convex env, EAS env, GitHub secrets, and `app.config.ts`. Lite mode runs the same battery automatically after sync (skip with `--no-verify`). Results are grouped by category:
@@ -464,38 +471,38 @@ Each check has a severity:
464
471
  - `fail`, broken (JWT expired, bundle ID mismatch between local and Convex, Resend API key rejected).
465
472
  - `skip`, can't be checked (no `.env.prod`, ASC creds not cached, EAS not signed in).
466
473
 
467
- Exit status: `0` for ok+warn, `1` if any fail, `1` for warn under `--strict`. Run after every `bunx vexpo env push` to confirm nothing drifted, in CI to catch credential rotation issues, or after a `bunx vexpo apple jwt --rotate` to confirm the new JWT is signed correctly.
474
+ Exit status: `0` for ok+warn, `1` if any fail, `1` for warn under `--strict`. Run after every `npx vexpo env push` to confirm nothing drifted, in CI to catch credential rotation issues, or after a `npx vexpo apple jwt --rotate` to confirm the new JWT is signed correctly.
468
475
 
469
476
  The check that catches the most real-world bugs: `apple/jwt-iss-matches`. Apple JWTs are easy to sign with the wrong Team ID, happens when you reuse a `.p8` from another project. Verify catches it instantly.
470
477
 
471
478
  ## Iterating: post-setup commands
472
479
 
473
480
  ```bash
474
- bun run convex:dev # T1: Convex functions
475
- bun run ios # T2: prebuild + simulator
476
-
477
- bunx eas build -p ios --profile production # production iOS build
478
- bunx eas submit -p ios --profile production # auto-creates the App Store record on first run
479
- bun run eas:tf # build + auto-submit to TestFlight
480
- bunx eas metadata:push # push store.config.json to App Store Connect
481
- bunx eas credentials -p ios # manage iOS dist cert / profile / keys
481
+ npm run convex:dev # T1: Convex functions
482
+ npm run ios # T2: prebuild + simulator
483
+
484
+ npx eas build -p ios --profile production # production iOS build
485
+ npx eas submit -p ios --profile production # auto-creates the App Store record on first run
486
+ npm run eas:tf # build + auto-submit to TestFlight
487
+ npx eas metadata:push # push store.config.json to App Store Connect
488
+ npx eas credentials -p ios # manage iOS dist cert / profile / keys
482
489
  ```
483
490
 
484
491
  EAS Workflows (`.eas/workflows/`) automate these for you on push to `main`, push tag `v*`, push to `beta/*`, on PR, and on `eas workflow:run`.
485
492
 
486
493
  ## Recovery: rotating things
487
494
 
488
- | Thing | Command |
489
- | -------------------------- | ------------------------------------------------------------------ |
490
- | Convex deployment | `bunx vexpo full --fresh` |
491
- | Better Auth secret | `bunx vexpo better-auth --force` |
492
- | Resend key + webhook | `bunx vexpo resend` |
493
- | Apple Sign In JWT (manual) | `bunx vexpo apple jwt --rotate` |
494
- | Apple Sign In JWT (auto) | EAS Workflows → `rotate-apple-jwt` → Run |
495
- | ASC API key | `bunx vexpo apple asc-key` (re-runs validation) |
496
- | EAS env mirror | `bunx eas init && bunx eas env:push --path .env.local --skip-init` |
497
- | EAS rotation secrets | `bunx vexpo apple eas-rotation-secrets --force` |
498
- | State cache | `trash .setup-state.json` |
495
+ | Thing | Command |
496
+ | -------------------------- | ---------------------------------------------------------------- |
497
+ | Convex deployment | `npx vexpo full --fresh` |
498
+ | Better Auth secret | `npx vexpo better-auth --force` |
499
+ | Resend key + webhook | `npx vexpo resend` |
500
+ | Apple Sign In JWT (manual) | `npx vexpo apple jwt --rotate` |
501
+ | Apple Sign In JWT (auto) | EAS Workflows → `rotate-apple-jwt` → Run |
502
+ | ASC API key | `npx vexpo apple asc-key` (re-runs validation) |
503
+ | EAS env mirror | `npx eas init && npx eas env:push --path .env.local --skip-init` |
504
+ | EAS rotation secrets | `npx vexpo apple eas-rotation-secrets --force` |
505
+ | State cache | `trash .setup-state.json` |
499
506
 
500
507
  ## Recovery: things break
501
508
 
@@ -503,20 +510,20 @@ EAS Workflows (`.eas/workflows/`) automate these for you on push to `main`, push
503
510
 
504
511
  ```
505
512
  trash .setup-state.json
506
- bunx vexpo full --no-state # full live re-probe
513
+ npx vexpo full --no-state # full live re-probe
507
514
  ```
508
515
 
509
516
  `.env.local` deleted, but Convex deployment still exists:
510
517
 
511
518
  ```
512
- bunx eas env:pull --environment development # pulls EXPO_PUBLIC_*
519
+ npx eas env:pull --environment development # pulls EXPO_PUBLIC_*
513
520
  echo "CONVEX_DEPLOYMENT=dev:happy-frog-123" >> .env.local
514
521
  ```
515
522
 
516
- `bun run ios` fails with provisioning profile errors:
523
+ `npm run ios` fails with provisioning profile errors:
517
524
 
518
525
  ```
519
- bunx eas credentials -p ios # interactive wizard, regenerate cert/profile
526
+ npx eas credentials -p ios # interactive wizard, regenerate cert/profile
520
527
  ```
521
528
 
522
529
  Resend domain unverified or DNS records changed:
@@ -526,8 +533,8 @@ Open `https://resend.com/domains/<id>`. Resend shows what's missing. Add the rec
526
533
  ASC API key revoked or replaced:
527
534
 
528
535
  ```
529
- bunx vexpo apple asc-key # validates cached, prompts for new on failure
530
- bunx eas credentials -p ios # re-upload to EAS
536
+ npx vexpo apple asc-key # validates cached, prompts for new on failure
537
+ npx eas credentials -p ios # re-upload to EAS
531
538
  ```
532
539
 
533
540
  ## CI
@@ -535,8 +542,8 @@ bunx eas credentials -p ios # re-upload to EAS
535
542
  Use `--no-state` to ignore the local state cache. Provide every interactive value via env. The orchestrator runs in non-TTY mode and skips any step that would prompt without env-var fallbacks.
536
543
 
537
544
  ```yaml
538
- - run: bun install
539
- - run: bunx vexpo full --no-state --skip-rebrand
545
+ - run: npm install
546
+ - run: npx vexpo full --no-state --skip-rebrand
540
547
  env:
541
548
  EXPO_PUBLIC_APP_BUNDLE_ID: com.yourname.myapp
542
549
  EXPO_PUBLIC_APPLE_TEAM_ID: ABCDE12345
@@ -553,16 +560,16 @@ Use `--no-state` to ignore the local state cache. Provide every interactive valu
553
560
 
554
561
  ## Files
555
562
 
556
- | Path | Purpose | Source of truth |
557
- | ----------------------------------- | ------------------------------------------------------ | ---------------------------------------------------------------------------------- |
558
- | `.env.local` | Public env (build-time + Convex URL) | written by setup |
559
- | `.setup-state.json` | Per-step verifyAt cache (gitignored) | written by setup, read by orchestrator |
560
- | Convex env (`bunx convex env list`) | Server-side secrets | written by setup |
561
- | EAS env (`bunx eas env:list`) | Build-time env per environment + rotation cron secrets | written by the EAS phase of `vexpo full` + `bunx vexpo apple eas-rotation-secrets` |
562
- | `app.config.ts` | Expo app config (reads `.env.local`) | edited by rebrand |
563
- | `app.json` | Static `eas.projectId` | written by `eas init` |
564
- | `store.config.json` | App Store metadata + review contact | edited by rebrand, gitignored |
565
- | `package.json` | Project metadata | edited by rebrand |
563
+ | Path | Purpose | Source of truth |
564
+ | ---------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------- |
565
+ | `.env.local` | Public env (build-time + Convex URL) | written by setup |
566
+ | `.setup-state.json` | Per-step verifyAt cache (gitignored) | written by setup, read by orchestrator |
567
+ | Convex env (`npx convex env list`) | Server-side secrets | written by setup |
568
+ | EAS env (`npx eas env:list`) | Build-time env per environment + rotation cron secrets | written by the EAS phase of `vexpo full` + `npx vexpo apple eas-rotation-secrets` |
569
+ | `app.config.ts` | Expo app config (reads `.env.local`) | edited by rebrand |
570
+ | `app.json` | Static `eas.projectId` | written by `eas init` |
571
+ | `store.config.json` | App Store metadata + review contact | edited by rebrand, gitignored |
572
+ | `package.json` | Project metadata | edited by rebrand |
566
573
 
567
574
  ## State schema
568
575
 
@@ -605,8 +612,8 @@ Atomic writes via `tmp + rename`. Schema mismatches fail-loud, `setup` will refu
605
612
 
606
613
  | Class | Lives in | Rotates |
607
614
  | --------------------- | -------------------------------------------------------------- | --------------------------------------------------------- |
608
- | `BETTER_AUTH_SECRET` | Convex env | rotate via `bunx vexpo better-auth --force` |
609
- | Resend sending key | Convex env (`RESEND_API_KEY`) | `bunx vexpo resend` deletes the named key + recreates |
615
+ | `BETTER_AUTH_SECRET` | Convex env | rotate via `npx vexpo better-auth --force` |
616
+ | Resend sending key | Convex env (`RESEND_API_KEY`) | `npx vexpo resend` deletes the named key + recreates |
610
617
  | Resend webhook secret | Convex env (`RESEND_WEBHOOK_SECRET`) | rotated alongside the key |
611
618
  | Apple Sign In JWT | Convex env (`APPLE_CLIENT_SECRET`) | 180-day max, auto-rotated by EAS Workflows cron every 90d |
612
619
  | Apple Sign In `.p8` | EAS env `APPLE_P8_PRIVATE_KEY` (secret visibility, production) | rotate the key in Apple Developer Console |