@ramonclaudio/create-vexpo 0.1.3 → 0.1.5

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 (36) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +29 -15
  3. package/dist/index.js +44 -14
  4. package/dist/templates/default/.eas/workflows/e2e-tests.yml +17 -1
  5. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +3 -3
  6. package/dist/templates/default/.maestro/auth.yaml +229 -0
  7. package/dist/templates/default/.maestro/launch.yaml +5 -5
  8. package/dist/templates/default/.maestro/tour.yaml +294 -0
  9. package/dist/templates/default/.maestro/zz-delete-restore.yaml +174 -0
  10. package/dist/templates/default/AGENTS.md +3 -2
  11. package/dist/templates/default/DESIGN.md +41 -41
  12. package/dist/templates/default/README.md +46 -40
  13. package/dist/templates/default/SETUP.md +34 -19
  14. package/dist/templates/default/_easignore +0 -1
  15. package/dist/templates/default/_env.example +15 -10
  16. package/dist/templates/default/app.config.ts +5 -5
  17. package/dist/templates/default/convex/pushTokens.ts +1 -26
  18. package/dist/templates/default/convex/rateLimit.ts +1 -21
  19. package/dist/templates/default/convex/users.ts +1 -49
  20. package/dist/templates/default/convex/validators.ts +0 -10
  21. package/dist/templates/default/package.json +1 -1
  22. package/dist/templates/default/scripts/README.md +24 -8
  23. package/dist/templates/default/scripts/clean.ts +3 -3
  24. package/dist/templates/default/scripts/gen-update-cert.mjs +3 -1
  25. package/dist/templates/default/src/app/(app)/_layout.tsx +15 -1
  26. package/dist/templates/default/src/app/(app)/auth/forgot-password.tsx +3 -0
  27. package/dist/templates/default/src/app/(app)/auth/reset-password.tsx +3 -0
  28. package/dist/templates/default/src/app/(app)/auth/sign-up.tsx +3 -1
  29. package/dist/templates/default/src/app/(app)/privacy.tsx +3 -2
  30. package/dist/templates/default/src/app/(app)/restore-account.tsx +2 -2
  31. package/dist/templates/default/src/app/(app)/sessions.tsx +15 -5
  32. package/dist/templates/default/src/components/ui/convex-error.tsx +0 -7
  33. package/dist/templates/default/src/constants/ui.ts +0 -11
  34. package/dist/templates/default/src/lib/dev-menu.ts +11 -2
  35. package/dist/templates/default/src/lib/preferences.ts +9 -0
  36. package/package.json +3 -2
@@ -1,11 +1,22 @@
1
1
  # vexpo
2
2
 
3
- Expo SDK 56 + Convex + Better Auth + Resend, wired end-to-end for iOS. Native SwiftUI via `@expo/ui/swift-ui`, email + password + email OTP + Apple Sign In, APNs push, Universal Links, profile + active sessions with avatar uploads and device-by-device revocation. EAS for the whole build surface: 10 workflows, fingerprint-gated OTA-or-build, TestFlight, rollback, rollout, ASC events, and the Apple Sign In JWT rotation cron.
3
+ An iOS app on Expo SDK 56, wired end-to-end with Convex, Better Auth, and Resend. Native SwiftUI throughout, email + password + OTP + Apple Sign In, push, and the full EAS build surface. Everything below is already wired, so you run two commands and you're in the app.
4
4
 
5
- A lot of the SwiftUI modifiers the template reaches for, `clipShape("capsule")`, `defaultScrollAnchorForRole`, `scrollTargetBehavior`, `scrollPosition`, `textInputAutocapitalization`, `textContentType`, the `Alert` component, the Dynamic Type pair (`textStyle` scaling on `font`, `dynamicTypeSize` bounds), `accessibilityHidden`, are upstream PRs we wrote and got merged into `expo/expo`. Full ledger in [`../../docs/UPSTREAM.md`](../../docs/UPSTREAM.md).
5
+ <p align="center">
6
+ <img src="https://raw.githubusercontent.com/ramonclaudio/vexpo/main/docs/assets/demo-app.gif" width="300" alt="Sign up, onboarding, search, and the dark-mode flip">
7
+ &nbsp;&nbsp;
8
+ </p>
9
+
10
+ <p align="center">
11
+ <img src="https://raw.githubusercontent.com/ramonclaudio/vexpo/main/docs/assets/screens.png" width="600" alt="Home, profile, and settings in light and dark">
12
+ </p>
6
13
 
7
14
  ## Quick start
8
15
 
16
+ Requires macOS and Xcode. This is an iOS-only template, and `npm run ios` builds against the simulator. See [Pre-reqs](#pre-reqs).
17
+
18
+ The `vexpo` CLI ships as a dependency, so `npm install` puts it on your path:
19
+
9
20
  ```bash
10
21
  npm install
11
22
 
@@ -20,44 +31,22 @@ npm run convex:dev # terminal 1
20
31
  npm run ios # terminal 2
21
32
  ```
22
33
 
23
- Lite mode skips Apple / EAS / Resend entirely. `REQUIRE_EMAIL_VERIFICATION` is off on Convex so sign-up auto-verifies, the user lands in the app with one tap, and the UI hides the OTP / password-reset / change-email flows that need Resend to work.
34
+ Lite mode skips Apple, EAS, and Resend. Sign-up auto-verifies and drops you in the app with one tap. The OTP, password-reset, and change-email flows that need Resend stay hidden.
35
+
36
+ ## Ship path
24
37
 
25
38
  When you're ready to ship, swap `lite` for `full`:
26
39
 
27
40
  ```bash
28
41
  npx vexpo full # provisions Resend, Apple Sign In, EAS, rebrand wizard
29
- npx vexpo full --new # same, plus walks Apple / Convex / Expo / Resend signups
42
+ npx vexpo full --new # same, plus walks Apple, Convex, Expo, and Resend signups
30
43
  ```
31
44
 
32
- `full` writes `.env.local`, sets Convex env vars (`REQUIRE_EMAIL_VERIFICATION=true` once Resend is wired), validates the ASC API key, signs the SIWA JWT, runs `eas init` and `eas env:push`, prompts the rebrand wizard. Prints the `eas build` command at the end. vexpo doesn't run it for you, you run `npx eas build -p ios --profile production --auto-submit-with-profile testflight` when you're ready.
33
-
34
- Run `npx vexpo doctor` any time to auth-check every credential against the real service and cross-reference IDs across `.env.local`, Convex env, EAS env, and `app.config.ts`. Catches "wrong .p8 from another project" or ".env.prod copied from a different fork" in seconds.
35
-
36
- Long-form walkthrough with every prompt, every env-var alternative, and recovery paths: [`SETUP.md`](./SETUP.md).
45
+ `full` writes `.env.local`, sets Convex env vars, validates the ASC API key, signs the SIWA JWT, runs `eas init` and `eas env:push`, and prints the `eas build` command at the end. It never runs the build for you.
37
46
 
38
- ## What's wired up
47
+ Run `npx vexpo doctor` any time to auth-check every credential against the real service and cross-reference IDs across `.env.local`, Convex env, EAS env, and `app.config.ts`.
39
48
 
40
- - Convex backend with reactive queries, storage, real-time sync, and `@convex-dev/rate-limiter` on every application mutation. Auth-route rate limits ship via Better Auth at the HTTP layer.
41
- - Better Auth via `@convex-dev/better-auth` (sessions, accounts; per-device revocation via `session.userAgent`)
42
- - App Attest device attestation via `@expo/app-integrity` with server-side verification in Convex
43
- - Resend via `@convex-dev/resend` for OTP, password reset, change-email, with webhook delivery events
44
- - Apple Sign In via Apple's official `AppleAuthenticationButton`, HIG-compliant (BLACK in dark mode, WHITE in light; `WHITE_OUTLINE` isn't used), SIWA Services ID + ES256 JWT signing (180-day expiry, auto-rotated every 90 days)
45
- - APNs push via `expo-notifications` with token registration on sign-in
46
- - Apple Universal Links from Convex's HTTP router (AASA at `/.well-known/apple-app-site-association`)
47
- - Profile editing with avatar uploads to Convex storage
48
- - Active sessions screen with device-by-device revocation
49
- - Account soft-delete with a 30-day grace window and a restore-or-confirm screen on next sign-in
50
- - Pull-to-refresh on home and sessions, plus an interactive update banner on iOS 26
51
- - Theme switching, haptics toggle, reduced motion, VoiceOver labels everywhere (decorative views hidden from the rotor)
52
- - Native Dynamic Type end to end: every label scales with the Larger Text setting via `textStyle`, bounded with `dynamicTypeSize` ceilings on the seven fixed-geometry controls that would clip instead of wrap
53
- - Spotlight-style search tab (debounced, scored, keyword-aware)
54
- - Skeleton placeholders during initial query loads
55
- - Debug screen at `/debug` gated by toggle, off in production by default
56
- - Liquid Glass on iOS 26+ via `expo-glass-effect`, UIVisualEffectView blur fallback on iOS 16.4-25 via `expo-blur`, both behind a `<Material>` primitive
57
- - OTA updates code-signed end-to-end (`expo-updates` code signing; generate the cert with `npm run updates:gen-cert`), so only signed bundles install
58
- - EAS Build / Update / Submit / Metadata. `runtimeVersion: { policy: "fingerprint" }` (auto-bumps on native code changes), branch/channel model, `appVersionSource: "remote"`. ASC API key managed by EAS (`eas credentials -p ios`), no `eas.json` patches. `@expo/fingerprint >= 0.19.3` makes the policy deterministic across machines and CI out of the box, so the earlier `fingerprint.config.js` + `.fingerprintignore` jsi knobs were dropped.
59
- - 10 EAS Workflows under `.eas/workflows/`: dev builds, PR previews with `github-comment` + QR + fingerprint-gated OTA-or-build, production deploy, TestFlight on `beta/*`, manual rollback / rollout, ASC event triggers to Slack, the SIWA JWT rotation cron, Maestro E2E. PR previews, Maestro E2E, and the production deploy are manual-only (`workflow_dispatch`) by default: the first two to conserve EAS build credits (restore their `pull_request` triggers to run on every PR), the deploy so a merge to `main` can't build, submit, and ship an OTA by surprise (add a `push: main` trigger if you want that)
60
- - GitHub Actions for general-purpose checks: typecheck, lint, format, tests, fingerprint diff on PR + push to `main`
49
+ Full walkthrough with every prompt, env-var alternative, and recovery path: [`SETUP.md`](./SETUP.md).
61
50
 
62
51
  ## Pre-reqs
63
52
 
@@ -98,7 +87,7 @@ npm run env:pull eas env:pull --environment development
98
87
  npm run env:pull:prod eas env:pull --environment production
99
88
 
100
89
  npm run clean Trash node_modules, ios, caches, then reinstall
101
- npm run clean:metro Trash Metro/Babel/Haste caches only
90
+ npm run clean:metro Trash Metro/Haste/node-compile caches only
102
91
  npm run clean:state Wipe .setup-state.json + standard clean
103
92
  npm run typecheck tsc --noEmit
104
93
  npm run lint oxlint
@@ -112,7 +101,24 @@ npm run upgrade expo install expo@next && expo install --fix
112
101
  npm run upgrade:stable expo install expo@latest && expo install --fix
113
102
  ```
114
103
 
115
- Setup is one-shot, not a `package.json` script. Run `npx vexpo lite` / `npx vexpo full` / `npx vexpo doctor` directly. All deletions go through `trash` (macOS Trash, recoverable).
104
+ Setup is one-shot, not a `package.json` script. Run `npx vexpo lite`, `npx vexpo full`, or `npx vexpo doctor` directly. All deletions go through `trash` (macOS Trash, recoverable).
105
+
106
+ ## What's wired up
107
+
108
+ - Convex backend, reactive queries, storage, real-time sync, per-mutation rate limiting
109
+ - Better Auth via `@convex-dev/better-auth`, email + password + OTP + Apple Sign In, per-device session revocation
110
+ - App Attest device-attestation primitives ready to wire (client lib + Convex verifier)
111
+ - Resend for OTP, password reset, and change-email, with delivery webhooks
112
+ - APNs push, Apple Universal Links, profile editing with avatar uploads
113
+ - Account soft-delete with a 30-day grace window
114
+ - Theme switching, haptics, reduced motion, VoiceOver, and Dynamic Type end to end
115
+ - Liquid Glass on iOS 26+, with a `UIVisualEffectView` blur fallback on iOS 16.4-25
116
+ - OTA updates code-signed end to end, so only signed bundles install
117
+ - EAS Build, Update, Submit, and Metadata, with ten workflows under `.eas/workflows/`
118
+
119
+ `runtimeVersion` uses the fingerprint policy with `appVersionSource: "remote"`, ASC key managed by EAS. PR previews, Maestro E2E, and the production deploy are `workflow_dispatch`-only by default. Restore the `pull_request` triggers to build on every PR, or add a `push: main` trigger to deploy on merge.
120
+
121
+ For the full feature list, design system, and the upstream PRs behind it, see [`DESIGN.md`](./DESIGN.md) and [`UPSTREAM.md`](https://github.com/ramonclaudio/vexpo/blob/main/docs/UPSTREAM.md).
116
122
 
117
123
  ## Project structure
118
124
 
@@ -128,7 +134,7 @@ src/
128
134
  +not-found.tsx 404 fallback
129
135
  components/ Reusable UI (auth/, ui/)
130
136
  constants/ Theme, layout, UI tokens
131
- hooks/ useNetwork, useTheme, useUpdates, etc.
137
+ hooks/ useNetwork, useColorScheme, useAppUpdates, etc.
132
138
  lib/ Auth client, haptics, env, deep links, native state
133
139
  convex/ Convex backend
134
140
  plugins/
@@ -144,15 +150,15 @@ __tests__/ Convex + lib unit tests (validators, HMAC, dee
144
150
 
145
151
  ## Long-form docs
146
152
 
147
- - [`SETUP.md`](./SETUP.md). Every setup phase with full prompts, env-var alternatives for non-interactive runs, recovery paths.
148
- - [`DESIGN.md`](./DESIGN.md). Color palette, typography, spacing, radius ladder, materials, the SwiftUI primitives + custom composition surface.
149
- - [`../../docs/UPSTREAM.md`](../../docs/UPSTREAM.md). Every upstream PR powering the template: `@expo/ui/swift-ui` modifiers, `expo-modules-core` fixes, `expo-tools` resolution, CI workflow guards.
153
+ - [`SETUP.md`](./SETUP.md). Every setup phase with full prompts, env-var alternatives, recovery paths.
154
+ - [`DESIGN.md`](./DESIGN.md). Palette, typography, spacing, materials, and the SwiftUI composition surface.
150
155
  - [`AGENTS.md`](./AGENTS.md). Guidance for AI coding agents working in this codebase.
156
+ - [`UPSTREAM.md`](https://github.com/ramonclaudio/vexpo/blob/main/docs/UPSTREAM.md). Every upstream PR powering the template.
151
157
 
152
158
  ## Version pinning
153
159
 
154
- Every `expo-*` package tracks the same SDK 56 release. Mismatched versions cause subtle runtime crashes. `npm run upgrade:stable` runs `expo install expo@latest && expo install --fix` to roll all of them forward together; `npm run upgrade` (`expo@next`) tracks the next SDK preview.
160
+ Every `expo-*` package tracks the same SDK 56 release. Mismatched versions cause subtle runtime crashes. `npm run upgrade:stable` rolls them all forward together. `npm run upgrade` tracks the next SDK preview.
155
161
 
156
- `@convex-dev/better-auth@0.12.0` is the minimum compatible with `better-auth@1.6.x` (peer-dep range is `>=1.6.9 <1.7.0`). Earlier versions peer-dep `better-auth <1.6.0` and reject the `mode` field newer better-auth adds to adapter queries, breaking signup. The template pins `better-auth@1.6.16` + `@convex-dev/better-auth@0.12.3`.
162
+ `@convex-dev/better-auth@0.12.0` is the minimum compatible with `better-auth@1.6.x` (`0.12.3` peer-deps `better-auth >=1.6.11 <1.7.0`). Earlier versions peer-dep `better-auth <1.6.0` and reject the `mode` field newer better-auth adds to adapter queries, which breaks signup. The template pins `better-auth@1.6.16` and `@convex-dev/better-auth@0.12.3`.
157
163
 
158
- `convex` is pinned `~1.40.0` for now: 1.41.0 adds a `transactionLimits` options param to `runMutation` that `@convex-dev/resend@0.2.4`'s ctx types reject, which breaks the `convex/http.ts` typecheck. Widen the range back to `^1.40.0` once resend's types accept 1.41.
164
+ `convex` is pinned `~1.40.0` for now. 1.41.0 adds a `transactionLimits` param to `runMutation` that `@convex-dev/resend@0.2.4`'s ctx types reject, which breaks the `convex/http.ts` typecheck. Widen back to `^1.40.0` once resend's types accept 1.41.
@@ -54,6 +54,8 @@ 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
+ `lite` and `full` also accept `--plan`. It prints the full setup journey upfront, every phase and human gate (async waits, web-UI clicks, automated steps), with costs and links, then exits. `--plan` is the map of the whole trip. `--dry-run` is what the next run does against current state. Neither writes anything.
58
+
57
59
  ## What `npx vexpo full` does
58
60
 
59
61
  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>`.
@@ -69,6 +71,7 @@ The orchestrator runs the following phases in order, skipping any that are cache
69
71
  | 6 | `npx vexpo full` (EAS phase) | eas | Thin wrapper: `eas init` + `eas env:push` from `.env.local` |
70
72
  | 7 | `npx vexpo apple asc-key` | ours | Validate ASC API key against ASC `/v1/apps` (no upload) |
71
73
  | 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 |
74
+ | 7.6 | `npx vexpo asc:connect` | eas | Wraps `eas integrations:asc:connect`, links the EAS project to its ASC app so `eas submit` resolves the bundle |
72
75
  | 8 | `npx vexpo apple services-id` | ours | Attach SIWA capability via ASC API (manual: create the Services ID itself) |
73
76
  | 9 | `npx vexpo apple jwt` | ours | Sign SIWA ES256 client_secret JWT, push to Convex env |
74
77
  | 10 | `npx vexpo apple eas-rotation-secrets` | ours | Push the 5 EAS production secrets the JWT rotation cron needs |
@@ -136,7 +139,7 @@ Edits:
136
139
  - `app.config.ts`, `name`, `slug`, `scheme`, `BUNDLE_ID` env-var fallback
137
140
  - `app.json`, clears stale `extra.eas.projectId` (next `eas init` regenerates)
138
141
  - `package.json`, `name`, `version` reset to `0.1.0`
139
- - `store.config.json`, regenerated from example with prompted values
142
+ - `store.config.json`, edited in place with prompted values (ships committed with placeholders)
140
143
 
141
144
  Backups land in `.rebrand-backup/<timestamp>/` before any write. Idempotent: re-runs detect "already rebranded" via state and skip unless `--force` is passed.
142
145
 
@@ -166,7 +169,9 @@ Prompts once for a Resend full-access key (or reads `RESEND_FULL_ACCESS_KEY`). P
166
169
  - A scoped sending key named `<pkg.name>` (deletes any existing key with the same name first)
167
170
  - A webhook pointing at `<convex-site-url>/resend-webhook`, signed with a fresh secret
168
171
 
169
- Sets on Convex: `RESEND_API_KEY`, `RESEND_WEBHOOK_SECRET`, `EMAIL_FROM=<pkg.name>@<domain>`, `RESEND_TEST_MODE=false`.
172
+ Sets on Convex: `RESEND_API_KEY`, `RESEND_WEBHOOK_SECRET`, `EMAIL_FROM=<pkg.name>@<domain>`, `RESEND_TEST_MODE=false`, `REQUIRE_EMAIL_VERIFICATION=true`.
173
+
174
+ `REQUIRE_EMAIL_VERIFICATION=true` flips sign-up to require an OTP before the account activates (now that real email sends).
170
175
 
171
176
  `RESEND_TEST_MODE=true` (default at provision time, flipped to `false` here) sends OTPs to Convex logs instead of real email, useful during local dev. Override `EMAIL_FROM` via `--from`.
172
177
 
@@ -201,7 +206,7 @@ Pass `--email` / `--password` to override the values from `store.config.json`. S
201
206
 
202
207
  ## Phase 6: EAS (auto, no standalone command, runs as part of `vexpo full`)
203
208
 
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"]`.
209
+ 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 `eas env:push --path .env.local --environment development --force`. Prod and preview values come from `.env.prod` via `vexpo env push`, which routes to `["production", "preview"]`.
205
210
 
206
211
  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`.
207
212
 
@@ -223,7 +228,7 @@ Walks you to https://appstoreconnect.apple.com/access/integrations/api, prints s
223
228
  3. Click "Generate". The key cannot be retrieved later, save the .p8 file.
224
229
  4. From the table: copy the Issuer ID (above the table) and the Key ID.
225
230
 
226
- Then prompts for issuer ID, key ID, and `.p8` path. Validates by signing an ES256 JWT and calling `GET /v1/apps`. Re-prompts up to 3x with specific error messaging on failure (`401` = bad token, `403` = role insufficient, etc.).
231
+ Then prompts for issuer ID, key ID, and `.p8` path. Validates by signing an ES256 JWT and calling `GET /v1/apps`. A bad key fails immediately with the reason (`401` = bad token, `403` = role insufficient, etc.), and you re-run the command. The only retry is 5 attempts on transient HTTP `429`/`5xx`, honoring `Retry-After`.
227
232
 
228
233
  Records `{issuerId, keyId, p8Path, validatedAt}` in `.setup-state.json`. The .p8 file itself stays where you put it, vexpo never copies it.
229
234
 
@@ -310,7 +315,7 @@ Rotate without re-prompting IDs: `npx vexpo apple jwt --rotate`.
310
315
 
311
316
  ### JWT rotation
312
317
 
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`.
318
+ Apple caps `client_secret` JWTs at 180 days. The `.eas/workflows/rotate-apple-jwt.yml` cron (`0 12 1 */3 *`) fires quarterly, at 12:00 UTC on the 1st of Jan, Apr, Jul, and Oct, 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`.
314
319
 
315
320
  ## Phase 10: EAS rotation secrets (`npx vexpo apple eas-rotation-secrets`)
316
321
 
@@ -386,6 +391,7 @@ Each known env-var has a fixed routing in `vexpo`'s env-files module ([source](h
386
391
  | `RESEND_API_KEY` | dev | prod | n/a |
387
392
  | `RESEND_WEBHOOK_SECRET` | dev | prod | n/a |
388
393
  | `RESEND_TEST_MODE`, `EMAIL_FROM` | dev | prod | n/a |
394
+ | `REQUIRE_EMAIL_VERIFICATION` | dev | prod | n/a |
389
395
  | `APP_NAME`, `SITE_URL`, `APP_BUNDLE_ID` | dev | prod | n/a |
390
396
  | `APPLE_CLIENT_ID` | dev | prod | n/a |
391
397
  | `APPLE_CLIENT_SECRET` | dev | prod | n/a |
@@ -400,7 +406,6 @@ The five rotation-cron secrets (`APPLE_TEAM_ID`, `APPLE_KEY_ID`, `APPLE_SERVICES
400
406
  Notes:
401
407
 
402
408
  - `APPLE_SERVICES_ID` is renamed to `APPLE_CLIENT_ID` on Convex (Better Auth's expected key name).
403
- - GitHub secrets only get pushed from `.env.prod`, they're consumed by the prod-only JWT rotation cron.
404
409
  - `CONVEX_DEPLOYMENT` is ignored entirely (file-local pointer used by the Convex CLI, not synced).
405
410
  - Anything else is reported as "unrecognized" and skipped.
406
411
 
@@ -453,7 +458,7 @@ npx vexpo doctor --json # machine-readable output
453
458
  npx vexpo doctor --strict # exit non-zero on warnings
454
459
  ```
455
460
 
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:
461
+ Runs a battery of checks that auth-test each credential and cross-reference the values across `.env.local`, Convex env, EAS env, and `app.config.ts`. Lite mode runs the same battery automatically after sync (skip with `--no-verify`). Results are grouped by category:
457
462
 
458
463
  | Category | What's checked |
459
464
  | ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -488,14 +493,14 @@ npx eas metadata:push # push store.conf
488
493
  npx eas credentials -p ios # manage iOS dist cert / profile / keys
489
494
  ```
490
495
 
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`.
496
+ EAS Workflows (`.eas/workflows/`) automate these. `release.yml` runs on push of a `v*` tag, `testflight.yml` on push to `beta/*`, `rotate-apple-jwt.yml` on a quarterly cron. The rest (`deploy-production.yml`, `pr-preview.yml`, `e2e-tests.yml`, `rollback.yml`, `rollout.yml`, `development-builds.yml`) are `workflow_dispatch` only. Nothing runs on push to `main` or on PR. `asc-events.yml` fires on App Store Connect events.
492
497
 
493
498
  ## Recovery: rotating things
494
499
 
495
500
  | Thing | Command |
496
501
  | -------------------------- | ---------------------------------------------------------------- |
497
502
  | Convex deployment | `npx vexpo full --fresh` |
498
- | Better Auth secret | `npx vexpo better-auth --force` |
503
+ | Better Auth secret | `npx vexpo better-auth --rotate-secret` |
499
504
  | Resend key + webhook | `npx vexpo resend` |
500
505
  | Apple Sign In JWT (manual) | `npx vexpo apple jwt --rotate` |
501
506
  | Apple Sign In JWT (auto) | EAS Workflows → `rotate-apple-jwt` → Run |
@@ -504,6 +509,16 @@ EAS Workflows (`.eas/workflows/`) automate these for you on push to `main`, push
504
509
  | EAS rotation secrets | `npx vexpo apple eas-rotation-secrets --force` |
505
510
  | State cache | `trash .setup-state.json` |
506
511
 
512
+ ## Other commands
513
+
514
+ For migrations and out-of-band fixes:
515
+
516
+ - `npx vexpo adopt`, finish a project created by `eas integrations:convex:connect` (adopts the existing dev deployment, backfills site URLs + Better Auth, prints the commands left to run).
517
+ - `npx vexpo convex:migrate --from <dep>`, copy server-side Convex env (`BETTER_AUTH_SECRET`, `RESEND_*`, `APPLE_*`, `APP_*`) from another deployment onto the current one. `CONVEX_*` left untouched.
518
+ - `npx vexpo env convex-key [--mint]`, sync the Convex deploy key to EAS env after a deployment migration (`env push` skips these on purpose). `--mint` mints a prod key via the Platform API if EAS lacks one.
519
+ - `npx vexpo apple jwt --copy-from <dep>`, copy `APPLE_*` env from another deployment instead of signing. No `.p8` needed.
520
+ - `npx vexpo resend --repoint`, move the webhook to the current `convex.site` and realign the secret, without rotating the sending key or changing auth policy.
521
+
507
522
  ## Recovery: things break
508
523
 
509
524
  `.setup-state.json` corrupt or schema mismatch:
@@ -568,7 +583,7 @@ Use `--no-state` to ignore the local state cache. Provide every interactive valu
568
583
  | 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
584
  | `app.config.ts` | Expo app config (reads `.env.local`) | edited by rebrand |
570
585
  | `app.json` | Static `eas.projectId` | written by `eas init` |
571
- | `store.config.json` | App Store metadata + review contact | edited by rebrand, gitignored |
586
+ | `store.config.json` | App Store metadata + review contact | committed with placeholders, edited in place by rebrand |
572
587
  | `package.json` | Project metadata | edited by rebrand |
573
588
 
574
589
  ## State schema
@@ -610,15 +625,15 @@ Atomic writes via `tmp + rename`. Schema mismatches fail-loud, `setup` will refu
610
625
 
611
626
  ## Security
612
627
 
613
- | Class | Lives in | Rotates |
614
- | --------------------- | -------------------------------------------------------------- | --------------------------------------------------------- |
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 |
617
- | Resend webhook secret | Convex env (`RESEND_WEBHOOK_SECRET`) | rotated alongside the key |
618
- | Apple Sign In JWT | Convex env (`APPLE_CLIENT_SECRET`) | 180-day max, auto-rotated by EAS Workflows cron every 90d |
619
- | Apple Sign In `.p8` | EAS env `APPLE_P8_PRIVATE_KEY` (secret visibility, production) | rotate the key in Apple Developer Console |
620
- | ASC API `.p8` | filesystem (path you choose) + state cache | manual, rotate via App Store Connect → Integrations |
621
- | `CONVEX_DEPLOY_KEY` | EAS env (production, secret visibility) | rotate at Convex Dashboard → Settings → Deploy Keys |
628
+ | Class | Lives in | Rotates |
629
+ | --------------------- | -------------------------------------------------------------- | ------------------------------------------------------------- |
630
+ | `BETTER_AUTH_SECRET` | Convex env | rotate via `npx vexpo better-auth --rotate-secret` |
631
+ | Resend sending key | Convex env (`RESEND_API_KEY`) | `npx vexpo resend` deletes the named key + recreates |
632
+ | Resend webhook secret | Convex env (`RESEND_WEBHOOK_SECRET`) | rotated alongside the key |
633
+ | Apple Sign In JWT | Convex env (`APPLE_CLIENT_SECRET`) | 180-day max, auto-rotated quarterly by the EAS Workflows cron |
634
+ | Apple Sign In `.p8` | EAS env `APPLE_P8_PRIVATE_KEY` (secret visibility, production) | rotate the key in Apple Developer Console |
635
+ | ASC API `.p8` | filesystem (path you choose) + state cache | manual, rotate via App Store Connect → Integrations |
636
+ | `CONVEX_DEPLOY_KEY` | EAS env (production, secret visibility) | rotate at Convex Dashboard → Settings → Deploy Keys |
622
637
 
623
638
  `.setup-state.json` only stores resource IDs, file paths, and timestamps, not secrets. Safe to share for debugging.
624
639
 
@@ -18,5 +18,4 @@ docs/
18
18
  .agents/
19
19
  .claude/
20
20
  app-store/
21
- .husky/
22
21
  convex/*.test.ts
@@ -1,10 +1,11 @@
1
- # .env.local template. Copy to .env.local then run `npm run setup`.
2
- # `npm run setup` writes most of these for you. The two identity vars
3
- # (EXPO_PUBLIC_APP_BUNDLE_ID and EXPO_PUBLIC_APPLE_TEAM_ID) are prompted by
4
- # `npm run setup:convex`. Edit by hand if you prefer.
1
+ # .env.local template. Copy to .env.local then run `npx vexpo full`.
2
+ # `npx vexpo full` writes most of these for you (`npx vexpo lite` for the
3
+ # dev-only subset). The two identity vars (EXPO_PUBLIC_APP_BUNDLE_ID and
4
+ # EXPO_PUBLIC_APPLE_TEAM_ID) are prompted by `npx vexpo convex`. Edit by hand
5
+ # if you prefer.
5
6
 
6
7
  # ---------------------------------------------------------------------------
7
- # Convex deployment (written by `npm run setup:convex`)
8
+ # Convex deployment (written by `npx vexpo convex`)
8
9
  # ---------------------------------------------------------------------------
9
10
  CONVEX_DEPLOYMENT=
10
11
  EXPO_PUBLIC_CONVEX_URL=
@@ -12,7 +13,7 @@ EXPO_PUBLIC_CONVEX_SITE_URL=
12
13
  EXPO_PUBLIC_SITE_URL=
13
14
 
14
15
  # ---------------------------------------------------------------------------
15
- # iOS identity (prompted by `npm run setup:convex`, also pushed to Convex env)
16
+ # iOS identity (prompted by `npx vexpo convex`, also pushed to Convex env)
16
17
  # ---------------------------------------------------------------------------
17
18
  # Reverse-DNS bundle id, e.g. com.you.vexpo.
18
19
  EXPO_PUBLIC_APP_BUNDLE_ID=
@@ -26,9 +27,13 @@ EXPO_PUBLIC_APPLE_TEAM_ID=
26
27
  # EXPO_PUBLIC_EXPO_OWNER=
27
28
  # Toggles `name` to "Vexpo (Dev)" so dev and prod can install side-by-side.
28
29
  # APP_VARIANT=development
29
- # Pre-fills the full-access Resend key for `npm run setup:resend` (else prompts).
30
+ # Pre-fills the full-access Resend key for `npx vexpo resend` (else prompts).
30
31
  # RESEND_FULL_ACCESS_KEY=
31
- # Skips prompts in `npm run setup:apple` / `setup:asc`.
32
- # APPLE_P8_PATH=
33
- # APPLE_AUTH_KEY_PATH=
32
+ # Apple credential overrides. The CLI reads these from the shell environment,
33
+ # not this file, so export them before running. The APPLE_ASC_* trio skips the
34
+ # `npx vexpo apple asc-key` prompts. APPLE_P8_PATH skips the .p8 prompt in
35
+ # `npx vexpo apple jwt`.
34
36
  # APPLE_ASC_ISSUER_ID=
37
+ # APPLE_ASC_KEY_ID=
38
+ # APPLE_ASC_P8_PATH=
39
+ # APPLE_P8_PATH=
@@ -16,17 +16,17 @@ const pkg = JSON.parse(readFileSync(resolve(process.cwd(), "package.json"), "utf
16
16
 
17
17
  const IS_DEV = process.env.APP_VARIANT === "development";
18
18
 
19
- // Identity comes from .env.local (written by `npm run setup:convex`). Fallbacks
19
+ // Identity comes from .env.local (written by `npx vexpo convex`). Fallbacks
20
20
  // keep `expo prebuild` from crashing on a fresh checkout, but a real build
21
21
  // requires real values.
22
22
  const BUNDLE_ID = process.env.EXPO_PUBLIC_APP_BUNDLE_ID ?? `com.example.${pkg.name}`;
23
23
  const APPLE_TEAM_ID = process.env.EXPO_PUBLIC_APPLE_TEAM_ID ?? "ABCDE12345";
24
24
  const EXPO_OWNER = process.env.EXPO_PUBLIC_EXPO_OWNER ?? undefined;
25
25
 
26
- // Support contact surface. Populated by `vexpo rebrand` from `store.config.json`
27
- // once the user creates one (needed only for App Store submission). On a fresh
28
- // checkout these are empty and `app/(app)/help.tsx` hides the corresponding
29
- // buttons gracefully.
26
+ // Support contact surface. `store.config.json` ships committed with
27
+ // placeholders; `vexpo rebrand` fills it in (needed only for App Store
28
+ // submission). On a fresh checkout these stay empty and `app/(app)/help.tsx`
29
+ // hides the corresponding buttons gracefully.
30
30
  type StoreConfig = {
31
31
  apple: {
32
32
  copyright?: string;
@@ -2,7 +2,7 @@ import { v } from "convex/values";
2
2
 
3
3
  import { internal } from "./_generated/api";
4
4
  import { internalMutation, internalQuery } from "./_generated/server";
5
- import { authMutation, authQuery } from "./functions";
5
+ import { authMutation } from "./functions";
6
6
  import { rateLimitWithThrow } from "./rateLimit";
7
7
  import { deviceTypeValidator } from "./validators";
8
8
 
@@ -76,31 +76,6 @@ export const remove = authMutation({
76
76
  },
77
77
  });
78
78
 
79
- export const list = authQuery({
80
- args: {},
81
- returns: v.array(
82
- v.object({
83
- _id: v.id("pushTokens"),
84
- _creationTime: v.number(),
85
- userId: v.string(),
86
- token: v.string(),
87
- deviceType: deviceTypeValidator,
88
- createdAt: v.number(),
89
- updatedAt: v.number(),
90
- lastSeenAt: v.optional(v.number()),
91
- revoked: v.optional(v.boolean()),
92
- revokedAt: v.optional(v.number()),
93
- lastErrorCode: v.optional(v.string()),
94
- }),
95
- ),
96
- handler: async (ctx) => {
97
- return ctx.db
98
- .query("pushTokens")
99
- .withIndex("by_user", (q) => q.eq("userId", ctx.user._id))
100
- .collect();
101
- },
102
- });
103
-
104
79
  export const removeAll = authMutation({
105
80
  args: {},
106
81
  returns: v.null(),
@@ -4,21 +4,6 @@ import { components } from "./_generated/api";
4
4
  import type { MutationCtx } from "./_generated/server";
5
5
 
6
6
  export const rateLimiter = new RateLimiter(components.rateLimiter, {
7
- apiRead: {
8
- kind: "token bucket",
9
- rate: 100,
10
- period: MINUTE,
11
- capacity: 20,
12
- shards: 2,
13
- },
14
-
15
- apiWrite: {
16
- kind: "token bucket",
17
- rate: 30,
18
- period: MINUTE,
19
- capacity: 10,
20
- },
21
-
22
7
  userAction: {
23
8
  kind: "token bucket",
24
9
  rate: 60,
@@ -44,12 +29,7 @@ export const rateLimiter = new RateLimiter(components.rateLimiter, {
44
29
  avatarUpload: { kind: "token bucket", rate: 30, period: HOUR, capacity: 10 },
45
30
  });
46
31
 
47
- export type RateLimitName =
48
- | "apiRead"
49
- | "apiWrite"
50
- | "userAction"
51
- | "criticalAction"
52
- | "avatarUpload";
32
+ export type RateLimitName = "userAction" | "criticalAction" | "avatarUpload";
53
33
 
54
34
  export async function rateLimitWithThrow(
55
35
  ctx: MutationCtx,
@@ -8,12 +8,7 @@ import { authComponent, authUserValidator } from "./auth";
8
8
  import { validationError } from "./errors";
9
9
  import { authMutation, optionalAuthQuery } from "./functions";
10
10
  import { rateLimitWithThrow } from "./rateLimit";
11
- import {
12
- paginatedUsersValidator,
13
- publicUserProfileValidator,
14
- userProfileUpdateFields,
15
- validateBio,
16
- } from "./validators";
11
+ import { publicUserProfileValidator, userProfileUpdateFields, validateBio } from "./validators";
17
12
 
18
13
  export const getMe = optionalAuthQuery({
19
14
  args: {},
@@ -58,49 +53,6 @@ export const getUser = optionalAuthQuery({
58
53
  },
59
54
  });
60
55
 
61
- export const listUsers = optionalAuthQuery({
62
- args: {
63
- cursor: v.optional(v.string()),
64
- limit: v.optional(v.number()),
65
- },
66
- returns: paginatedUsersValidator,
67
- handler: async (ctx, args) => {
68
- const limit = Math.min(Math.max(args.limit ?? 20, 1), 100);
69
-
70
- const results = await ctx.db
71
- .query("users")
72
- .order("desc")
73
- .paginate({ cursor: args.cursor ?? null, numItems: limit });
74
-
75
- const page = await Promise.all(
76
- results.page.map(async (user) => {
77
- const authUser = await authComponent.getAnyUserById(ctx, user.authId);
78
- if (!authUser) return null;
79
- const avatarUrl = user.avatar
80
- ? await ctx.storage.getUrl(user.avatar)
81
- : (authUser.image ?? null);
82
- return {
83
- _id: user._id,
84
- _creationTime: user._creationTime,
85
- name: authUser.name,
86
- username:
87
- (authUser as { displayUsername?: string | null }).displayUsername ??
88
- (authUser as { username?: string | null }).username ??
89
- null,
90
- avatarUrl,
91
- bio: user.bio,
92
- };
93
- }),
94
- );
95
-
96
- return {
97
- page: page.filter((entry): entry is NonNullable<typeof entry> => entry !== null),
98
- continueCursor: results.continueCursor,
99
- isDone: results.isDone,
100
- };
101
- },
102
- });
103
-
104
56
  export const updateProfile = authMutation({
105
57
  args: userProfileUpdateFields,
106
58
  returns: v.id("users"),
@@ -1,11 +1,6 @@
1
1
  import { literals } from "convex-helpers/validators";
2
2
  import { v } from "convex/values";
3
3
 
4
- export const paginatedResponseFields = {
5
- continueCursor: v.string(),
6
- isDone: v.boolean(),
7
- };
8
-
9
4
  // Name changes go through Better Auth (authClient.updateUser) directly.
10
5
  export const userProfileUpdateFields = {
11
6
  bio: v.optional(v.string()),
@@ -20,11 +15,6 @@ export const publicUserProfileValidator = v.object({
20
15
  bio: v.optional(v.string()),
21
16
  });
22
17
 
23
- export const paginatedUsersValidator = v.object({
24
- page: v.array(publicUserProfileValidator),
25
- ...paginatedResponseFields,
26
- });
27
-
28
18
  export const deviceTypeValidator = literals("ios");
29
19
 
30
20
  const BIO_MAX_LENGTH = 500;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vexpo",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "private": true,
5
5
  "main": "expo-router/entry",
6
6
  "scripts": {
@@ -4,29 +4,45 @@ Build and maintenance scripts. Setup orchestration lives in the published `vexpo
4
4
 
5
5
  ## What's in this directory
6
6
 
7
- | Script | What it does |
8
- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
9
- | `clean.ts` | Trash + reinstall. `--metro` for cache-only nuke (Metro/Babel/Haste). `--state` also wipes `.setup-state.json`. |
10
- | `rotate-apple-jwt.mjs` | Re-signs the Apple Sign In `client_secret` JWT from env vars only. Used by `.eas/workflows/rotate-apple-jwt.yml` every 90 days. |
11
- | `_run.mjs` | Runtime selector for `clean.ts`. Picks `bun` if available, falls back to `tsx`. Not used by the CLI. |
7
+ | Script | What it does |
8
+ | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
9
+ | `clean.ts` | Trash + reinstall. `--metro` for cache-only nuke (Metro/Haste/node-compile-cache). `--state` also wipes `.setup-state.json`. |
10
+ | `gen-update-cert.mjs` | One-shot OTA update code-signing setup. Wraps `npx expo-updates codesigning:generate`, writes `certs/certificate.pem` (committed) and `../keys/private-key.pem` (gitignored). Run via `npm run updates:gen-cert -- --name "<Org>"`. |
11
+ | `rotate-apple-jwt.mjs` | Re-signs the Apple Sign In `client_secret` JWT from env vars only. Used by `.eas/workflows/rotate-apple-jwt.yml` every 90 days. |
12
+ | `_run.mjs` | Runtime selector for `clean.ts`. Picks `bun` if available, falls back to `tsx`. Not used by the CLI. |
12
13
 
13
14
  Anything else (preflight checks, env validation, version bumps) lives in the `vexpo` CLI or in `eas-cli` directly.
14
15
 
16
+ ## Cleaning
17
+
18
+ ```bash
19
+ npm run clean # trash + reinstall
20
+ npm run clean:metro # just Metro/Haste/node-compile-cache
21
+ npm run clean:state # also wipe .setup-state.json
22
+ ```
23
+
24
+ These run through `_run.mjs`, which picks `bun` if it's on PATH and falls back to `tsx`. To call it without the npm script: `node scripts/_run.mjs scripts/clean.ts --metro`.
25
+
15
26
  ## Setup orchestration
16
27
 
17
- Use the `vexpo` CLI:
28
+ Use the `vexpo` CLI. `lite` and `full` are alternatives, pick the path you need:
18
29
 
19
30
  ```bash
20
31
  npx vexpo lite # dev-mode setup (Convex + Better Auth only)
21
32
  npx vexpo full # full provisioning to TestFlight-ready
33
+ ```
34
+
35
+ The rest are independent maintenance commands, run them when you need them:
36
+
37
+ ```bash
22
38
  npx vexpo doctor # cross-source drift detection
23
- npx vexpo env push # sync from .env.local + .env.prod to Convex/EAS
39
+ npx vexpo env push # sync from .env.local + .env.prod to Convex and EAS
24
40
  npx vexpo apple asc-key # validate ASC API key
25
41
  npx vexpo apple services-id # attach SIWA capability to App ID
26
42
  npx vexpo apple jwt # sign client_secret JWT, push to Convex
27
43
  ```
28
44
 
29
- Version bumps run through `eas build:version:set` / `eas build:version:sync` (`appVersionSource: "remote"` in `eas.json` puts EAS in charge of the version).
45
+ Version bumps run through `eas build:version:set` or `eas build:version:sync`. `appVersionSource: "remote"` in `eas.json` puts EAS in charge of the version.
30
46
 
31
47
  The CLI itself ships from [`@ramonclaudio/vexpo` on npm](https://www.npmjs.com/package/@ramonclaudio/vexpo). Source lives at [`github.com/ramonclaudio/vexpo`](https://github.com/ramonclaudio/vexpo).
32
48