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,48 @@
1
+ set shell := ["zsh", "-cu"]
2
+
3
+ install:
4
+ bun install --workspaces
5
+
6
+ fmt:
7
+ bunx prettier --write client server
8
+
9
+ prebuild:
10
+ cd client && bunx expo prebuild --platform all
11
+
12
+ dev:
13
+ bunx concurrently -n CLIENT,SERVER -c blue,green "just dev-client" "just dev-server"
14
+
15
+ dev-client:
16
+ cd client && bun run start
17
+
18
+ dev-server:
19
+ cd server && bun run dev
20
+
21
+ ios:
22
+ cd client && bun run ios
23
+
24
+ android:
25
+ cd client && bun run android
26
+
27
+ lint:
28
+ cd client && bun run lint
29
+ cd server && bun run lint
30
+
31
+ typecheck:
32
+ cd client && bun run typecheck
33
+ cd server && bun run typecheck
34
+
35
+ test:
36
+ cd client && bun run test
37
+ cd server && bun run test
38
+
39
+ test-client:
40
+ cd client && bun run test
41
+
42
+ test-server:
43
+ cd server && bun run test
44
+
45
+ ci:
46
+ just lint
47
+ just typecheck
48
+ just test
@@ -0,0 +1,94 @@
1
+ # React Native Airborne
2
+
3
+ Opinionated React Native starter for mobile-first apps with Expo + Convex.
4
+
5
+ ## 🧰 Stack
6
+
7
+ - Bun workspaces monorepo (`client/`, `server/`)
8
+ - Expo + Expo Router + Native Tabs (SDK 55)
9
+ - Uniwind + Tailwind v4
10
+ - Clerk authentication
11
+ - Convex backend + `convex-test`
12
+ - Zustand + MMKV persistence
13
+ - Expo push notifications
14
+
15
+ ## 🤝 Contributor Guide
16
+
17
+ Detailed implementation and maintenance notes for engineers/agents live in `AGENTS.md`.
18
+
19
+ ## ✅ Prerequisites
20
+
21
+ - Bun `1.3.4+`
22
+ - `just` command runner
23
+ - Expo toolchain for iOS/Android simulators
24
+ - Clerk app + Convex project
25
+
26
+ ## ⚡ Quickstart
27
+
28
+ ```bash
29
+ bun install --workspaces
30
+ cp client/.env.example client/.env
31
+ cp server/.env.example server/.env
32
+ ```
33
+
34
+ First-time Convex setup (one-time per new deployment):
35
+
36
+ ```bash
37
+ cd server
38
+ bun run dev
39
+ ```
40
+
41
+ Then run both apps:
42
+
43
+ ```bash
44
+ just dev
45
+ ```
46
+
47
+ ## 🧪 Commands
48
+
49
+ - `just dev`: start Expo + Convex
50
+ - `just dev-client`: start Expo only
51
+ - `just dev-server`: start Convex only
52
+ - `just fmt`: run Prettier on client and server
53
+ - `just prebuild`: generate local iOS/Android native folders
54
+ - `just ios`: launch iOS app
55
+ - `just android`: launch Android app
56
+ - `just lint`: lint/type lint checks
57
+ - `just typecheck`: TypeScript checks
58
+ - `just test`: client + server tests
59
+ - `just test-client`: client tests only
60
+ - `just test-server`: server tests only
61
+ - `just ci`: lint + typecheck + tests
62
+
63
+ ## 📱 Native Projects
64
+
65
+ Generate native projects locally when needed:
66
+
67
+ ```bash
68
+ just prebuild
69
+ ```
70
+
71
+ `client/ios` and `client/android` are intentionally gitignored and should not be committed.
72
+
73
+ ## 🔐 Environment Variables
74
+
75
+ ### Client (`client/.env`)
76
+
77
+ - `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY`
78
+ - `EXPO_PUBLIC_CONVEX_URL`
79
+ - `EXPO_PUBLIC_EAS_PROJECT_ID` (optional)
80
+
81
+ ### Server (`server/.env`)
82
+
83
+ - `CLERK_JWT_ISSUER_DOMAIN`
84
+ - `EXPO_PUSH_ENDPOINT` (optional)
85
+ - `EXPO_ACCESS_TOKEN` (optional)
86
+
87
+ ## 📝 Notes
88
+
89
+ - Mobile-only target (iOS/Android).
90
+ - Do not store sensitive auth tokens in MMKV.
91
+ - Uniwind classes are enabled by `client/global.css` and `client/metro.config.js`.
92
+ - `SafeAreaView` is wrapped with `withUniwind` in `client/src/components/screen.tsx` for className support.
93
+ - `server/convex/_generated` ships with starter stubs so typecheck/tests pass before deployment setup.
94
+ After connecting Convex, run `cd server && bun run codegen` to regenerate.
@@ -0,0 +1,3 @@
1
+ EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY=
2
+ EXPO_PUBLIC_CONVEX_URL=
3
+ EXPO_PUBLIC_EAS_PROJECT_ID=
@@ -0,0 +1 @@
1
+ { "recommendations": ["expo.vscode-expo-tools"] }
@@ -0,0 +1,7 @@
1
+ {
2
+ "editor.codeActionsOnSave": {
3
+ "source.fixAll": "explicit",
4
+ "source.organizeImports": "explicit",
5
+ "source.sortMembers": "explicit"
6
+ }
7
+ }
@@ -0,0 +1,33 @@
1
+ # Airborne Client
2
+
3
+ Expo Router app configured for:
4
+
5
+ - Clerk authentication
6
+ - Convex client integration
7
+ - Uniwind (Tailwind v4)
8
+ - Zustand + MMKV local state persistence
9
+ - Expo push notifications
10
+
11
+ ## Run
12
+
13
+ ```bash
14
+ bun install
15
+ bun run start
16
+ ```
17
+
18
+ Generate native folders locally when you need native runs:
19
+
20
+ ```bash
21
+ bunx expo prebuild --platform all
22
+ ```
23
+
24
+ Then use:
25
+
26
+ - `bun run ios`
27
+ - `bun run android`
28
+
29
+ ## Required env (`.env`)
30
+
31
+ - `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY`
32
+ - `EXPO_PUBLIC_CONVEX_URL`
33
+ - `EXPO_PUBLIC_EAS_PROJECT_ID` (optional)
@@ -0,0 +1,34 @@
1
+ import { useAuth } from "@clerk/clerk-expo";
2
+ import { Redirect } from "expo-router";
3
+ import { NativeTabs } from "expo-router/unstable-native-tabs";
4
+
5
+ export default function AppLayout() {
6
+ const { isLoaded, isSignedIn } = useAuth();
7
+
8
+ if (!isLoaded) {
9
+ return null;
10
+ }
11
+
12
+ if (!isSignedIn) {
13
+ return <Redirect href="/(auth)/sign-in" />;
14
+ }
15
+
16
+ return (
17
+ <NativeTabs>
18
+ <NativeTabs.Trigger name="index" disableTransparentOnScrollEdge>
19
+ <NativeTabs.Trigger.Icon sf={{ default: "house", selected: "house.fill" }} md="home" />
20
+ <NativeTabs.Trigger.Label>Home</NativeTabs.Trigger.Label>
21
+ </NativeTabs.Trigger>
22
+
23
+ <NativeTabs.Trigger name="push" disableTransparentOnScrollEdge>
24
+ <NativeTabs.Trigger.Icon sf={{ default: "bell", selected: "bell.fill" }} md="notifications" />
25
+ <NativeTabs.Trigger.Label>Push</NativeTabs.Trigger.Label>
26
+ </NativeTabs.Trigger>
27
+
28
+ <NativeTabs.Trigger name="settings" disableTransparentOnScrollEdge>
29
+ <NativeTabs.Trigger.Icon sf="gearshape" md="settings" />
30
+ <NativeTabs.Trigger.Label>Settings</NativeTabs.Trigger.Label>
31
+ </NativeTabs.Trigger>
32
+ </NativeTabs>
33
+ );
34
+ }
@@ -0,0 +1,66 @@
1
+ import { SignedIn, useUser } from "@clerk/clerk-expo";
2
+ import { useMutation, useQuery } from "convex/react";
3
+ import { Link } from "expo-router";
4
+ import { useEffect, useRef } from "react";
5
+ import { Text, View } from "react-native";
6
+ import { Screen } from "@/src/components/screen";
7
+ import { SignOutButton } from "@/src/components/sign-out-button";
8
+ import { api } from "@convex/_generated/api";
9
+
10
+ export default function HomeScreen() {
11
+ const { user } = useUser();
12
+ const currentUser = useQuery(api.users.current, {});
13
+ const bootstrapUser = useMutation(api.users.bootstrap);
14
+ const didBootstrap = useRef(false);
15
+
16
+ useEffect(() => {
17
+ if (!user || currentUser !== null || didBootstrap.current) {
18
+ return;
19
+ }
20
+
21
+ didBootstrap.current = true;
22
+ void bootstrapUser({
23
+ email: user.primaryEmailAddress?.emailAddress,
24
+ name: user.fullName ?? undefined,
25
+ imageUrl: user.imageUrl ?? undefined,
26
+ });
27
+ }, [bootstrapUser, currentUser, user]);
28
+
29
+ return (
30
+ <Screen>
31
+ <SignedIn>
32
+ <View className="flex-1 gap-5">
33
+ <View className="gap-2">
34
+ <Text className="text-3xl font-bold text-zinc-900 dark:text-zinc-100">
35
+ React Native Airborne
36
+ </Text>
37
+ <Text className="text-zinc-600 dark:text-zinc-300">
38
+ Opinionated Expo + Convex + Clerk starter with Uniwind.
39
+ </Text>
40
+ </View>
41
+
42
+ <View className="rounded-xl border border-zinc-300 bg-zinc-50 p-4 dark:border-zinc-800 dark:bg-zinc-900">
43
+ <Text className="text-sm text-zinc-600 dark:text-zinc-300">Authenticated user</Text>
44
+ <Text className="text-base font-semibold text-zinc-900 dark:text-zinc-100">
45
+ {currentUser?.email ?? "Loading profile..."}
46
+ </Text>
47
+ <Text className="text-sm text-zinc-500 dark:text-zinc-400">
48
+ Clerk ID: {currentUser?.clerkUserId ?? "..."}
49
+ </Text>
50
+ </View>
51
+
52
+ <View className="gap-3">
53
+ <Link href="/(app)/settings" className="text-base font-medium text-blue-600">
54
+ Open Settings (theme)
55
+ </Link>
56
+ <Link href="/(app)/push" className="text-base font-medium text-blue-600">
57
+ Open Push Demo
58
+ </Link>
59
+ </View>
60
+
61
+ <SignOutButton />
62
+ </View>
63
+ </SignedIn>
64
+ </Screen>
65
+ );
66
+ }
@@ -0,0 +1,75 @@
1
+ import { useAction, useMutation } from "convex/react";
2
+ import { useState } from "react";
3
+ import { Platform, Text, View } from "react-native";
4
+ import { PrimaryButton } from "@/src/components/primary-button";
5
+ import { Screen } from "@/src/components/screen";
6
+ import { registerForPushNotificationsAsync } from "@/src/lib/notifications";
7
+ import { usePreferencesStore } from "@/src/store/preferences-store";
8
+ import { api } from "@convex/_generated/api";
9
+
10
+ export default function PushScreen() {
11
+ const [status, setStatus] = useState<string>("Idle");
12
+ const lastPushToken = usePreferencesStore((state) => state.lastPushToken);
13
+ const setLastPushToken = usePreferencesStore((state) => state.setLastPushToken);
14
+
15
+ const registerToken = useMutation(api.push.registerToken);
16
+ const unregisterToken = useMutation(api.push.unregisterToken);
17
+ const sendTestNotification = useAction(api.push.sendTestNotification);
18
+
19
+ const onRegister = async () => {
20
+ try {
21
+ setStatus("Requesting notification permissions...");
22
+ const token = await registerForPushNotificationsAsync();
23
+ setStatus("Registering token in Convex...");
24
+ await registerToken({ token, platform: Platform.OS === "ios" ? "ios" : "android" });
25
+ setLastPushToken(token);
26
+ setStatus("Token registered.");
27
+ } catch (error) {
28
+ setStatus(error instanceof Error ? error.message : "Failed to register push token.");
29
+ }
30
+ };
31
+
32
+ const onSendTest = async () => {
33
+ try {
34
+ setStatus("Sending test notification...");
35
+ const response = await sendTestNotification({});
36
+ setStatus(`Sent. Status ${response.status}.`);
37
+ } catch (error) {
38
+ setStatus(error instanceof Error ? error.message : "Failed to send notification.");
39
+ }
40
+ };
41
+
42
+ const onUnregister = async () => {
43
+ if (!lastPushToken) {
44
+ setStatus("No token stored to remove.");
45
+ return;
46
+ }
47
+
48
+ try {
49
+ await unregisterToken({ token: lastPushToken });
50
+ setLastPushToken(null);
51
+ setStatus("Token removed.");
52
+ } catch (error) {
53
+ setStatus(error instanceof Error ? error.message : "Failed to remove token.");
54
+ }
55
+ };
56
+
57
+ return (
58
+ <Screen>
59
+ <View className="flex-1 gap-4">
60
+ <Text className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">Push Demo</Text>
61
+
62
+ <Text className="text-sm text-zinc-600 dark:text-zinc-300">Status: {status}</Text>
63
+ <Text className="text-xs text-zinc-500 dark:text-zinc-400">
64
+ Last token: {lastPushToken ?? "Not registered yet"}
65
+ </Text>
66
+
67
+ <View className="gap-3">
68
+ <PrimaryButton onPress={onRegister}>Register push token</PrimaryButton>
69
+ <PrimaryButton onPress={onSendTest}>Send test notification</PrimaryButton>
70
+ <PrimaryButton onPress={onUnregister}>Remove token</PrimaryButton>
71
+ </View>
72
+ </View>
73
+ </Screen>
74
+ );
75
+ }
@@ -0,0 +1,36 @@
1
+ import { useUniwind } from "uniwind";
2
+ import { Text, View } from "react-native";
3
+ import { PrimaryButton } from "@/src/components/primary-button";
4
+ import { Screen } from "@/src/components/screen";
5
+ import { usePreferencesStore } from "@/src/store/preferences-store";
6
+ import type { ThemePreference } from "@/src/types/theme";
7
+
8
+ export default function SettingsScreen() {
9
+ const { theme, hasAdaptiveThemes } = useUniwind();
10
+ const selectedTheme = usePreferencesStore((state) => state.theme);
11
+ const setTheme = usePreferencesStore((state) => state.setTheme);
12
+
13
+ const activeTheme = hasAdaptiveThemes ? "system" : theme;
14
+
15
+ const onChangeTheme = (nextTheme: ThemePreference) => {
16
+ setTheme(nextTheme);
17
+ };
18
+
19
+ return (
20
+ <Screen>
21
+ <View className="flex-1 gap-4">
22
+ <Text className="text-2xl font-bold text-zinc-900 dark:text-zinc-100">Settings</Text>
23
+
24
+ <Text className="text-sm text-zinc-600 dark:text-zinc-300">
25
+ Active theme: {activeTheme}. Stored preference: {selectedTheme}.
26
+ </Text>
27
+
28
+ <View className="gap-3">
29
+ <PrimaryButton onPress={() => onChangeTheme("system")}>Use system theme</PrimaryButton>
30
+ <PrimaryButton onPress={() => onChangeTheme("light")}>Use light theme</PrimaryButton>
31
+ <PrimaryButton onPress={() => onChangeTheme("dark")}>Use dark theme</PrimaryButton>
32
+ </View>
33
+ </View>
34
+ </Screen>
35
+ );
36
+ }
@@ -0,0 +1,22 @@
1
+ import { useAuth } from "@clerk/clerk-expo";
2
+ import { Redirect, Stack } from "expo-router";
3
+
4
+ export default function AuthLayout() {
5
+ const { isLoaded, isSignedIn } = useAuth();
6
+
7
+ if (!isLoaded) {
8
+ return null;
9
+ }
10
+
11
+ if (isSignedIn) {
12
+ return <Redirect href="/" />;
13
+ }
14
+
15
+ return (
16
+ <Stack
17
+ screenOptions={{
18
+ headerShown: false,
19
+ }}
20
+ />
21
+ );
22
+ }