@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.
- package/LICENSE +21 -0
- package/README.md +29 -15
- package/dist/index.js +44 -14
- package/dist/templates/default/.eas/workflows/e2e-tests.yml +17 -1
- package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +3 -3
- package/dist/templates/default/.maestro/auth.yaml +229 -0
- package/dist/templates/default/.maestro/launch.yaml +5 -5
- package/dist/templates/default/.maestro/tour.yaml +294 -0
- package/dist/templates/default/.maestro/zz-delete-restore.yaml +174 -0
- package/dist/templates/default/AGENTS.md +3 -2
- package/dist/templates/default/DESIGN.md +41 -41
- package/dist/templates/default/README.md +46 -40
- package/dist/templates/default/SETUP.md +34 -19
- package/dist/templates/default/_easignore +0 -1
- package/dist/templates/default/_env.example +15 -10
- package/dist/templates/default/app.config.ts +5 -5
- package/dist/templates/default/convex/pushTokens.ts +1 -26
- package/dist/templates/default/convex/rateLimit.ts +1 -21
- package/dist/templates/default/convex/users.ts +1 -49
- package/dist/templates/default/convex/validators.ts +0 -10
- package/dist/templates/default/package.json +1 -1
- package/dist/templates/default/scripts/README.md +24 -8
- package/dist/templates/default/scripts/clean.ts +3 -3
- package/dist/templates/default/scripts/gen-update-cert.mjs +3 -1
- package/dist/templates/default/src/app/(app)/_layout.tsx +15 -1
- package/dist/templates/default/src/app/(app)/auth/forgot-password.tsx +3 -0
- package/dist/templates/default/src/app/(app)/auth/reset-password.tsx +3 -0
- package/dist/templates/default/src/app/(app)/auth/sign-up.tsx +3 -1
- package/dist/templates/default/src/app/(app)/privacy.tsx +3 -2
- package/dist/templates/default/src/app/(app)/restore-account.tsx +2 -2
- package/dist/templates/default/src/app/(app)/sessions.tsx +15 -5
- package/dist/templates/default/src/components/ui/convex-error.tsx +0 -7
- package/dist/templates/default/src/constants/ui.ts +0 -11
- package/dist/templates/default/src/lib/dev-menu.ts +11 -2
- package/dist/templates/default/src/lib/preferences.ts +9 -0
- package/package.json +3 -2
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
# vexpo
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
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
|
+
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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
|
|
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,
|
|
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
|
|
148
|
-
- [`DESIGN.md`](./DESIGN.md).
|
|
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`
|
|
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-
|
|
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
|
|
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`,
|
|
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 `
|
|
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`.
|
|
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
|
|
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,
|
|
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
|
|
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 --
|
|
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
|
|
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 --
|
|
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
|
|
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
|
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
# .env.local template. Copy to .env.local then run `
|
|
2
|
-
# `
|
|
3
|
-
#
|
|
4
|
-
# `
|
|
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 `
|
|
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 `
|
|
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 `
|
|
30
|
+
# Pre-fills the full-access Resend key for `npx vexpo resend` (else prompts).
|
|
30
31
|
# RESEND_FULL_ACCESS_KEY=
|
|
31
|
-
#
|
|
32
|
-
#
|
|
33
|
-
#
|
|
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 `
|
|
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.
|
|
27
|
-
//
|
|
28
|
-
// checkout these
|
|
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
|
|
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;
|
|
@@ -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/
|
|
10
|
-
| `
|
|
11
|
-
| `
|
|
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
|
|
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`
|
|
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
|
|