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.
- package/README.md +24 -0
- package/package.json +21 -0
- package/src/index.mjs +103 -0
- package/template/.agents/skills/convex-best-practices/SKILL.md +333 -0
- package/template/.agents/skills/convex-file-storage/SKILL.md +466 -0
- package/template/.agents/skills/convex-security-audit/SKILL.md +538 -0
- package/template/.agents/skills/convex-security-check/SKILL.md +377 -0
- package/template/.github/workflows/ci.yml +130 -0
- package/template/.prettierignore +8 -0
- package/template/.prettierrc.json +6 -0
- package/template/AGENTS.md +156 -0
- package/template/Justfile +48 -0
- package/template/README.md +94 -0
- package/template/client/.env.example +3 -0
- package/template/client/.vscode/extensions.json +1 -0
- package/template/client/.vscode/settings.json +7 -0
- package/template/client/README.md +33 -0
- package/template/client/app/(app)/_layout.tsx +34 -0
- package/template/client/app/(app)/index.tsx +66 -0
- package/template/client/app/(app)/push.tsx +75 -0
- package/template/client/app/(app)/settings.tsx +36 -0
- package/template/client/app/(auth)/_layout.tsx +22 -0
- package/template/client/app/(auth)/sign-in.tsx +358 -0
- package/template/client/app/(auth)/sign-up.tsx +237 -0
- package/template/client/app/_layout.tsx +30 -0
- package/template/client/app/index.tsx +127 -0
- package/template/client/app.config.ts +30 -0
- package/template/client/assets/images/android-icon-background.png +0 -0
- package/template/client/assets/images/android-icon-foreground.png +0 -0
- package/template/client/assets/images/android-icon-monochrome.png +0 -0
- package/template/client/assets/images/favicon.png +0 -0
- package/template/client/assets/images/icon.png +0 -0
- package/template/client/assets/images/partial-react-logo.png +0 -0
- package/template/client/assets/images/react-logo.png +0 -0
- package/template/client/assets/images/react-logo@2x.png +0 -0
- package/template/client/assets/images/react-logo@3x.png +0 -0
- package/template/client/assets/images/splash-icon.png +0 -0
- package/template/client/eslint.config.js +10 -0
- package/template/client/global.css +2 -0
- package/template/client/metro.config.js +9 -0
- package/template/client/package.json +51 -0
- package/template/client/src/components/auth-shell.tsx +63 -0
- package/template/client/src/components/form-input.tsx +62 -0
- package/template/client/src/components/primary-button.tsx +37 -0
- package/template/client/src/components/screen.tsx +17 -0
- package/template/client/src/components/sign-out-button.tsx +32 -0
- package/template/client/src/hooks/use-theme-sync.ts +11 -0
- package/template/client/src/lib/convex.ts +6 -0
- package/template/client/src/lib/env-schema.ts +13 -0
- package/template/client/src/lib/env.test.ts +24 -0
- package/template/client/src/lib/env.ts +19 -0
- package/template/client/src/lib/notifications.ts +47 -0
- package/template/client/src/store/preferences-store.ts +42 -0
- package/template/client/src/types/theme.ts +1 -0
- package/template/client/tsconfig.json +18 -0
- package/template/client/uniwind-types.d.ts +10 -0
- package/template/client/vitest.config.ts +7 -0
- package/template/package.json +22 -0
- package/template/server/.env.example +8 -0
- package/template/server/README.md +31 -0
- package/template/server/convex/_generated/api.d.ts +55 -0
- package/template/server/convex/_generated/api.js +23 -0
- package/template/server/convex/_generated/dataModel.d.ts +60 -0
- package/template/server/convex/_generated/server.d.ts +143 -0
- package/template/server/convex/_generated/server.js +93 -0
- package/template/server/convex/auth.config.ts +11 -0
- package/template/server/convex/env.ts +18 -0
- package/template/server/convex/lib.ts +12 -0
- package/template/server/convex/push.ts +148 -0
- package/template/server/convex/schema.ts +22 -0
- package/template/server/convex/users.ts +54 -0
- package/template/server/convex.json +3 -0
- package/template/server/eslint.config.js +51 -0
- package/template/server/package.json +29 -0
- package/template/server/tests/convex.test.ts +52 -0
- package/template/server/tests/import-meta.d.ts +3 -0
- package/template/server/tsconfig.json +15 -0
- 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,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.
|