create-react-native-airborne 0.0.1

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 (78) hide show
  1. package/README.md +24 -0
  2. package/package.json +21 -0
  3. package/src/index.mjs +103 -0
  4. package/template/.agents/skills/convex-best-practices/SKILL.md +333 -0
  5. package/template/.agents/skills/convex-file-storage/SKILL.md +466 -0
  6. package/template/.agents/skills/convex-security-audit/SKILL.md +538 -0
  7. package/template/.agents/skills/convex-security-check/SKILL.md +377 -0
  8. package/template/.github/workflows/ci.yml +130 -0
  9. package/template/.prettierignore +8 -0
  10. package/template/.prettierrc.json +6 -0
  11. package/template/AGENTS.md +156 -0
  12. package/template/Justfile +48 -0
  13. package/template/README.md +94 -0
  14. package/template/client/.env.example +3 -0
  15. package/template/client/.vscode/extensions.json +1 -0
  16. package/template/client/.vscode/settings.json +7 -0
  17. package/template/client/README.md +33 -0
  18. package/template/client/app/(app)/_layout.tsx +34 -0
  19. package/template/client/app/(app)/index.tsx +66 -0
  20. package/template/client/app/(app)/push.tsx +75 -0
  21. package/template/client/app/(app)/settings.tsx +36 -0
  22. package/template/client/app/(auth)/_layout.tsx +22 -0
  23. package/template/client/app/(auth)/sign-in.tsx +358 -0
  24. package/template/client/app/(auth)/sign-up.tsx +237 -0
  25. package/template/client/app/_layout.tsx +30 -0
  26. package/template/client/app/index.tsx +127 -0
  27. package/template/client/app.config.ts +30 -0
  28. package/template/client/assets/images/android-icon-background.png +0 -0
  29. package/template/client/assets/images/android-icon-foreground.png +0 -0
  30. package/template/client/assets/images/android-icon-monochrome.png +0 -0
  31. package/template/client/assets/images/favicon.png +0 -0
  32. package/template/client/assets/images/icon.png +0 -0
  33. package/template/client/assets/images/partial-react-logo.png +0 -0
  34. package/template/client/assets/images/react-logo.png +0 -0
  35. package/template/client/assets/images/react-logo@2x.png +0 -0
  36. package/template/client/assets/images/react-logo@3x.png +0 -0
  37. package/template/client/assets/images/splash-icon.png +0 -0
  38. package/template/client/eslint.config.js +10 -0
  39. package/template/client/global.css +2 -0
  40. package/template/client/metro.config.js +9 -0
  41. package/template/client/package.json +51 -0
  42. package/template/client/src/components/auth-shell.tsx +63 -0
  43. package/template/client/src/components/form-input.tsx +62 -0
  44. package/template/client/src/components/primary-button.tsx +37 -0
  45. package/template/client/src/components/screen.tsx +17 -0
  46. package/template/client/src/components/sign-out-button.tsx +32 -0
  47. package/template/client/src/hooks/use-theme-sync.ts +11 -0
  48. package/template/client/src/lib/convex.ts +6 -0
  49. package/template/client/src/lib/env-schema.ts +13 -0
  50. package/template/client/src/lib/env.test.ts +24 -0
  51. package/template/client/src/lib/env.ts +19 -0
  52. package/template/client/src/lib/notifications.ts +47 -0
  53. package/template/client/src/store/preferences-store.ts +42 -0
  54. package/template/client/src/types/theme.ts +1 -0
  55. package/template/client/tsconfig.json +18 -0
  56. package/template/client/uniwind-types.d.ts +10 -0
  57. package/template/client/vitest.config.ts +7 -0
  58. package/template/package.json +22 -0
  59. package/template/server/.env.example +8 -0
  60. package/template/server/README.md +31 -0
  61. package/template/server/convex/_generated/api.d.ts +55 -0
  62. package/template/server/convex/_generated/api.js +23 -0
  63. package/template/server/convex/_generated/dataModel.d.ts +60 -0
  64. package/template/server/convex/_generated/server.d.ts +143 -0
  65. package/template/server/convex/_generated/server.js +93 -0
  66. package/template/server/convex/auth.config.ts +11 -0
  67. package/template/server/convex/env.ts +18 -0
  68. package/template/server/convex/lib.ts +12 -0
  69. package/template/server/convex/push.ts +148 -0
  70. package/template/server/convex/schema.ts +22 -0
  71. package/template/server/convex/users.ts +54 -0
  72. package/template/server/convex.json +3 -0
  73. package/template/server/eslint.config.js +51 -0
  74. package/template/server/package.json +29 -0
  75. package/template/server/tests/convex.test.ts +52 -0
  76. package/template/server/tests/import-meta.d.ts +3 -0
  77. package/template/server/tsconfig.json +15 -0
  78. package/template/server/vitest.config.ts +13 -0
@@ -0,0 +1,377 @@
1
+ ---
2
+ name: Convex Security Check
3
+ description: Quick security audit checklist covering authentication, function exposure, argument validation, row-level access control, and environment variable handling
4
+ version: 1.0.0
5
+ author: Convex
6
+ tags: [convex, security, authentication, authorization, checklist]
7
+ ---
8
+
9
+ # Convex Security Check
10
+
11
+ A quick security audit checklist for Convex applications covering authentication, function exposure, argument validation, row-level access control, and environment variable handling.
12
+
13
+ ## Documentation Sources
14
+
15
+ Before implementing, do not assume; fetch the latest documentation:
16
+
17
+ - Primary: https://docs.convex.dev/auth
18
+ - Production Security: https://docs.convex.dev/production
19
+ - Functions Auth: https://docs.convex.dev/auth/functions-auth
20
+ - For broader context: https://docs.convex.dev/llms.txt
21
+
22
+ ## Instructions
23
+
24
+ ### Security Checklist
25
+
26
+ Use this checklist to quickly audit your Convex application's security:
27
+
28
+ #### 1. Authentication
29
+
30
+ - [ ] Authentication provider configured (Clerk, Auth0, etc.)
31
+ - [ ] All sensitive queries check `ctx.auth.getUserIdentity()`
32
+ - [ ] Unauthenticated access explicitly allowed where intended
33
+ - [ ] Session tokens properly validated
34
+
35
+ #### 2. Function Exposure
36
+
37
+ - [ ] Public functions (`query`, `mutation`, `action`) reviewed
38
+ - [ ] Internal functions use `internalQuery`, `internalMutation`, `internalAction`
39
+ - [ ] No sensitive operations exposed as public functions
40
+ - [ ] HTTP actions validate origin/authentication
41
+
42
+ #### 3. Argument Validation
43
+
44
+ - [ ] All functions have explicit `args` validators
45
+ - [ ] All functions have explicit `returns` validators
46
+ - [ ] No `v.any()` used for sensitive data
47
+ - [ ] ID validators use correct table names
48
+
49
+ #### 4. Row-Level Access Control
50
+
51
+ - [ ] Users can only access their own data
52
+ - [ ] Admin functions check user roles
53
+ - [ ] Shared resources have proper access checks
54
+ - [ ] Deletion functions verify ownership
55
+
56
+ #### 5. Environment Variables
57
+
58
+ - [ ] API keys stored in environment variables
59
+ - [ ] No secrets in code or schema
60
+ - [ ] Different keys for dev/prod environments
61
+ - [ ] Environment variables accessed only in actions
62
+
63
+ ### Authentication Check
64
+
65
+ ```typescript
66
+ // convex/auth.ts
67
+ import { query, mutation } from "./_generated/server";
68
+ import { v } from "convex/values";
69
+ import { ConvexError } from "convex/values";
70
+
71
+ // Helper to require authentication
72
+ async function requireAuth(ctx: QueryCtx | MutationCtx) {
73
+ const identity = await ctx.auth.getUserIdentity();
74
+ if (!identity) {
75
+ throw new ConvexError("Authentication required");
76
+ }
77
+ return identity;
78
+ }
79
+
80
+ // Secure query pattern
81
+ export const getMyProfile = query({
82
+ args: {},
83
+ returns: v.union(v.object({
84
+ _id: v.id("users"),
85
+ name: v.string(),
86
+ email: v.string(),
87
+ }), v.null()),
88
+ handler: async (ctx) => {
89
+ const identity = await requireAuth(ctx);
90
+
91
+ return await ctx.db
92
+ .query("users")
93
+ .withIndex("by_tokenIdentifier", (q) =>
94
+ q.eq("tokenIdentifier", identity.tokenIdentifier)
95
+ )
96
+ .unique();
97
+ },
98
+ });
99
+ ```
100
+
101
+ ### Function Exposure Check
102
+
103
+ ```typescript
104
+ // PUBLIC - Exposed to clients (review carefully!)
105
+ export const listPublicPosts = query({
106
+ args: {},
107
+ returns: v.array(v.object({ /* ... */ })),
108
+ handler: async (ctx) => {
109
+ // Anyone can call this - intentionally public
110
+ return await ctx.db
111
+ .query("posts")
112
+ .withIndex("by_public", (q) => q.eq("isPublic", true))
113
+ .collect();
114
+ },
115
+ });
116
+
117
+ // INTERNAL - Only callable from other Convex functions
118
+ export const _updateUserCredits = internalMutation({
119
+ args: { userId: v.id("users"), amount: v.number() },
120
+ returns: v.null(),
121
+ handler: async (ctx, args) => {
122
+ // This cannot be called directly from clients
123
+ await ctx.db.patch(args.userId, {
124
+ credits: args.amount,
125
+ });
126
+ return null;
127
+ },
128
+ });
129
+ ```
130
+
131
+ ### Argument Validation Check
132
+
133
+ ```typescript
134
+ // GOOD: Strict validation
135
+ export const createPost = mutation({
136
+ args: {
137
+ title: v.string(),
138
+ content: v.string(),
139
+ category: v.union(
140
+ v.literal("tech"),
141
+ v.literal("news"),
142
+ v.literal("other")
143
+ ),
144
+ },
145
+ returns: v.id("posts"),
146
+ handler: async (ctx, args) => {
147
+ const identity = await requireAuth(ctx);
148
+ return await ctx.db.insert("posts", {
149
+ ...args,
150
+ authorId: identity.tokenIdentifier,
151
+ });
152
+ },
153
+ });
154
+
155
+ // BAD: Weak validation
156
+ export const createPostUnsafe = mutation({
157
+ args: {
158
+ data: v.any(), // DANGEROUS: Allows any data
159
+ },
160
+ returns: v.id("posts"),
161
+ handler: async (ctx, args) => {
162
+ return await ctx.db.insert("posts", args.data);
163
+ },
164
+ });
165
+ ```
166
+
167
+ ### Row-Level Access Control Check
168
+
169
+ ```typescript
170
+ // Verify ownership before update
171
+ export const updateTask = mutation({
172
+ args: {
173
+ taskId: v.id("tasks"),
174
+ title: v.string(),
175
+ },
176
+ returns: v.null(),
177
+ handler: async (ctx, args) => {
178
+ const identity = await requireAuth(ctx);
179
+
180
+ const task = await ctx.db.get(args.taskId);
181
+
182
+ // Check ownership
183
+ if (!task || task.userId !== identity.tokenIdentifier) {
184
+ throw new ConvexError("Not authorized to update this task");
185
+ }
186
+
187
+ await ctx.db.patch(args.taskId, { title: args.title });
188
+ return null;
189
+ },
190
+ });
191
+
192
+ // Verify ownership before delete
193
+ export const deleteTask = mutation({
194
+ args: { taskId: v.id("tasks") },
195
+ returns: v.null(),
196
+ handler: async (ctx, args) => {
197
+ const identity = await requireAuth(ctx);
198
+
199
+ const task = await ctx.db.get(args.taskId);
200
+
201
+ if (!task || task.userId !== identity.tokenIdentifier) {
202
+ throw new ConvexError("Not authorized to delete this task");
203
+ }
204
+
205
+ await ctx.db.delete(args.taskId);
206
+ return null;
207
+ },
208
+ });
209
+ ```
210
+
211
+ ### Environment Variables Check
212
+
213
+ ```typescript
214
+ // convex/actions.ts
215
+ "use node";
216
+
217
+ import { action } from "./_generated/server";
218
+ import { v } from "convex/values";
219
+
220
+ export const sendEmail = action({
221
+ args: {
222
+ to: v.string(),
223
+ subject: v.string(),
224
+ body: v.string(),
225
+ },
226
+ returns: v.object({ success: v.boolean() }),
227
+ handler: async (ctx, args) => {
228
+ // Access API key from environment
229
+ const apiKey = process.env.RESEND_API_KEY;
230
+
231
+ if (!apiKey) {
232
+ throw new Error("RESEND_API_KEY not configured");
233
+ }
234
+
235
+ const response = await fetch("https://api.resend.com/emails", {
236
+ method: "POST",
237
+ headers: {
238
+ "Authorization": `Bearer ${apiKey}`,
239
+ "Content-Type": "application/json",
240
+ },
241
+ body: JSON.stringify({
242
+ from: "noreply@example.com",
243
+ to: args.to,
244
+ subject: args.subject,
245
+ html: args.body,
246
+ }),
247
+ });
248
+
249
+ return { success: response.ok };
250
+ },
251
+ });
252
+ ```
253
+
254
+ ## Examples
255
+
256
+ ### Complete Security Pattern
257
+
258
+ ```typescript
259
+ // convex/secure.ts
260
+ import { query, mutation, internalMutation } from "./_generated/server";
261
+ import { v } from "convex/values";
262
+ import { ConvexError } from "convex/values";
263
+
264
+ // Authentication helper
265
+ async function getAuthenticatedUser(ctx: QueryCtx | MutationCtx) {
266
+ const identity = await ctx.auth.getUserIdentity();
267
+ if (!identity) {
268
+ throw new ConvexError({
269
+ code: "UNAUTHENTICATED",
270
+ message: "You must be logged in",
271
+ });
272
+ }
273
+
274
+ const user = await ctx.db
275
+ .query("users")
276
+ .withIndex("by_tokenIdentifier", (q) =>
277
+ q.eq("tokenIdentifier", identity.tokenIdentifier)
278
+ )
279
+ .unique();
280
+
281
+ if (!user) {
282
+ throw new ConvexError({
283
+ code: "USER_NOT_FOUND",
284
+ message: "User profile not found",
285
+ });
286
+ }
287
+
288
+ return user;
289
+ }
290
+
291
+ // Check admin role
292
+ async function requireAdmin(ctx: QueryCtx | MutationCtx) {
293
+ const user = await getAuthenticatedUser(ctx);
294
+
295
+ if (user.role !== "admin") {
296
+ throw new ConvexError({
297
+ code: "FORBIDDEN",
298
+ message: "Admin access required",
299
+ });
300
+ }
301
+
302
+ return user;
303
+ }
304
+
305
+ // Public: List own tasks
306
+ export const listMyTasks = query({
307
+ args: {},
308
+ returns: v.array(v.object({
309
+ _id: v.id("tasks"),
310
+ title: v.string(),
311
+ completed: v.boolean(),
312
+ })),
313
+ handler: async (ctx) => {
314
+ const user = await getAuthenticatedUser(ctx);
315
+
316
+ return await ctx.db
317
+ .query("tasks")
318
+ .withIndex("by_user", (q) => q.eq("userId", user._id))
319
+ .collect();
320
+ },
321
+ });
322
+
323
+ // Admin only: List all users
324
+ export const listAllUsers = query({
325
+ args: {},
326
+ returns: v.array(v.object({
327
+ _id: v.id("users"),
328
+ name: v.string(),
329
+ role: v.string(),
330
+ })),
331
+ handler: async (ctx) => {
332
+ await requireAdmin(ctx);
333
+
334
+ return await ctx.db.query("users").collect();
335
+ },
336
+ });
337
+
338
+ // Internal: Update user role (never exposed)
339
+ export const _setUserRole = internalMutation({
340
+ args: {
341
+ userId: v.id("users"),
342
+ role: v.union(v.literal("user"), v.literal("admin")),
343
+ },
344
+ returns: v.null(),
345
+ handler: async (ctx, args) => {
346
+ await ctx.db.patch(args.userId, { role: args.role });
347
+ return null;
348
+ },
349
+ });
350
+ ```
351
+
352
+ ## Best Practices
353
+
354
+ - Never run `npx convex deploy` unless explicitly instructed
355
+ - Never run any git commands unless explicitly instructed
356
+ - Always verify user identity before returning sensitive data
357
+ - Use internal functions for sensitive operations
358
+ - Validate all arguments with strict validators
359
+ - Check ownership before update/delete operations
360
+ - Store API keys in environment variables
361
+ - Review all public functions for security implications
362
+
363
+ ## Common Pitfalls
364
+
365
+ 1. **Missing authentication checks** - Always verify identity
366
+ 2. **Exposing internal operations** - Use internalMutation/Query
367
+ 3. **Trusting client-provided IDs** - Verify ownership
368
+ 4. **Using v.any() for arguments** - Use specific validators
369
+ 5. **Hardcoding secrets** - Use environment variables
370
+
371
+ ## References
372
+
373
+ - Convex Documentation: https://docs.convex.dev/
374
+ - Convex LLMs.txt: https://docs.convex.dev/llms.txt
375
+ - Authentication: https://docs.convex.dev/auth
376
+ - Production Security: https://docs.convex.dev/production
377
+ - Functions Auth: https://docs.convex.dev/auth/functions-auth
@@ -0,0 +1,130 @@
1
+ name: "🧪 CI"
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - master
8
+
9
+ jobs:
10
+ validate:
11
+ name: "✅ Validate"
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: "🥟 Setup Bun"
17
+ uses: oven-sh/setup-bun@v2
18
+ with:
19
+ bun-version: "1.3.4"
20
+
21
+ - name: "📦 Install dependencies"
22
+ run: bun install --workspaces
23
+
24
+ - name: "🔍 Lint"
25
+ run: |
26
+ cd client && bun run lint
27
+ cd ../server && bun run lint
28
+
29
+ - name: "🧠 Typecheck"
30
+ run: |
31
+ cd client && bun run typecheck
32
+ cd ../server && bun run typecheck
33
+
34
+ - name: "🧪 Test"
35
+ run: |
36
+ cd client && bun run test
37
+ cd ../server && bun run test
38
+
39
+ android-native-build:
40
+ name: "🤖 Android Native Build"
41
+ needs: validate
42
+ runs-on: ubuntu-latest
43
+ defaults:
44
+ run:
45
+ shell: bash
46
+ steps:
47
+ - uses: actions/checkout@v4
48
+
49
+ - name: "🥟 Setup Bun"
50
+ uses: oven-sh/setup-bun@v2
51
+ with:
52
+ bun-version: "1.3.4"
53
+
54
+ - name: "☕ Setup Java"
55
+ uses: actions/setup-java@v4
56
+ with:
57
+ distribution: temurin
58
+ java-version: "17"
59
+
60
+ - name: "🤖 Setup Android SDK"
61
+ uses: android-actions/setup-android@v3
62
+
63
+ - name: "📦 Install dependencies"
64
+ run: bun install --workspaces
65
+
66
+ - name: "⚙️ Generate Android native project"
67
+ run: |
68
+ cd client
69
+ bunx expo prebuild --platform android --no-install
70
+
71
+ - name: "🏗️ Build Android (arm64-v8a)"
72
+ run: |
73
+ cd client/android
74
+ ./gradlew :app:assembleDebug --no-daemon -PreactNativeArchitectures=arm64-v8a
75
+
76
+ ios-native-build:
77
+ name: "🍎 iOS Native Build"
78
+ needs: validate
79
+ runs-on: macos-26
80
+ defaults:
81
+ run:
82
+ shell: bash
83
+ steps:
84
+ - uses: actions/checkout@v4
85
+
86
+ - name: "🥟 Setup Bun"
87
+ uses: oven-sh/setup-bun@v2
88
+ with:
89
+ bun-version: "1.3.4"
90
+
91
+ - name: "📦 Install dependencies"
92
+ run: bun install --workspaces
93
+
94
+ - name: "⚙️ Generate iOS native project"
95
+ run: |
96
+ cd client
97
+ bunx expo prebuild --platform ios --no-install
98
+
99
+ - name: "🫘 Install CocoaPods dependencies"
100
+ run: |
101
+ cd client/ios
102
+ pod install
103
+
104
+ - name: "🏗️ Build iOS Simulator (arm64)"
105
+ run: |
106
+ set -euo pipefail
107
+ cd client
108
+
109
+ workspace="$(find ios -maxdepth 1 -name '*.xcworkspace' | head -n 1)"
110
+ if [[ -z "${workspace}" ]]; then
111
+ echo "No iOS workspace found under client/ios"
112
+ exit 1
113
+ fi
114
+
115
+ scheme="$(xcodebuild -list -workspace "${workspace}" | awk '/Schemes:/{getline; gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit}')"
116
+ if [[ -z "${scheme}" ]]; then
117
+ echo "No iOS scheme found for ${workspace}"
118
+ exit 1
119
+ fi
120
+
121
+ xcodebuild \
122
+ -workspace "${workspace}" \
123
+ -scheme "${scheme}" \
124
+ -configuration Debug \
125
+ -sdk iphonesimulator \
126
+ -destination 'generic/platform=iOS Simulator' \
127
+ ARCHS=arm64 \
128
+ CODE_SIGNING_ALLOWED=NO \
129
+ ONLY_ACTIVE_ARCH=YES \
130
+ build -quiet
@@ -0,0 +1,8 @@
1
+ node_modules
2
+ bun.lock
3
+ coverage
4
+ client/.expo
5
+ client/android
6
+ client/ios
7
+ server/convex/_generated
8
+ tooling/create-react-native-airborne/template
@@ -0,0 +1,6 @@
1
+ {
2
+ "printWidth": 100,
3
+ "semi": true,
4
+ "singleQuote": false,
5
+ "trailingComma": "all"
6
+ }
@@ -0,0 +1,156 @@
1
+ # AGENTS.md
2
+
3
+ This file is for engineers and coding agents working in this repository.
4
+
5
+ ## Purpose
6
+
7
+ `react-native-airborne` is an opinionated Bun workspace starter for mobile apps:
8
+
9
+ - Expo + Expo Router client
10
+ - Expo Router Native Tabs (SDK 55)
11
+ - Clerk auth
12
+ - Uniwind styling (Tailwind v4)
13
+ - Convex backend
14
+ - Zustand + MMKV for local non-sensitive preferences
15
+ - Expo notifications
16
+
17
+
18
+ ## Monorepo Structure
19
+
20
+ - `client/`: Expo app (mobile only: iOS + Android)
21
+ - `server/`: Convex functions, schema, tests
22
+ - `uniwind/`: local integration docs used for setup decisions
23
+ - `Justfile`: top-level task runner
24
+ - `.github/workflows/ci.yml`: CI pipeline
25
+
26
+ ## Tooling Baseline
27
+
28
+ - Package manager: Bun (`bun@1.3.4`)
29
+ - Workspace management: Bun workspaces (root `package.json`)
30
+ - Task runner: `just`
31
+ - Formatting: Prettier (root `.prettierrc.json`)
32
+ - Client lint: Expo ESLint config (flat config)
33
+ - Server lint: strict ESLint 9 + `typescript-eslint` + `@convex-dev/eslint-plugin`
34
+ - Tests: Vitest (client and server), plus `convex-test` on server
35
+
36
+ ## Essential Commands
37
+
38
+ From repository root:
39
+
40
+ - `just install`: install all workspace dependencies
41
+ - `just dev`: run client and server together
42
+ - `just dev-client`: run Expo only
43
+ - `just dev-server`: run Convex only
44
+ - `just prebuild`: generate native iOS/Android projects locally
45
+ - `just ios`: run iOS app
46
+ - `just android`: run Android app
47
+ - `just fmt`: format client/server with Prettier
48
+ - `just lint`: lint both workspaces
49
+ - `just typecheck`: typecheck both workspaces
50
+ - `just test`: run all tests
51
+ - `just ci`: lint + typecheck + tests
52
+
53
+ ## Environment Variables
54
+
55
+ ### Client (`client/.env`)
56
+
57
+ - `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` (required)
58
+ - `EXPO_PUBLIC_CONVEX_URL` (required)
59
+ - `EXPO_PUBLIC_EAS_PROJECT_ID` (optional; used for push token registration when needed)
60
+
61
+ Validation is in `client/src/lib/env-schema.ts`.
62
+
63
+ ### Server (`server/.env`)
64
+
65
+ - `CLERK_JWT_ISSUER_DOMAIN` (required in real environments)
66
+ - `EXPO_PUSH_ENDPOINT` (optional, default Expo endpoint)
67
+ - `EXPO_ACCESS_TOKEN` (optional)
68
+
69
+ Validation is in `server/convex/env.ts`.
70
+
71
+ ## Auth Architecture (Clerk + Expo Router)
72
+
73
+ - Root providers live in `client/app/_layout.tsx`:
74
+ - `ClerkProvider` with `tokenCache` from `@clerk/clerk-expo/token-cache` (secure storage)
75
+ - `ConvexProviderWithClerk`
76
+ - `SafeAreaProvider`
77
+ - Route guards:
78
+ - `client/app/(auth)/_layout.tsx` redirects signed-in users to `/(app)`
79
+ - `client/app/(app)/_layout.tsx` redirects signed-out users to `/(auth)/sign-in`
80
+ - App shell:
81
+ - `client/app/(app)/_layout.tsx` uses `NativeTabs` from `expo-router/unstable-native-tabs`
82
+ - tabs are explicitly registered for `index`, `push`, and `settings`
83
+ - Auth screens are custom flows in:
84
+ - `client/app/(auth)/sign-in.tsx`
85
+ - `client/app/(auth)/sign-up.tsx`
86
+
87
+ Important: never store auth/session tokens in MMKV.
88
+
89
+ ## Uniwind Integration Rules
90
+
91
+ Uniwind integration is intentionally specific. Keep these rules:
92
+
93
+ 1. `client/global.css` must contain:
94
+ - `@import "tailwindcss";`
95
+ - `@import "uniwind";`
96
+ 2. Import `../global.css` in `client/app/_layout.tsx`.
97
+ 3. Keep Metro wrapped by `withUniwindConfig(...)` in `client/metro.config.js` with:
98
+ - `cssEntryFile: "./global.css"`
99
+ - `dtsFile: "./uniwind-types.d.ts"`
100
+ 4. For third-party components without `className` support (for example `SafeAreaView`), wrap with `withUniwind(...)`.
101
+ 5. Theming defaults to `system`; explicit `light`/`dark`/`system` is handled by preferences store + theme sync hook:
102
+ - `client/src/store/preferences-store.ts`
103
+ - `client/src/hooks/use-theme-sync.ts`
104
+
105
+ ## Client Data/State Notes
106
+
107
+ - MMKV + Zustand is used for app preferences, not secrets.
108
+ - Convex API imports in client use the alias path:
109
+ - `@convex/_generated/api`
110
+ - Alias is configured in `client/tsconfig.json`.
111
+
112
+ ## Convex Backend Notes
113
+
114
+ - Convex directory: `server/convex/`
115
+ - Auth provider config: `server/convex/auth.config.ts`
116
+ - Main starter functions:
117
+ - `users.bootstrap`
118
+ - `push.registerToken`
119
+ - `push.unregisterToken`
120
+ - `push.sendTestNotification`
121
+ - Schema: `server/convex/schema.ts`
122
+ - Regenerate generated types after Convex setup:
123
+ - `cd server && bun run codegen`
124
+
125
+ ## Prebuild Policy
126
+
127
+ - `just prebuild` is supported for local native runs.
128
+ - `client/ios` and `client/android` are intentionally gitignored.
129
+ - Do not commit generated native folders in this starter.
130
+
131
+ ## Testing Expectations
132
+
133
+ - Run `just ci` before handing off significant changes.
134
+ - Server tests use `convex-test`; keep tests deterministic and isolated.
135
+ - If changing auth/push flows, verify both happy path and obvious edge cases.
136
+
137
+ ## CI and Quality Gates
138
+
139
+ CI (`.github/workflows/ci.yml`) runs:
140
+
141
+ 1. install (`bun install --workspaces`)
142
+ 2. validate (lint + typecheck + tests for client and server)
143
+ 3. native Android build on Linux (`arm64-v8a`)
144
+ 4. native iOS simulator build on `macos-26` (`arm64`)
145
+
146
+ Keep local changes compatible with these checks.
147
+
148
+ ## Common Troubleshooting
149
+
150
+ - Convex asks for missing env var on startup:
151
+ - set required variable in Convex dashboard deployment environment variables.
152
+ - Clerk says user is already signed in on auth pages:
153
+ - ensure route guards are active and auth pages redirect signed-in users.
154
+ - if simulator state is stale, clear app data/reinstall app and retry.
155
+ - Uniwind styles missing:
156
+ - verify `global.css` imports and `withUniwindConfig` metro wrapper.