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
package/README.md ADDED
@@ -0,0 +1,24 @@
1
+ # create-react-native-airborne
2
+
3
+ Create projects from the React Native Airborne template.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ bun create react-native-airborne my-app
9
+ ```
10
+
11
+ ## Flags
12
+
13
+ - `--skip-install`: skip `bun install --workspaces`
14
+ - `--no-git`: skip `git init`
15
+
16
+ ## Development
17
+
18
+ From repo root:
19
+
20
+ ```bash
21
+ cd tooling/create-react-native-airborne && bun run sync-template
22
+ ```
23
+
24
+ This refreshes `template/` from the current root starter files.
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "create-react-native-airborne",
3
+ "version": "0.0.1",
4
+ "description": "Scaffold a react-native-airborne starter project",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-react-native-airborne": "./src/index.mjs"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "template"
12
+ ],
13
+ "scripts": {
14
+ "sync-template": "bun ./scripts/sync-template.ts",
15
+ "prepare": "bun run sync-template",
16
+ "prepack": "bun run sync-template"
17
+ },
18
+ "devDependencies": {
19
+ "bun-types": "^1.3.4"
20
+ }
21
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ import { mkdir, readdir, readFile, writeFile, copyFile, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ const args = process.argv.slice(2);
8
+ const flags = new Set(args.filter((arg) => arg.startsWith("--")));
9
+ const projectNameArg = args.find((arg) => !arg.startsWith("--"));
10
+
11
+ if (!projectNameArg) {
12
+ console.error("Usage: bun create react-native-airborne <project-name> [--skip-install] [--no-git]");
13
+ process.exit(1);
14
+ }
15
+
16
+ const projectName = projectNameArg.trim();
17
+ const projectSlug = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
18
+ const targetDir = path.resolve(process.cwd(), projectName);
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+ const templateDir = path.resolve(__dirname, "../template");
21
+
22
+ async function exists(filePath) {
23
+ try {
24
+ await stat(filePath);
25
+ return true;
26
+ } catch {
27
+ return false;
28
+ }
29
+ }
30
+
31
+ async function copyDirectory(src, dest) {
32
+ await mkdir(dest, { recursive: true });
33
+ const entries = await readdir(src, { withFileTypes: true });
34
+
35
+ for (const entry of entries) {
36
+ const srcPath = path.join(src, entry.name);
37
+ const destPath = path.join(dest, entry.name);
38
+
39
+ if (entry.isDirectory()) {
40
+ await copyDirectory(srcPath, destPath);
41
+ } else {
42
+ await copyFile(srcPath, destPath);
43
+ }
44
+ }
45
+ }
46
+
47
+ async function replaceTokens(dir) {
48
+ const entries = await readdir(dir, { withFileTypes: true });
49
+ for (const entry of entries) {
50
+ const filePath = path.join(dir, entry.name);
51
+ if (entry.isDirectory()) {
52
+ await replaceTokens(filePath);
53
+ continue;
54
+ }
55
+
56
+ if (!/\.(json|md|ts|tsx|js|css|yml|yaml|env|toml|txt)$/.test(entry.name)) {
57
+ continue;
58
+ }
59
+
60
+ const content = await readFile(filePath, "utf8");
61
+ const updated = content
62
+ .replaceAll("__APP_NAME__", projectName)
63
+ .replaceAll("__APP_SLUG__", projectSlug)
64
+ .replaceAll("__APP_BUNDLE_ID__", `com.${projectSlug.replace(/-/g, "")}.app`);
65
+
66
+ if (updated !== content) {
67
+ await writeFile(filePath, updated, "utf8");
68
+ }
69
+ }
70
+ }
71
+
72
+ if (await exists(targetDir)) {
73
+ const contents = await readdir(targetDir);
74
+ if (contents.length > 0) {
75
+ console.error(`Target directory is not empty: ${targetDir}`);
76
+ process.exit(1);
77
+ }
78
+ }
79
+
80
+ await copyDirectory(templateDir, targetDir);
81
+ await replaceTokens(targetDir);
82
+
83
+ if (!flags.has("--skip-install")) {
84
+ const install = spawnSync("bun", ["install", "--workspaces"], {
85
+ cwd: targetDir,
86
+ stdio: "inherit",
87
+ });
88
+
89
+ if (install.status !== 0) {
90
+ process.exit(install.status ?? 1);
91
+ }
92
+ }
93
+
94
+ if (!flags.has("--no-git")) {
95
+ spawnSync("git", ["init"], { cwd: targetDir, stdio: "inherit" });
96
+ }
97
+
98
+ console.log(`\nCreated ${projectName} at ${targetDir}`);
99
+ console.log("\nNext steps:");
100
+ console.log(` cd ${projectName}`);
101
+ console.log(" cp client/.env.example client/.env");
102
+ console.log(" cp server/.env.example server/.env");
103
+ console.log(" just dev");
@@ -0,0 +1,333 @@
1
+ ---
2
+ name: Convex Best Practices
3
+ description: Guidelines for building production-ready Convex apps covering function organization, query patterns, validation, TypeScript usage, error handling, and the Zen of Convex design philosophy
4
+ version: 1.0.0
5
+ author: Convex
6
+ tags: [convex, best-practices, typescript, production, error-handling]
7
+ ---
8
+
9
+ # Convex Best Practices
10
+
11
+ Build production-ready Convex applications by following established patterns for function organization, query optimization, validation, TypeScript usage, and error handling.
12
+
13
+ ## Documentation Sources
14
+
15
+ Before implementing, do not assume; fetch the latest documentation:
16
+
17
+ - Primary: https://docs.convex.dev/understanding/best-practices/
18
+ - Error Handling: https://docs.convex.dev/functions/error-handling
19
+ - Write Conflicts: https://docs.convex.dev/error#1
20
+ - For broader context: https://docs.convex.dev/llms.txt
21
+
22
+ ## Instructions
23
+
24
+ ### The Zen of Convex
25
+
26
+ 1. **Convex manages the hard parts** - Let Convex handle caching, real-time sync, and consistency
27
+ 2. **Functions are the API** - Design your functions as your application's interface
28
+ 3. **Schema is truth** - Define your data model explicitly in schema.ts
29
+ 4. **TypeScript everywhere** - Leverage end-to-end type safety
30
+ 5. **Queries are reactive** - Think in terms of subscriptions, not requests
31
+
32
+ ### Function Organization
33
+
34
+ Organize your Convex functions by domain:
35
+
36
+ ```typescript
37
+ // convex/users.ts - User-related functions
38
+ import { query, mutation } from "./_generated/server";
39
+ import { v } from "convex/values";
40
+
41
+ export const get = query({
42
+ args: { userId: v.id("users") },
43
+ returns: v.union(v.object({
44
+ _id: v.id("users"),
45
+ _creationTime: v.number(),
46
+ name: v.string(),
47
+ email: v.string(),
48
+ }), v.null()),
49
+ handler: async (ctx, args) => {
50
+ return await ctx.db.get(args.userId);
51
+ },
52
+ });
53
+ ```
54
+
55
+ ### Argument and Return Validation
56
+
57
+ Always define validators for arguments AND return types:
58
+
59
+ ```typescript
60
+ export const createTask = mutation({
61
+ args: {
62
+ title: v.string(),
63
+ description: v.optional(v.string()),
64
+ priority: v.union(v.literal("low"), v.literal("medium"), v.literal("high")),
65
+ },
66
+ returns: v.id("tasks"),
67
+ handler: async (ctx, args) => {
68
+ return await ctx.db.insert("tasks", {
69
+ title: args.title,
70
+ description: args.description,
71
+ priority: args.priority,
72
+ completed: false,
73
+ createdAt: Date.now(),
74
+ });
75
+ },
76
+ });
77
+ ```
78
+
79
+ ### Query Patterns
80
+
81
+ Use indexes instead of filters for efficient queries:
82
+
83
+ ```typescript
84
+ // Schema with index
85
+ export default defineSchema({
86
+ tasks: defineTable({
87
+ userId: v.id("users"),
88
+ status: v.string(),
89
+ createdAt: v.number(),
90
+ })
91
+ .index("by_user", ["userId"])
92
+ .index("by_user_and_status", ["userId", "status"]),
93
+ });
94
+
95
+ // Query using index
96
+ export const getTasksByUser = query({
97
+ args: { userId: v.id("users") },
98
+ returns: v.array(v.object({
99
+ _id: v.id("tasks"),
100
+ _creationTime: v.number(),
101
+ userId: v.id("users"),
102
+ status: v.string(),
103
+ createdAt: v.number(),
104
+ })),
105
+ handler: async (ctx, args) => {
106
+ return await ctx.db
107
+ .query("tasks")
108
+ .withIndex("by_user", (q) => q.eq("userId", args.userId))
109
+ .order("desc")
110
+ .collect();
111
+ },
112
+ });
113
+ ```
114
+
115
+ ### Error Handling
116
+
117
+ Use ConvexError for user-facing errors:
118
+
119
+ ```typescript
120
+ import { ConvexError } from "convex/values";
121
+
122
+ export const updateTask = mutation({
123
+ args: {
124
+ taskId: v.id("tasks"),
125
+ title: v.string(),
126
+ },
127
+ returns: v.null(),
128
+ handler: async (ctx, args) => {
129
+ const task = await ctx.db.get(args.taskId);
130
+
131
+ if (!task) {
132
+ throw new ConvexError({
133
+ code: "NOT_FOUND",
134
+ message: "Task not found",
135
+ });
136
+ }
137
+
138
+ await ctx.db.patch(args.taskId, { title: args.title });
139
+ return null;
140
+ },
141
+ });
142
+ ```
143
+
144
+ ### Avoiding Write Conflicts (Optimistic Concurrency Control)
145
+
146
+ Convex uses OCC. Follow these patterns to minimize conflicts:
147
+
148
+ ```typescript
149
+ // GOOD: Make mutations idempotent
150
+ export const completeTask = mutation({
151
+ args: { taskId: v.id("tasks") },
152
+ returns: v.null(),
153
+ handler: async (ctx, args) => {
154
+ const task = await ctx.db.get(args.taskId);
155
+
156
+ // Early return if already complete (idempotent)
157
+ if (!task || task.status === "completed") {
158
+ return null;
159
+ }
160
+
161
+ await ctx.db.patch(args.taskId, {
162
+ status: "completed",
163
+ completedAt: Date.now(),
164
+ });
165
+ return null;
166
+ },
167
+ });
168
+
169
+ // GOOD: Patch directly without reading first when possible
170
+ export const updateNote = mutation({
171
+ args: { id: v.id("notes"), content: v.string() },
172
+ returns: v.null(),
173
+ handler: async (ctx, args) => {
174
+ // Patch directly - ctx.db.patch throws if document doesn't exist
175
+ await ctx.db.patch(args.id, { content: args.content });
176
+ return null;
177
+ },
178
+ });
179
+
180
+ // GOOD: Use Promise.all for parallel independent updates
181
+ export const reorderItems = mutation({
182
+ args: { itemIds: v.array(v.id("items")) },
183
+ returns: v.null(),
184
+ handler: async (ctx, args) => {
185
+ const updates = args.itemIds.map((id, index) =>
186
+ ctx.db.patch(id, { order: index })
187
+ );
188
+ await Promise.all(updates);
189
+ return null;
190
+ },
191
+ });
192
+ ```
193
+
194
+ ### TypeScript Best Practices
195
+
196
+ ```typescript
197
+ import { Id, Doc } from "./_generated/dataModel";
198
+
199
+ // Use Id type for document references
200
+ type UserId = Id<"users">;
201
+
202
+ // Use Doc type for full documents
203
+ type User = Doc<"users">;
204
+
205
+ // Define Record types properly
206
+ const userScores: Record<Id<"users">, number> = {};
207
+ ```
208
+
209
+ ### Internal vs Public Functions
210
+
211
+ ```typescript
212
+ // Public function - exposed to clients
213
+ export const getUser = query({
214
+ args: { userId: v.id("users") },
215
+ returns: v.union(v.null(), v.object({ /* ... */ })),
216
+ handler: async (ctx, args) => {
217
+ // ...
218
+ },
219
+ });
220
+
221
+ // Internal function - only callable from other Convex functions
222
+ export const _updateUserStats = internalMutation({
223
+ args: { userId: v.id("users") },
224
+ returns: v.null(),
225
+ handler: async (ctx, args) => {
226
+ // ...
227
+ },
228
+ });
229
+ ```
230
+
231
+ ## Examples
232
+
233
+ ### Complete CRUD Pattern
234
+
235
+ ```typescript
236
+ // convex/tasks.ts
237
+ import { query, mutation } from "./_generated/server";
238
+ import { v } from "convex/values";
239
+ import { ConvexError } from "convex/values";
240
+
241
+ const taskValidator = v.object({
242
+ _id: v.id("tasks"),
243
+ _creationTime: v.number(),
244
+ title: v.string(),
245
+ completed: v.boolean(),
246
+ userId: v.id("users"),
247
+ });
248
+
249
+ export const list = query({
250
+ args: { userId: v.id("users") },
251
+ returns: v.array(taskValidator),
252
+ handler: async (ctx, args) => {
253
+ return await ctx.db
254
+ .query("tasks")
255
+ .withIndex("by_user", (q) => q.eq("userId", args.userId))
256
+ .collect();
257
+ },
258
+ });
259
+
260
+ export const create = mutation({
261
+ args: {
262
+ title: v.string(),
263
+ userId: v.id("users"),
264
+ },
265
+ returns: v.id("tasks"),
266
+ handler: async (ctx, args) => {
267
+ return await ctx.db.insert("tasks", {
268
+ title: args.title,
269
+ completed: false,
270
+ userId: args.userId,
271
+ });
272
+ },
273
+ });
274
+
275
+ export const update = mutation({
276
+ args: {
277
+ taskId: v.id("tasks"),
278
+ title: v.optional(v.string()),
279
+ completed: v.optional(v.boolean()),
280
+ },
281
+ returns: v.null(),
282
+ handler: async (ctx, args) => {
283
+ const { taskId, ...updates } = args;
284
+
285
+ // Remove undefined values
286
+ const cleanUpdates = Object.fromEntries(
287
+ Object.entries(updates).filter(([_, v]) => v !== undefined)
288
+ );
289
+
290
+ if (Object.keys(cleanUpdates).length > 0) {
291
+ await ctx.db.patch(taskId, cleanUpdates);
292
+ }
293
+ return null;
294
+ },
295
+ });
296
+
297
+ export const remove = mutation({
298
+ args: { taskId: v.id("tasks") },
299
+ returns: v.null(),
300
+ handler: async (ctx, args) => {
301
+ await ctx.db.delete(args.taskId);
302
+ return null;
303
+ },
304
+ });
305
+ ```
306
+
307
+ ## Best Practices
308
+
309
+ - Never run `npx convex deploy` unless explicitly instructed
310
+ - Never run any git commands unless explicitly instructed
311
+ - Always define return validators for functions
312
+ - Use indexes for all queries that filter data
313
+ - Make mutations idempotent to handle retries gracefully
314
+ - Use ConvexError for user-facing error messages
315
+ - Organize functions by domain (users.ts, tasks.ts, etc.)
316
+ - Use internal functions for sensitive operations
317
+ - Leverage TypeScript's Id and Doc types
318
+
319
+ ## Common Pitfalls
320
+
321
+ 1. **Using filter instead of withIndex** - Always define indexes and use withIndex
322
+ 2. **Missing return validators** - Always specify the returns field
323
+ 3. **Non-idempotent mutations** - Check current state before updating
324
+ 4. **Reading before patching unnecessarily** - Patch directly when possible
325
+ 5. **Not handling null returns** - Document IDs might not exist
326
+
327
+ ## References
328
+
329
+ - Convex Documentation: https://docs.convex.dev/
330
+ - Convex LLMs.txt: https://docs.convex.dev/llms.txt
331
+ - Best Practices: https://docs.convex.dev/understanding/best-practices/
332
+ - Error Handling: https://docs.convex.dev/functions/error-handling
333
+ - Write Conflicts: https://docs.convex.dev/error#1