create-100x-mobile 0.4.11 → 0.5.0

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.
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workerMigrationTemplate = workerMigrationTemplate;
4
+ function workerMigrationTemplate() {
5
+ return `CREATE TABLE IF NOT EXISTS uploads (
6
+ id TEXT PRIMARY KEY,
7
+ object_key TEXT NOT NULL UNIQUE,
8
+ owner_id TEXT NOT NULL,
9
+ file_name TEXT NOT NULL,
10
+ content_type TEXT NOT NULL,
11
+ size INTEGER NOT NULL,
12
+ created_at TEXT NOT NULL
13
+ );
14
+
15
+ CREATE INDEX IF NOT EXISTS uploads_owner_id_created_at_idx
16
+ ON uploads (owner_id, created_at DESC);
17
+ `;
18
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.workerTsconfigTemplate = workerTsconfigTemplate;
4
+ function workerTsconfigTemplate() {
5
+ const config = {
6
+ compilerOptions: {
7
+ target: "ES2022",
8
+ module: "ESNext",
9
+ moduleResolution: "Bundler",
10
+ lib: ["ES2022"],
11
+ types: ["@cloudflare/workers-types", "node"],
12
+ strict: true,
13
+ noEmit: true,
14
+ allowImportingTsExtensions: true,
15
+ skipLibCheck: true,
16
+ },
17
+ include: ["src/**/*.ts", "../alchemy.run.ts"],
18
+ };
19
+ return JSON.stringify(config, null, 2) + "\n";
20
+ }
@@ -1,9 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.appJsonTemplate = appJsonTemplate;
4
- function appJsonTemplate(appName, backend) {
5
- const plugins = backend === "convex"
6
- ? ["expo-router", "expo-font", "expo-web-browser", "expo-secure-store"]
4
+ function appJsonTemplate(appName, backend, instantAuthMode = "clerk") {
5
+ const plugins = backend === "instantdb" &&
6
+ (instantAuthMode === "magic-code" ||
7
+ instantAuthMode === "magic-code-cloudflare")
8
+ ? ["expo-router", "expo-font"]
7
9
  : ["expo-router", "expo-font", "expo-web-browser", "expo-secure-store"];
8
10
  const config = {
9
11
  expo: {
@@ -1,8 +1,38 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.envExampleTemplate = envExampleTemplate;
4
- function envExampleTemplate(backend) {
4
+ function envExampleTemplate(backend, instantAuthMode = "clerk") {
5
5
  if (backend === "instantdb") {
6
+ if (instantAuthMode === "magic-code-cloudflare") {
7
+ return `# InstantDB
8
+ EXPO_PUBLIC_INSTANT_APP_ID=
9
+
10
+ # Cloudflare Worker storage API
11
+ # Set this after running: bun run cloudflare:deploy
12
+ EXPO_PUBLIC_STORAGE_WORKER_URL=
13
+
14
+ # Alchemy reads this while deploying Cloudflare storage.
15
+ # INSTANT_ADMIN_TOKEN stays server-side and is bound only to the Worker.
16
+ # You can also use EXPO_PUBLIC_INSTANT_APP_ID as the app id for local development.
17
+ INSTANT_APP_ID=
18
+ INSTANT_ADMIN_TOKEN=
19
+
20
+ # Comma-separated origins allowed by the storage Worker for web builds.
21
+ # Native mobile requests do not use browser CORS, but web uploads do.
22
+ ALLOWED_ORIGINS=http://localhost:3000,http://localhost:8080,http://localhost:8081,http://localhost:19006
23
+
24
+ # Storage Worker policy. Values are read by Alchemy and bound to the Worker.
25
+ MAX_UPLOAD_BYTES=26214400
26
+ USER_STORAGE_LIMIT_BYTES=524288000
27
+ DAILY_UPLOAD_LIMIT=100
28
+ ALLOWED_CONTENT_TYPES=image/jpeg,image/png,image/webp,application/pdf,text/plain
29
+ `;
30
+ }
31
+ if (instantAuthMode === "magic-code") {
32
+ return `# InstantDB
33
+ EXPO_PUBLIC_INSTANT_APP_ID=
34
+ `;
35
+ }
6
36
  return `# InstantDB
7
37
  EXPO_PUBLIC_INSTANT_APP_ID=
8
38
  EXPO_PUBLIC_INSTANT_CLERK_CLIENT_NAME=clerk
@@ -1,19 +1,54 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.packageJsonTemplate = packageJsonTemplate;
4
- function packageJsonTemplate(appName, expoVersion = "latest", backend = "convex") {
4
+ function isCloudflareInstantAuthMode(instantAuthMode) {
5
+ return instantAuthMode === "magic-code-cloudflare";
6
+ }
7
+ function isMagicCodeInstantAuthMode(instantAuthMode) {
8
+ return (instantAuthMode === "magic-code" ||
9
+ instantAuthMode === "magic-code-cloudflare");
10
+ }
11
+ function packageJsonTemplate(appName, expoVersion = "latest", backend = "convex", instantAuthMode = "clerk") {
5
12
  const backendDependencies = backend === "instantdb"
6
- ? {
7
- "@clerk/clerk-expo": "^2.14.24",
8
- "@instantdb/react-native": "^0.20.0",
9
- "@react-native-async-storage/async-storage": "^2.2.0",
10
- "@react-native-community/netinfo": "^11.4.1",
11
- "react-native-get-random-values": "^1.11.0",
12
- }
13
+ ? isMagicCodeInstantAuthMode(instantAuthMode)
14
+ ? {
15
+ ...(isCloudflareInstantAuthMode(instantAuthMode)
16
+ ? { "@instantdb/admin": "^1.0.15" }
17
+ : {}),
18
+ "@instantdb/react-native": "^0.20.0",
19
+ "@react-native-async-storage/async-storage": "^2.2.0",
20
+ "@react-native-community/netinfo": "^11.4.1",
21
+ "react-native-get-random-values": "^1.11.0",
22
+ }
23
+ : {
24
+ "@clerk/clerk-expo": "^2.14.24",
25
+ "@instantdb/react-native": "^0.20.0",
26
+ "@react-native-async-storage/async-storage": "^2.2.0",
27
+ "@react-native-community/netinfo": "^11.4.1",
28
+ "react-native-get-random-values": "^1.11.0",
29
+ }
13
30
  : {
14
31
  "@clerk/clerk-expo": "^2.14.24",
15
32
  convex: "^1.26.1",
16
33
  };
34
+ const cloudflareScripts = isCloudflareInstantAuthMode(instantAuthMode)
35
+ ? {
36
+ "cloudflare:configure": "alchemy configure",
37
+ "cloudflare:login": "alchemy login",
38
+ "cloudflare:dev": "alchemy dev",
39
+ "cloudflare:deploy": "alchemy deploy",
40
+ "cloudflare:destroy": "alchemy destroy",
41
+ "typecheck:worker": "tsc --noEmit -p worker/tsconfig.json",
42
+ }
43
+ : {};
44
+ const cloudflareDevDependencies = isCloudflareInstantAuthMode(instantAuthMode)
45
+ ? {
46
+ "@cloudflare/workers-types": "^4.20260424.1",
47
+ "@types/node": "^22.0.0",
48
+ alchemy: "^0.93.0",
49
+ wrangler: "^4.85.0",
50
+ }
51
+ : {};
17
52
  const pkg = {
18
53
  name: appName,
19
54
  main: "expo-router/entry",
@@ -24,6 +59,7 @@ function packageJsonTemplate(appName, expoVersion = "latest", backend = "convex"
24
59
  "build:web": "expo export --platform web",
25
60
  lint: "expo lint",
26
61
  format: "prettier --write .",
62
+ ...cloudflareScripts,
27
63
  },
28
64
  dependencies: {
29
65
  "@react-navigation/bottom-tabs": "^7.3.10",
@@ -39,6 +75,7 @@ function packageJsonTemplate(appName, expoVersion = "latest", backend = "convex"
39
75
  "eslint-config-expo": "~10.0.0",
40
76
  prettier: "^3.4.2",
41
77
  typescript: "~5.9.2",
78
+ ...cloudflareDevDependencies,
42
79
  },
43
80
  };
44
81
  return JSON.stringify(pkg, null, 2) + "\n";
@@ -1,8 +1,156 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readmeTemplate = readmeTemplate;
4
- function readmeTemplate(projectName, backend) {
4
+ function readmeTemplate(projectName, backend, instantAuthMode = "clerk") {
5
5
  if (backend === "instantdb") {
6
+ if (instantAuthMode === "magic-code-cloudflare") {
7
+ return `# ${projectName}
8
+
9
+ A mobile app built with **Expo**, **InstantDB magic code auth**, and **Cloudflare storage managed by Alchemy**.
10
+
11
+ ## Tech Stack
12
+
13
+ - **[Expo](https://expo.dev)** — React Native framework
14
+ - **[InstantDB](https://instantdb.com)** — Real-time backend, auth, and sync
15
+ - **[Cloudflare Workers](https://developers.cloudflare.com/workers/)** — Storage API
16
+ - **[Cloudflare R2](https://developers.cloudflare.com/r2/)** — Object storage
17
+ - **[Cloudflare D1](https://developers.cloudflare.com/d1/)** — Upload metadata database
18
+ - **[Alchemy](https://alchemy.run)** — TypeScript infrastructure for Cloudflare resources
19
+ - **[Expo Router](https://docs.expo.dev/router/introduction/)** — File-based navigation
20
+
21
+ ## Getting Started
22
+
23
+ \`\`\`bash
24
+ # Install dependencies
25
+ bun install
26
+
27
+ # Add your Instant app id
28
+ cp .env.example .env.local
29
+
30
+ # Start Expo
31
+ bunx expo start
32
+ \`\`\`
33
+
34
+ Set this in \`.env.local\`:
35
+ - \`EXPO_PUBLIC_INSTANT_APP_ID\`
36
+ - \`EXPO_PUBLIC_STORAGE_WORKER_URL\` after Cloudflare deploy
37
+ - \`INSTANT_APP_ID\` for Alchemy Cloudflare deploys; this can match \`EXPO_PUBLIC_INSTANT_APP_ID\`
38
+ - \`INSTANT_ADMIN_TOKEN\` for the Worker-only InstantDB metadata sync
39
+ - \`ALLOWED_ORIGINS\` with your production web origins before deploy
40
+ - \`MAX_UPLOAD_BYTES\`, \`USER_STORAGE_LIMIT_BYTES\`, \`DAILY_UPLOAD_LIMIT\`, and \`ALLOWED_CONTENT_TYPES\` for Worker policy
41
+
42
+ You can create an Instant app id via:
43
+
44
+ \`\`\`bash
45
+ npx instant-cli init-without-files --title ${projectName}
46
+ \`\`\`
47
+
48
+ ## Cloudflare Storage
49
+
50
+ This template uses Cloudflare Workers, R2, and D1 only. It does not use Durable Objects, Queues, Workflows, Cron Triggers, or other paid-plan Workers features.
51
+
52
+ \`\`\`bash
53
+ bun run cloudflare:configure
54
+ bun run cloudflare:login
55
+ bun run cloudflare:deploy
56
+ \`\`\`
57
+
58
+ After deploy, copy the printed \`storageWorkerUrl\` into \`.env.local\` as \`EXPO_PUBLIC_STORAGE_WORKER_URL\`.
59
+
60
+ The storage flow is:
61
+ 1. App sends the InstantDB refresh token to the Cloudflare Worker as a Bearer token.
62
+ 2. Worker verifies the token with \`@instantdb/admin\`.
63
+ 3. Worker derives the object owner from the verified InstantDB user.
64
+ 4. Worker stores the object in R2.
65
+ 5. Worker records metadata in D1.
66
+ 6. Worker syncs the canonical metadata to InstantDB with the admin SDK.
67
+ 7. App reads the realtime metadata from InstantDB.
68
+
69
+ Production hardening included in the starter:
70
+ - InstantDB token verification for upload, read, download, and delete routes
71
+ - Worker-owned InstantDB metadata writes using \`INSTANT_ADMIN_TOKEN\`
72
+ - Owner checks before reading, downloading, or deleting stored objects
73
+ - Configurable CORS allowlist through \`ALLOWED_ORIGINS\`
74
+ - Configurable content-type allowlist
75
+ - Streaming upload size enforcement
76
+ - Per-user storage quota and daily upload limit backed by D1 metadata
77
+ - Private Worker-served downloads; the R2 bucket is not exposed publicly
78
+
79
+ ## Scripts
80
+
81
+ | Command | Description |
82
+ |---------|-------------|
83
+ | \`bun run dev\` | Start Expo dev server |
84
+ | \`bun run lint\` | Run ESLint |
85
+ | \`bun run format\` | Format code with Prettier |
86
+ | \`bun run cloudflare:configure\` | Configure Alchemy Cloudflare credentials |
87
+ | \`bun run cloudflare:login\` | Login to Cloudflare through Alchemy |
88
+ | \`bun run cloudflare:dev\` | Run the Cloudflare Worker locally |
89
+ | \`bun run cloudflare:deploy\` | Deploy Worker, R2, and D1 with Alchemy |
90
+ | \`bun run typecheck:worker\` | Type-check the Worker template |
91
+
92
+ ## Learn More
93
+
94
+ - [Expo Docs](https://docs.expo.dev)
95
+ - [InstantDB Docs](https://instantdb.com/docs)
96
+ - [Magic Code Auth](https://instantdb.com/docs/auth/magic-codes)
97
+ - [Alchemy Docs](https://alchemy.run/getting-started)
98
+ - [Cloudflare Workers Docs](https://developers.cloudflare.com/workers/)
99
+ `;
100
+ }
101
+ if (instantAuthMode === "magic-code") {
102
+ return `# ${projectName}
103
+
104
+ A mobile app built with **Expo**, **InstantDB**, and **magic code auth**.
105
+
106
+ ## Tech Stack
107
+
108
+ - **[Expo](https://expo.dev)** — React Native framework
109
+ - **[InstantDB](https://instantdb.com)** — Real-time backend, auth, and sync
110
+ - **[Expo Router](https://docs.expo.dev/router/introduction/)** — File-based navigation
111
+
112
+ ## Getting Started
113
+
114
+ \`\`\`bash
115
+ # Install dependencies
116
+ bun install
117
+
118
+ # Add your Instant app id
119
+ cp .env.example .env.local
120
+
121
+ # Start Expo
122
+ bunx expo start
123
+ \`\`\`
124
+
125
+ Set this in \`.env.local\`:
126
+ - \`EXPO_PUBLIC_INSTANT_APP_ID\`
127
+
128
+ You can create an Instant app id via:
129
+
130
+ \`\`\`bash
131
+ npx instant-cli init-without-files --title ${projectName}
132
+ \`\`\`
133
+
134
+ InstantDB magic code auth setup:
135
+ 1. Enter your email in the sign-in screen.
136
+ 2. Instant sends a login code to that address.
137
+ 3. Paste the code to sign in.
138
+
139
+ ## Scripts
140
+
141
+ | Command | Description |
142
+ |---------|-------------|
143
+ | \`bun run dev\` | Start Expo dev server |
144
+ | \`bun run lint\` | Run ESLint |
145
+ | \`bun run format\` | Format code with Prettier |
146
+
147
+ ## Learn More
148
+
149
+ - [Expo Docs](https://docs.expo.dev)
150
+ - [InstantDB Docs](https://instantdb.com/docs)
151
+ - [Magic Code Auth](https://instantdb.com/docs/auth/magic-codes)
152
+ `;
153
+ }
6
154
  return `# ${projectName}
7
155
 
8
156
  A mobile app built with **Expo**, **InstantDB**, and **Clerk**.
@@ -17,6 +17,7 @@ function tsconfigTemplate() {
17
17
  ".expo/types/**/*.ts",
18
18
  "expo-env.d.ts",
19
19
  ],
20
+ exclude: ["alchemy.run.ts", "worker"],
20
21
  };
21
22
  return JSON.stringify(config, null, 2) + "\n";
22
23
  }
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.magicAuthLayoutTemplate = magicAuthLayoutTemplate;
4
+ function magicAuthLayoutTemplate() {
5
+ return `import { Redirect, Stack } from "expo-router";
6
+ import { ActivityIndicator, StyleSheet, Text, View } from "react-native";
7
+ import { instantDb } from "@/lib/instant";
8
+
9
+ function SetupRequired() {
10
+ return (
11
+ <View style={styles.setupContainer}>
12
+ <View style={styles.setupCard}>
13
+ <Text style={styles.setupTitle}>Setup Required</Text>
14
+ <Text style={styles.setupText}>
15
+ Add EXPO_PUBLIC_INSTANT_APP_ID to your .env.local file.
16
+ </Text>
17
+ <Text style={styles.setupStep}>
18
+ You can create an app id with:\n
19
+ npx instant-cli init-without-files --title my-app
20
+ </Text>
21
+ </View>
22
+ </View>
23
+ );
24
+ }
25
+
26
+ function AuthenticatedAuthLayout({
27
+ db,
28
+ }: {
29
+ db: NonNullable<typeof instantDb>;
30
+ }) {
31
+ const { isLoading, user, error } = db.useAuth();
32
+
33
+ if (isLoading) {
34
+ return (
35
+ <View style={styles.loadingContainer}>
36
+ <ActivityIndicator size="large" color="#4B5563" />
37
+ </View>
38
+ );
39
+ }
40
+
41
+ if (error) {
42
+ return (
43
+ <View style={styles.setupContainer}>
44
+ <View style={styles.setupCard}>
45
+ <Text style={styles.setupTitle}>Authentication Error</Text>
46
+ <Text style={styles.setupText}>{error.message}</Text>
47
+ </View>
48
+ </View>
49
+ );
50
+ }
51
+
52
+ if (user) {
53
+ return <Redirect href="/" />;
54
+ }
55
+
56
+ return (
57
+ <Stack screenOptions={{ headerShown: false }}>
58
+ <Stack.Screen name="sign-in" />
59
+ </Stack>
60
+ );
61
+ }
62
+
63
+ export default function AuthLayout() {
64
+ if (!instantDb) {
65
+ return <SetupRequired />;
66
+ }
67
+
68
+ return <AuthenticatedAuthLayout db={instantDb} />;
69
+ }
70
+
71
+ const styles = StyleSheet.create({
72
+ setupContainer: {
73
+ flex: 1,
74
+ backgroundColor: "#f5f5f5",
75
+ justifyContent: "center",
76
+ alignItems: "center",
77
+ padding: 20,
78
+ },
79
+ setupCard: {
80
+ backgroundColor: "#fff",
81
+ borderRadius: 12,
82
+ padding: 24,
83
+ maxWidth: 420,
84
+ width: "100%",
85
+ shadowColor: "#000",
86
+ shadowOffset: { width: 0, height: 2 },
87
+ shadowOpacity: 0.1,
88
+ shadowRadius: 8,
89
+ elevation: 3,
90
+ },
91
+ setupTitle: {
92
+ fontSize: 24,
93
+ fontWeight: "700",
94
+ color: "#333",
95
+ marginBottom: 16,
96
+ textAlign: "center",
97
+ },
98
+ setupText: {
99
+ fontSize: 16,
100
+ color: "#666",
101
+ marginBottom: 12,
102
+ lineHeight: 22,
103
+ textAlign: "center",
104
+ },
105
+ setupStep: {
106
+ fontSize: 14,
107
+ color: "#888",
108
+ marginTop: 16,
109
+ padding: 16,
110
+ backgroundColor: "#f8f9fa",
111
+ borderRadius: 8,
112
+ lineHeight: 20,
113
+ },
114
+ loadingContainer: {
115
+ flex: 1,
116
+ alignItems: "center",
117
+ justifyContent: "center",
118
+ backgroundColor: "#FFFFFF",
119
+ },
120
+ });
121
+ `;
122
+ }
@@ -0,0 +1,24 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.magicRootLayoutTemplate = magicRootLayoutTemplate;
4
+ function magicRootLayoutTemplate() {
5
+ return `import { Stack } from "expo-router";
6
+ import { StatusBar } from "expo-status-bar";
7
+ import { useFrameworkReady } from "@/hooks/useFrameworkReady";
8
+
9
+ export default function RootLayout() {
10
+ useFrameworkReady();
11
+
12
+ return (
13
+ <>
14
+ <Stack screenOptions={{ headerShown: false }}>
15
+ <Stack.Screen name="(tabs)" />
16
+ <Stack.Screen name="(auth)" />
17
+ <Stack.Screen name="+not-found" />
18
+ </Stack>
19
+ <StatusBar style="auto" />
20
+ </>
21
+ );
22
+ }
23
+ `;
24
+ }