@ramonclaudio/create-vexpo 0.1.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.
Files changed (174) hide show
  1. package/README.md +50 -0
  2. package/dist/index.js +183 -0
  3. package/dist/templates/default/.eas/workflows/asc-events.yml +84 -0
  4. package/dist/templates/default/.eas/workflows/deploy-production.yml +129 -0
  5. package/dist/templates/default/.eas/workflows/development-builds.yml +19 -0
  6. package/dist/templates/default/.eas/workflows/e2e-tests.yml +42 -0
  7. package/dist/templates/default/.eas/workflows/pr-preview.yml +98 -0
  8. package/dist/templates/default/.eas/workflows/release.yml +44 -0
  9. package/dist/templates/default/.eas/workflows/rollback.yml +86 -0
  10. package/dist/templates/default/.eas/workflows/rollout.yml +84 -0
  11. package/dist/templates/default/.eas/workflows/rotate-apple-jwt.yml +42 -0
  12. package/dist/templates/default/.eas/workflows/testflight.yml +57 -0
  13. package/dist/templates/default/.github/workflows/check.yml +28 -0
  14. package/dist/templates/default/.maestro/launch.yaml +18 -0
  15. package/dist/templates/default/AGENTS.md +79 -0
  16. package/dist/templates/default/DESIGN.md +331 -0
  17. package/dist/templates/default/LICENSE +21 -0
  18. package/dist/templates/default/README.md +153 -0
  19. package/dist/templates/default/SETUP.md +618 -0
  20. package/dist/templates/default/__tests__/convex/constants.test.ts +49 -0
  21. package/dist/templates/default/__tests__/convex/validators.test.ts +23 -0
  22. package/dist/templates/default/__tests__/convex/webhook.test.ts +343 -0
  23. package/dist/templates/default/__tests__/lib/deep-link.test.ts +67 -0
  24. package/dist/templates/default/_easignore +22 -0
  25. package/dist/templates/default/_editorconfig +9 -0
  26. package/dist/templates/default/_env.example +34 -0
  27. package/dist/templates/default/_fingerprintignore +24 -0
  28. package/dist/templates/default/_gitattributes +7 -0
  29. package/dist/templates/default/_gitignore +69 -0
  30. package/dist/templates/default/_oxfmtrc.json +3 -0
  31. package/dist/templates/default/_oxlintrc.json +34 -0
  32. package/dist/templates/default/app/(app)/(tabs)/(home)/index.tsx +50 -0
  33. package/dist/templates/default/app/(app)/(tabs)/(home,search)/_layout.tsx +44 -0
  34. package/dist/templates/default/app/(app)/(tabs)/(search)/index.tsx +247 -0
  35. package/dist/templates/default/app/(app)/(tabs)/_layout.tsx +77 -0
  36. package/dist/templates/default/app/(app)/(tabs)/settings/_layout.tsx +37 -0
  37. package/dist/templates/default/app/(app)/(tabs)/settings/index.tsx +362 -0
  38. package/dist/templates/default/app/(app)/(tabs)/settings/preferences.tsx +184 -0
  39. package/dist/templates/default/app/(app)/_layout.tsx +73 -0
  40. package/dist/templates/default/app/(app)/debug.tsx +389 -0
  41. package/dist/templates/default/app/(app)/help.tsx +254 -0
  42. package/dist/templates/default/app/(app)/linked.tsx +116 -0
  43. package/dist/templates/default/app/(app)/privacy.tsx +159 -0
  44. package/dist/templates/default/app/(app)/profile.tsx +915 -0
  45. package/dist/templates/default/app/(app)/sessions.tsx +191 -0
  46. package/dist/templates/default/app/(app)/welcome.tsx +140 -0
  47. package/dist/templates/default/app/(auth)/_layout.tsx +31 -0
  48. package/dist/templates/default/app/(auth)/forgot-password.tsx +168 -0
  49. package/dist/templates/default/app/(auth)/reset-password.tsx +314 -0
  50. package/dist/templates/default/app/(auth)/sign-in.tsx +453 -0
  51. package/dist/templates/default/app/(auth)/sign-up.tsx +563 -0
  52. package/dist/templates/default/app/+native-intent.tsx +14 -0
  53. package/dist/templates/default/app/+not-found.tsx +51 -0
  54. package/dist/templates/default/app/_layout.tsx +102 -0
  55. package/dist/templates/default/app-store/screenshots/.gitkeep +0 -0
  56. package/dist/templates/default/app-store/screenshots/README.md +13 -0
  57. package/dist/templates/default/app.config.ts +201 -0
  58. package/dist/templates/default/app.json +11 -0
  59. package/dist/templates/default/assets/brand-icon-dark.png +0 -0
  60. package/dist/templates/default/assets/brand-icon-light.png +0 -0
  61. package/dist/templates/default/assets/fonts/Geist-Black.ttf +0 -0
  62. package/dist/templates/default/assets/fonts/Geist-BlackItalic.ttf +0 -0
  63. package/dist/templates/default/assets/fonts/Geist-Bold.ttf +0 -0
  64. package/dist/templates/default/assets/fonts/Geist-BoldItalic.ttf +0 -0
  65. package/dist/templates/default/assets/fonts/Geist-ExtraBold.ttf +0 -0
  66. package/dist/templates/default/assets/fonts/Geist-ExtraBoldItalic.ttf +0 -0
  67. package/dist/templates/default/assets/fonts/Geist-ExtraLight.ttf +0 -0
  68. package/dist/templates/default/assets/fonts/Geist-ExtraLightItalic.ttf +0 -0
  69. package/dist/templates/default/assets/fonts/Geist-Italic.ttf +0 -0
  70. package/dist/templates/default/assets/fonts/Geist-Light.ttf +0 -0
  71. package/dist/templates/default/assets/fonts/Geist-LightItalic.ttf +0 -0
  72. package/dist/templates/default/assets/fonts/Geist-Medium.ttf +0 -0
  73. package/dist/templates/default/assets/fonts/Geist-MediumItalic.ttf +0 -0
  74. package/dist/templates/default/assets/fonts/Geist-Regular.ttf +0 -0
  75. package/dist/templates/default/assets/fonts/Geist-SemiBold.ttf +0 -0
  76. package/dist/templates/default/assets/fonts/Geist-SemiBoldItalic.ttf +0 -0
  77. package/dist/templates/default/assets/fonts/Geist-Thin.ttf +0 -0
  78. package/dist/templates/default/assets/fonts/Geist-ThinItalic.ttf +0 -0
  79. package/dist/templates/default/assets/fonts/Geist-Variable-Italic.ttf +0 -0
  80. package/dist/templates/default/assets/fonts/Geist-Variable.ttf +0 -0
  81. package/dist/templates/default/assets/fonts/GeistMono-Bold.ttf +0 -0
  82. package/dist/templates/default/assets/fonts/GeistMono-BoldItalic.ttf +0 -0
  83. package/dist/templates/default/assets/fonts/GeistMono-Italic.ttf +0 -0
  84. package/dist/templates/default/assets/fonts/GeistMono-Medium.ttf +0 -0
  85. package/dist/templates/default/assets/fonts/GeistMono-MediumItalic.ttf +0 -0
  86. package/dist/templates/default/assets/fonts/GeistMono-Regular.ttf +0 -0
  87. package/dist/templates/default/assets/fonts/GeistPixel-Square.ttf +0 -0
  88. package/dist/templates/default/assets/icon.png +0 -0
  89. package/dist/templates/default/assets/sounds/notification.wav +0 -0
  90. package/dist/templates/default/assets/splash-image-dark.png +0 -0
  91. package/dist/templates/default/assets/splash-image-light.png +0 -0
  92. package/dist/templates/default/bun.lock +1860 -0
  93. package/dist/templates/default/components/auth/otp-verification.tsx +255 -0
  94. package/dist/templates/default/components/auth/password-field.tsx +121 -0
  95. package/dist/templates/default/components/auth/segmented-toggle.tsx +47 -0
  96. package/dist/templates/default/components/ui/convex-error.tsx +32 -0
  97. package/dist/templates/default/components/ui/error-boundary.tsx +57 -0
  98. package/dist/templates/default/components/ui/loading-screen.tsx +31 -0
  99. package/dist/templates/default/components/ui/material.tsx +94 -0
  100. package/dist/templates/default/components/ui/offline-banner.tsx +58 -0
  101. package/dist/templates/default/components/ui/prominent-button.tsx +71 -0
  102. package/dist/templates/default/components/ui/skeleton.tsx +107 -0
  103. package/dist/templates/default/components/ui/status-text.tsx +49 -0
  104. package/dist/templates/default/components/ui/update-banner.tsx +82 -0
  105. package/dist/templates/default/constants/layout.ts +102 -0
  106. package/dist/templates/default/constants/theme.ts +401 -0
  107. package/dist/templates/default/constants/ui.ts +77 -0
  108. package/dist/templates/default/convex/_generated/api.d.ts +77 -0
  109. package/dist/templates/default/convex/_generated/api.js +23 -0
  110. package/dist/templates/default/convex/_generated/dataModel.d.ts +60 -0
  111. package/dist/templates/default/convex/_generated/server.d.ts +143 -0
  112. package/dist/templates/default/convex/_generated/server.js +93 -0
  113. package/dist/templates/default/convex/admin.ts +102 -0
  114. package/dist/templates/default/convex/auth.config.ts +6 -0
  115. package/dist/templates/default/convex/auth.ts +335 -0
  116. package/dist/templates/default/convex/constants.ts +46 -0
  117. package/dist/templates/default/convex/convex.config.ts +11 -0
  118. package/dist/templates/default/convex/crons.ts +42 -0
  119. package/dist/templates/default/convex/email.ts +109 -0
  120. package/dist/templates/default/convex/env.ts +31 -0
  121. package/dist/templates/default/convex/errors.ts +33 -0
  122. package/dist/templates/default/convex/functions.ts +54 -0
  123. package/dist/templates/default/convex/http.ts +176 -0
  124. package/dist/templates/default/convex/log.ts +81 -0
  125. package/dist/templates/default/convex/pushTokens.ts +114 -0
  126. package/dist/templates/default/convex/rateLimit.ts +92 -0
  127. package/dist/templates/default/convex/schema.ts +28 -0
  128. package/dist/templates/default/convex/tsconfig.json +18 -0
  129. package/dist/templates/default/convex/users.ts +279 -0
  130. package/dist/templates/default/convex/validators.ts +74 -0
  131. package/dist/templates/default/convex/webhook.ts +193 -0
  132. package/dist/templates/default/convex.json +6 -0
  133. package/dist/templates/default/eas.json +56 -0
  134. package/dist/templates/default/fingerprint.config.js +9 -0
  135. package/dist/templates/default/hooks/use-debounce.ts +20 -0
  136. package/dist/templates/default/hooks/use-deep-link.ts +43 -0
  137. package/dist/templates/default/hooks/use-navigation-tracking.ts +15 -0
  138. package/dist/templates/default/hooks/use-network.ts +11 -0
  139. package/dist/templates/default/hooks/use-notifications.ts +107 -0
  140. package/dist/templates/default/hooks/use-onboarding.ts +15 -0
  141. package/dist/templates/default/hooks/use-reduced-motion.ts +11 -0
  142. package/dist/templates/default/hooks/use-theme.ts +53 -0
  143. package/dist/templates/default/hooks/use-updates.ts +86 -0
  144. package/dist/templates/default/lib/a11y.ts +5 -0
  145. package/dist/templates/default/lib/app.ts +14 -0
  146. package/dist/templates/default/lib/assets.ts +17 -0
  147. package/dist/templates/default/lib/auth-client.ts +21 -0
  148. package/dist/templates/default/lib/convex-auth.tsx +79 -0
  149. package/dist/templates/default/lib/deep-link.ts +71 -0
  150. package/dist/templates/default/lib/dev-menu.ts +119 -0
  151. package/dist/templates/default/lib/device.ts +40 -0
  152. package/dist/templates/default/lib/dynamic-font.ts +49 -0
  153. package/dist/templates/default/lib/env.ts +10 -0
  154. package/dist/templates/default/lib/haptics.ts +24 -0
  155. package/dist/templates/default/lib/notifications.ts +276 -0
  156. package/dist/templates/default/lib/preferences.ts +45 -0
  157. package/dist/templates/default/lib/schemas.ts +137 -0
  158. package/dist/templates/default/lib/storage.ts +47 -0
  159. package/dist/templates/default/lib/updates.ts +107 -0
  160. package/dist/templates/default/metro.config.js +14 -0
  161. package/dist/templates/default/package.json +129 -0
  162. package/dist/templates/default/patches/PR-368.patch +91 -0
  163. package/dist/templates/default/patches/convex-dev-better-auth-0.12.2.tgz +0 -0
  164. package/dist/templates/default/plugins/README.md +9 -0
  165. package/dist/templates/default/plugins/with-auto-signing.js +45 -0
  166. package/dist/templates/default/plugins/with-pod-deployment-target.js +35 -0
  167. package/dist/templates/default/scripts/README.md +36 -0
  168. package/dist/templates/default/scripts/_run.mjs +77 -0
  169. package/dist/templates/default/scripts/clean.ts +543 -0
  170. package/dist/templates/default/scripts/rotate-apple-jwt.mjs +80 -0
  171. package/dist/templates/default/store.config.json +58 -0
  172. package/dist/templates/default/tsconfig.json +13 -0
  173. package/dist/templates/default/vitest.config.ts +21 -0
  174. package/package.json +69 -0
@@ -0,0 +1,143 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated utilities for implementing server-side Convex query and mutation functions.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import {
12
+ ActionBuilder,
13
+ HttpActionBuilder,
14
+ MutationBuilder,
15
+ QueryBuilder,
16
+ GenericActionCtx,
17
+ GenericMutationCtx,
18
+ GenericQueryCtx,
19
+ GenericDatabaseReader,
20
+ GenericDatabaseWriter,
21
+ } from "convex/server";
22
+ import type { DataModel } from "./dataModel.js";
23
+
24
+ /**
25
+ * Define a query in this Convex app's public API.
26
+ *
27
+ * This function will be allowed to read your Convex database and will be accessible from the client.
28
+ *
29
+ * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
30
+ * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
31
+ */
32
+ export declare const query: QueryBuilder<DataModel, "public">;
33
+
34
+ /**
35
+ * Define a query that is only accessible from other Convex functions (but not from the client).
36
+ *
37
+ * This function will be allowed to read from your Convex database. It will not be accessible from the client.
38
+ *
39
+ * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
40
+ * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
41
+ */
42
+ export declare const internalQuery: QueryBuilder<DataModel, "internal">;
43
+
44
+ /**
45
+ * Define a mutation in this Convex app's public API.
46
+ *
47
+ * This function will be allowed to modify your Convex database and will be accessible from the client.
48
+ *
49
+ * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
50
+ * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
51
+ */
52
+ export declare const mutation: MutationBuilder<DataModel, "public">;
53
+
54
+ /**
55
+ * Define a mutation that is only accessible from other Convex functions (but not from the client).
56
+ *
57
+ * This function will be allowed to modify your Convex database. It will not be accessible from the client.
58
+ *
59
+ * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
60
+ * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
61
+ */
62
+ export declare const internalMutation: MutationBuilder<DataModel, "internal">;
63
+
64
+ /**
65
+ * Define an action in this Convex app's public API.
66
+ *
67
+ * An action is a function which can execute any JavaScript code, including non-deterministic
68
+ * code and code with side-effects, like calling third-party services.
69
+ * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
70
+ * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
71
+ *
72
+ * @param func - The action. It receives an {@link ActionCtx} as its first argument.
73
+ * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
74
+ */
75
+ export declare const action: ActionBuilder<DataModel, "public">;
76
+
77
+ /**
78
+ * Define an action that is only accessible from other Convex functions (but not from the client).
79
+ *
80
+ * @param func - The function. It receives an {@link ActionCtx} as its first argument.
81
+ * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
82
+ */
83
+ export declare const internalAction: ActionBuilder<DataModel, "internal">;
84
+
85
+ /**
86
+ * Define an HTTP action.
87
+ *
88
+ * The wrapped function will be used to respond to HTTP requests received
89
+ * by a Convex deployment if the requests matches the path and method where
90
+ * this action is routed. Be sure to route your httpAction in `convex/http.js`.
91
+ *
92
+ * @param func - The function. It receives an {@link ActionCtx} as its first argument
93
+ * and a Fetch API `Request` object as its second.
94
+ * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
95
+ */
96
+ export declare const httpAction: HttpActionBuilder;
97
+
98
+ /**
99
+ * A set of services for use within Convex query functions.
100
+ *
101
+ * The query context is passed as the first argument to any Convex query
102
+ * function run on the server.
103
+ *
104
+ * This differs from the {@link MutationCtx} because all of the services are
105
+ * read-only.
106
+ */
107
+ export type QueryCtx = GenericQueryCtx<DataModel>;
108
+
109
+ /**
110
+ * A set of services for use within Convex mutation functions.
111
+ *
112
+ * The mutation context is passed as the first argument to any Convex mutation
113
+ * function run on the server.
114
+ */
115
+ export type MutationCtx = GenericMutationCtx<DataModel>;
116
+
117
+ /**
118
+ * A set of services for use within Convex action functions.
119
+ *
120
+ * The action context is passed as the first argument to any Convex action
121
+ * function run on the server.
122
+ */
123
+ export type ActionCtx = GenericActionCtx<DataModel>;
124
+
125
+ /**
126
+ * An interface to read from the database within Convex query functions.
127
+ *
128
+ * The two entry points are {@link DatabaseReader.get}, which fetches a single
129
+ * document by its {@link Id}, or {@link DatabaseReader.query}, which starts
130
+ * building a query.
131
+ */
132
+ export type DatabaseReader = GenericDatabaseReader<DataModel>;
133
+
134
+ /**
135
+ * An interface to read from and write to the database within Convex mutation
136
+ * functions.
137
+ *
138
+ * Convex guarantees that all writes within a single mutation are
139
+ * executed atomically, so you never have to worry about partial writes leaving
140
+ * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control)
141
+ * for the guarantees Convex provides your functions.
142
+ */
143
+ export type DatabaseWriter = GenericDatabaseWriter<DataModel>;
@@ -0,0 +1,93 @@
1
+ /* eslint-disable */
2
+ /**
3
+ * Generated utilities for implementing server-side Convex query and mutation functions.
4
+ *
5
+ * THIS CODE IS AUTOMATICALLY GENERATED.
6
+ *
7
+ * To regenerate, run `npx convex dev`.
8
+ * @module
9
+ */
10
+
11
+ import {
12
+ actionGeneric,
13
+ httpActionGeneric,
14
+ queryGeneric,
15
+ mutationGeneric,
16
+ internalActionGeneric,
17
+ internalMutationGeneric,
18
+ internalQueryGeneric,
19
+ } from "convex/server";
20
+
21
+ /**
22
+ * Define a query in this Convex app's public API.
23
+ *
24
+ * This function will be allowed to read your Convex database and will be accessible from the client.
25
+ *
26
+ * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
27
+ * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
28
+ */
29
+ export const query = queryGeneric;
30
+
31
+ /**
32
+ * Define a query that is only accessible from other Convex functions (but not from the client).
33
+ *
34
+ * This function will be allowed to read from your Convex database. It will not be accessible from the client.
35
+ *
36
+ * @param func - The query function. It receives a {@link QueryCtx} as its first argument.
37
+ * @returns The wrapped query. Include this as an `export` to name it and make it accessible.
38
+ */
39
+ export const internalQuery = internalQueryGeneric;
40
+
41
+ /**
42
+ * Define a mutation in this Convex app's public API.
43
+ *
44
+ * This function will be allowed to modify your Convex database and will be accessible from the client.
45
+ *
46
+ * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
47
+ * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
48
+ */
49
+ export const mutation = mutationGeneric;
50
+
51
+ /**
52
+ * Define a mutation that is only accessible from other Convex functions (but not from the client).
53
+ *
54
+ * This function will be allowed to modify your Convex database. It will not be accessible from the client.
55
+ *
56
+ * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument.
57
+ * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible.
58
+ */
59
+ export const internalMutation = internalMutationGeneric;
60
+
61
+ /**
62
+ * Define an action in this Convex app's public API.
63
+ *
64
+ * An action is a function which can execute any JavaScript code, including non-deterministic
65
+ * code and code with side-effects, like calling third-party services.
66
+ * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive.
67
+ * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}.
68
+ *
69
+ * @param func - The action. It receives an {@link ActionCtx} as its first argument.
70
+ * @returns The wrapped action. Include this as an `export` to name it and make it accessible.
71
+ */
72
+ export const action = actionGeneric;
73
+
74
+ /**
75
+ * Define an action that is only accessible from other Convex functions (but not from the client).
76
+ *
77
+ * @param func - The function. It receives an {@link ActionCtx} as its first argument.
78
+ * @returns The wrapped function. Include this as an `export` to name it and make it accessible.
79
+ */
80
+ export const internalAction = internalActionGeneric;
81
+
82
+ /**
83
+ * Define an HTTP action.
84
+ *
85
+ * The wrapped function will be used to respond to HTTP requests received
86
+ * by a Convex deployment if the requests matches the path and method where
87
+ * this action is routed. Be sure to route your httpAction in `convex/http.js`.
88
+ *
89
+ * @param func - The function. It receives an {@link ActionCtx} as its first argument
90
+ * and a Fetch API `Request` object as its second.
91
+ * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up.
92
+ */
93
+ export const httpAction = httpActionGeneric;
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Admin actions for fixture / review accounts. Internal-only, never exposed
3
+ * to client code. Run via `bunx convex run admin:<fn>` (or your PM's dlx).
4
+ */
5
+
6
+ import { v } from "convex/values";
7
+
8
+ import { components } from "./_generated/api";
9
+ import { internalAction, internalMutation } from "./_generated/server";
10
+ import { createAuth } from "./auth";
11
+ import { rateLimiter, type RateLimitName } from "./rateLimit";
12
+
13
+ /**
14
+ * Create a fully verified review account. Used by `setup:review-account`
15
+ * to seed Apple's App Review with a working sign-in.
16
+ *
17
+ * Idempotent: if the user already exists, just re-asserts emailVerified=true.
18
+ * Does NOT rotate the password on re-run, delete the user from the dashboard
19
+ * first if you need a fresh password.
20
+ *
21
+ * Side effect: triggers a verification OTP email on first run via the normal
22
+ * sign-up flow. The OTP is unused (we flip emailVerified directly via the
23
+ * adapter) and lands in the configured inbox.
24
+ */
25
+ export const createReviewAccount = internalAction({
26
+ args: {
27
+ email: v.string(),
28
+ password: v.string(),
29
+ name: v.string(),
30
+ username: v.optional(v.string()),
31
+ },
32
+ returns: v.object({
33
+ userId: v.string(),
34
+ email: v.string(),
35
+ created: v.boolean(),
36
+ verified: v.boolean(),
37
+ name: v.string(),
38
+ }),
39
+ handler: async (ctx, { email, password, name, username }) => {
40
+ const auth = createAuth(ctx);
41
+
42
+ type User = { _id?: string; id?: string; email: string; emailVerified: boolean };
43
+
44
+ const lookup = async (): Promise<User | null> =>
45
+ (await ctx.runQuery(components.betterAuth.adapter.findOne, {
46
+ model: "user",
47
+ where: [{ field: "email", value: email }],
48
+ } as never)) as User | null;
49
+
50
+ let user = await lookup();
51
+ let created = false;
52
+
53
+ if (!user) {
54
+ const body: Record<string, string> = { email, password, name };
55
+ if (username) body.username = username;
56
+ await auth.api.signUpEmail({
57
+ body: body as { email: string; password: string; name: string },
58
+ asResponse: false,
59
+ });
60
+ user = await lookup();
61
+ if (!user) throw new Error("user not found after signUpEmail");
62
+ created = true;
63
+ }
64
+
65
+ const docId = user._id ?? user.id;
66
+ if (!docId) throw new Error("user document is missing both _id and id");
67
+
68
+ await ctx.runMutation(components.betterAuth.adapter.updateOne, {
69
+ input: {
70
+ model: "user",
71
+ where: [{ field: "_id", value: docId }],
72
+ update: { emailVerified: true },
73
+ },
74
+ } as never);
75
+
76
+ return {
77
+ userId: docId,
78
+ email,
79
+ created,
80
+ verified: true,
81
+ name,
82
+ };
83
+ },
84
+ });
85
+
86
+ /**
87
+ * Reset a rate-limit bucket. Run from the dashboard:
88
+ * `bunx convex run admin:resetRateLimit '{"name":"avatarUpload","key":"<userId>"}'`
89
+ * Omit `key` to reset the shared bucket.
90
+ */
91
+ export const resetRateLimit = internalMutation({
92
+ args: { name: v.string(), key: v.optional(v.string()) },
93
+ returns: v.object({
94
+ reset: v.boolean(),
95
+ name: v.string(),
96
+ key: v.union(v.string(), v.null()),
97
+ }),
98
+ handler: async (ctx, { name, key }) => {
99
+ await rateLimiter.reset(ctx, name as RateLimitName, key ? { key } : undefined);
100
+ return { reset: true, name, key: key ?? null };
101
+ },
102
+ });
@@ -0,0 +1,6 @@
1
+ import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config";
2
+ import type { AuthConfig } from "convex/server";
3
+
4
+ export default {
5
+ providers: [getAuthConfigProvider()],
6
+ } satisfies AuthConfig;
@@ -0,0 +1,335 @@
1
+ import { expo } from "@better-auth/expo";
2
+ import { createClient } from "@convex-dev/better-auth";
3
+ import type { AuthFunctions, GenericCtx } from "@convex-dev/better-auth";
4
+ import { convex } from "@convex-dev/better-auth/plugins";
5
+ import type { BetterAuthOptions } from "better-auth";
6
+ import { betterAuth } from "better-auth/minimal";
7
+ import { emailOTP, username } from "better-auth/plugins";
8
+ import { v } from "convex/values";
9
+
10
+ import { components, internal } from "./_generated/api";
11
+ import type { DataModel, Doc } from "./_generated/dataModel";
12
+ import { internalAction, query } from "./_generated/server";
13
+ import type { MutationCtx, QueryCtx } from "./_generated/server";
14
+ import authConfig from "./auth.config";
15
+ import {
16
+ USERNAME_FORMAT_REGEX,
17
+ USERNAME_MAX_LENGTH,
18
+ USERNAME_MIN_LENGTH,
19
+ isReservedUsername,
20
+ } from "./constants";
21
+ import { sendAuthOTP } from "./email";
22
+ import { env } from "./env";
23
+ import { authenticationRequired } from "./errors";
24
+
25
+ const ONE_MINUTE = 60;
26
+ const ONE_HOUR = 60 * ONE_MINUTE;
27
+ const ONE_DAY = 24 * ONE_HOUR;
28
+ const SEVEN_DAYS = 7 * ONE_DAY;
29
+ const TEN_MINUTES = 10 * ONE_MINUTE;
30
+ const FIVE_MINUTES = 5 * ONE_MINUTE;
31
+
32
+ const authFunctions: AuthFunctions = internal.auth;
33
+
34
+ /**
35
+ * Get the app user doc by Better Auth id, using the indexed lookup.
36
+ */
37
+ export async function getUserByAuthId(
38
+ ctx: QueryCtx | MutationCtx,
39
+ authId: string,
40
+ ): Promise<Doc<"users"> | null> {
41
+ return await ctx.db
42
+ .query("users")
43
+ .withIndex("authId", (q) => q.eq("authId", authId))
44
+ .unique();
45
+ }
46
+
47
+ /**
48
+ * Merged representation of the authenticated user.
49
+ *
50
+ * Identity fields (email, name, username, image, emailVerified) come from the
51
+ * Better Auth user. App-specific fields (_id, bio, avatar, timestamps) come
52
+ * from our users table. `avatarUrl` is resolved: user-uploaded storage id
53
+ * takes precedence, otherwise falls back to Better Auth's `image` (e.g. OAuth
54
+ * provider avatar).
55
+ */
56
+ export type AuthUser = Doc<"users"> & {
57
+ authUserId: string;
58
+ email: string;
59
+ name: string;
60
+ emailVerified: boolean;
61
+ image: string | null;
62
+ username: string | null;
63
+ displayUsername: string | null;
64
+ avatarUrl: string | null;
65
+ hasUploadedAvatar: boolean;
66
+ };
67
+
68
+ // The component client has methods needed for integrating Convex with Better
69
+ // Auth, plus helper methods for general use.
70
+ export const authComponent = createClient<DataModel>(components.betterAuth, {
71
+ authFunctions,
72
+ triggers: {
73
+ user: {
74
+ onCreate: async (ctx, authUser) => {
75
+ // Create the app user row with defaults. Identity fields live on the
76
+ // Better Auth user record, not here.
77
+ await ctx.db.insert("users", {
78
+ authId: authUser._id,
79
+ createdAt: Date.now(),
80
+ updatedAt: Date.now(),
81
+ });
82
+ },
83
+ onDelete: async (ctx, authUser) => {
84
+ const user = await getUserByAuthId(ctx, authUser._id);
85
+ if (!user) return;
86
+ // Free the avatar blob before dropping the row so we don't leak storage.
87
+ if (user.avatar) await ctx.storage.delete(user.avatar);
88
+ await ctx.db.delete(user._id);
89
+ },
90
+ },
91
+ },
92
+ });
93
+
94
+ // Export trigger handlers - these become available at internal.auth
95
+ export const { onCreate, onDelete } = authComponent.triggersApi();
96
+
97
+ // Export client API for AuthBoundary and other client-side auth checks
98
+ export const { getAuthUser } = authComponent.clientApi();
99
+
100
+ export const createAuth = (ctx: GenericCtx<DataModel>) =>
101
+ betterAuth({
102
+ baseURL: env.convexSiteUrl,
103
+ trustedOrigins: [
104
+ "vexpo://",
105
+ env.siteUrl,
106
+ // In dev, Expo Go uses `exp://<lan-ip>:<port>` and the dev client uses
107
+ // `exp+<scheme>://`. Wildcards match the host/port suffix that Better
108
+ // Auth sees in the request origin. Production builds drop these.
109
+ ...(process.env.NODE_ENV === "development"
110
+ ? ["exp://*", "exp://**", "http://localhost:8081"]
111
+ : []),
112
+ ],
113
+ database: authComponent.adapter(ctx),
114
+ emailAndPassword: {
115
+ enabled: true,
116
+ // Email verification is gated on the `REQUIRE_EMAIL_VERIFICATION`
117
+ // Convex env var. The lite-mode setup (`bunx vexpo lite`) leaves it
118
+ // unset (default `false`) so sign-up creates verified accounts
119
+ // immediately and the user can sign in without an OTP. No Resend
120
+ // configuration needed to get up and running on the iOS Simulator.
121
+ // `bunx vexpo full` flips this to `true` when it provisions Resend.
122
+ // Production runs with verification on.
123
+ requireEmailVerification: env.requireEmailVerification,
124
+ minPasswordLength: 10,
125
+ maxPasswordLength: 128,
126
+ // When verification is off, accounts land verified-on-create so
127
+ // password sign-in works without the email round-trip.
128
+ autoSignIn: !env.requireEmailVerification,
129
+ },
130
+ emailVerification: {
131
+ // When `emailOtp.verifyEmail` succeeds, Better Auth creates a session and
132
+ // sets the cookie inline instead of returning { token: null } and forcing
133
+ // the user to sign in manually.
134
+ autoSignInAfterVerification: true,
135
+ },
136
+ // Only register the Apple provider when its credentials are present.
137
+ // Better Auth logs a warning on every request otherwise, and the client
138
+ // hides the button via `getEnabledProviders` when the env vars are unset,
139
+ // so registering an empty provider serves no purpose.
140
+ socialProviders:
141
+ process.env.APPLE_CLIENT_ID && process.env.APPLE_CLIENT_SECRET
142
+ ? {
143
+ apple: {
144
+ clientId: process.env.APPLE_CLIENT_ID,
145
+ clientSecret: process.env.APPLE_CLIENT_SECRET,
146
+ },
147
+ }
148
+ : {},
149
+ session: {
150
+ expiresIn: SEVEN_DAYS,
151
+ updateAge: ONE_DAY,
152
+ freshAge: TEN_MINUTES,
153
+ cookieCache: { enabled: true, maxAge: FIVE_MINUTES },
154
+ },
155
+ // Better Auth handles HTTP-level rate limiting for all auth endpoints.
156
+ // Custom rules use EXACT match unless the key contains "*" (wildcard).
157
+ // Paths here are the post-basePath form (Better Auth strips /api/auth).
158
+ // This app uses the email-OTP flow exclusively, so password reset and
159
+ // verification hit /email-otp/* rather than the link-based endpoints.
160
+ rateLimit: {
161
+ enabled: true,
162
+ window: ONE_MINUTE,
163
+ max: 100,
164
+ customRules: {
165
+ "/sign-in/*": { window: ONE_MINUTE, max: 5 },
166
+ "/sign-up/*": { window: ONE_MINUTE, max: 3 },
167
+ "/email-otp/request-password-reset": { window: ONE_HOUR, max: 3 },
168
+ "/email-otp/reset-password": { window: ONE_MINUTE, max: 3 },
169
+ "/email-otp/send-verification-otp": { window: ONE_MINUTE, max: 3 },
170
+ "/list-sessions": { window: ONE_MINUTE, max: 30 },
171
+ "/get-session": { window: ONE_MINUTE, max: 60 },
172
+ },
173
+ },
174
+ advanced: {
175
+ ipAddress: {
176
+ ipAddressHeaders: ["x-forwarded-for", "x-real-ip"],
177
+ },
178
+ },
179
+ plugins: [
180
+ convex({ authConfig }),
181
+ // Email OTP for sign-in, verification, password reset, and change-email.
182
+ emailOTP({
183
+ otpLength: 6,
184
+ expiresIn: FIVE_MINUTES,
185
+ overrideDefaultEmailVerification: true,
186
+ // Only send a verification OTP on sign-up when verification is
187
+ // actually required. Minimal-tier setup short-circuits the OTP
188
+ // so users can sign up without ever opening their email.
189
+ sendVerificationOnSignUp: env.requireEmailVerification,
190
+ changeEmail: {
191
+ enabled: true,
192
+ verifyCurrentEmail: true,
193
+ },
194
+ sendVerificationOTP: async ({ email, otp, type }) => {
195
+ await sendAuthOTP(ctx, { email, otp, type });
196
+ },
197
+ }),
198
+ username({
199
+ minUsernameLength: USERNAME_MIN_LENGTH,
200
+ maxUsernameLength: USERNAME_MAX_LENGTH,
201
+ validationOrder: { username: "post-normalization" },
202
+ usernameValidator: (normalized) => {
203
+ if (isReservedUsername(normalized)) return false;
204
+ return USERNAME_FORMAT_REGEX.test(normalized);
205
+ },
206
+ }),
207
+ expo(),
208
+ ],
209
+ } satisfies BetterAuthOptions);
210
+
211
+ /**
212
+ * Safely get the current authenticated user. Returns undefined if not
213
+ * authenticated or if the app user row is missing (shouldn't happen in
214
+ * practice, but we handle it gracefully).
215
+ */
216
+ export async function safeGetAuthenticatedUser(
217
+ ctx: QueryCtx | MutationCtx,
218
+ ): Promise<AuthUser | undefined> {
219
+ const authUser = await authComponent.safeGetAuthUser(ctx);
220
+ if (!authUser) return undefined;
221
+
222
+ const user = await getUserByAuthId(ctx, authUser._id);
223
+ if (!user) return undefined;
224
+
225
+ // Resolve avatar: user upload takes precedence over Better Auth image.
226
+ const hasUploadedAvatar = !!user.avatar;
227
+ const avatarUrl = hasUploadedAvatar
228
+ ? await ctx.storage.getUrl(user.avatar!)
229
+ : (authUser.image ?? null);
230
+
231
+ return {
232
+ ...user,
233
+ authUserId: authUser._id,
234
+ email: authUser.email,
235
+ name: authUser.name,
236
+ emailVerified: authUser.emailVerified,
237
+ image: authUser.image ?? null,
238
+ username: (authUser as { username?: string | null }).username ?? null,
239
+ displayUsername: (authUser as { displayUsername?: string | null }).displayUsername ?? null,
240
+ avatarUrl,
241
+ hasUploadedAvatar,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * Get the current authenticated user, throwing if not authenticated.
247
+ */
248
+ export async function requireAuthenticatedUser(ctx: QueryCtx | MutationCtx): Promise<AuthUser> {
249
+ const user = await safeGetAuthenticatedUser(ctx);
250
+ if (!user) throw authenticationRequired();
251
+ return user;
252
+ }
253
+
254
+ /**
255
+ * Validator for AuthUser return type.
256
+ */
257
+ export const authUserValidator = v.object({
258
+ _id: v.id("users"),
259
+ _creationTime: v.number(),
260
+ authId: v.string(),
261
+ bio: v.optional(v.string()),
262
+ avatar: v.optional(v.id("_storage")),
263
+ createdAt: v.number(),
264
+ updatedAt: v.number(),
265
+ authUserId: v.string(),
266
+ email: v.string(),
267
+ name: v.string(),
268
+ emailVerified: v.boolean(),
269
+ image: v.union(v.string(), v.null()),
270
+ username: v.union(v.string(), v.null()),
271
+ displayUsername: v.union(v.string(), v.null()),
272
+ avatarUrl: v.union(v.string(), v.null()),
273
+ hasUploadedAvatar: v.boolean(),
274
+ });
275
+
276
+ // ============================================================================
277
+ // Queries
278
+ // ============================================================================
279
+ // These use the raw `query` builder because this file IS the auth primitive
280
+ // that functions.ts depends on. Importing wrappers from ./functions would
281
+ // create a circular dependency.
282
+
283
+ /**
284
+ * Check if the current user has a password-based account.
285
+ * Useful for detecting social-only accounts that need password setup.
286
+ * Returns false if not authenticated.
287
+ */
288
+ export const hasPassword = query({
289
+ args: {},
290
+ returns: v.boolean(),
291
+ handler: async (ctx) => {
292
+ const user = await safeGetAuthenticatedUser(ctx);
293
+ if (!user) return false;
294
+ const { auth, headers } = await authComponent.getAuth(createAuth, ctx);
295
+ const accounts = await auth.api.listUserAccounts({ headers });
296
+ return accounts.some((account) => account.providerId === "credential");
297
+ },
298
+ });
299
+
300
+ /**
301
+ * Public read of which auth features are configured server-side. Lets the
302
+ * client hide buttons that would fail at submit (e.g. Apple Sign In with empty
303
+ * `APPLE_CLIENT_ID`, OTP sign-in or password reset with `REQUIRE_EMAIL_VERIFICATION`
304
+ * unset). Returns booleans only, never leaks the credentials.
305
+ *
306
+ * `emailFeatures` is true when `REQUIRE_EMAIL_VERIFICATION` is set on the
307
+ * Convex deployment env (testflight tier setup or later). When false, the
308
+ * client hides OTP sign-in, password reset, change-email. the only working
309
+ * flow is email + password sign-up/sign-in. This is the minimal-tier path:
310
+ * users get into the app without configuring Resend or any DNS.
311
+ */
312
+ export const getEnabledProviders = query({
313
+ args: {},
314
+ returns: v.object({ apple: v.boolean(), emailFeatures: v.boolean() }),
315
+ handler: async () => {
316
+ const apple = !!process.env.APPLE_CLIENT_ID && !!process.env.APPLE_CLIENT_SECRET;
317
+ const emailFeatures = env.requireEmailVerification;
318
+ return { apple, emailFeatures };
319
+ },
320
+ });
321
+
322
+ /**
323
+ * Rotate JWKS keys for JWT signing.
324
+ * Run with: bunx convex run auth:rotateKeys
325
+ */
326
+ export const rotateKeys = internalAction({
327
+ args: {},
328
+ // Better Auth's `rotateKeys()` returns implementation-specific JWKS metadata
329
+ // that we don't constrain here. `v.any()` documents the upstream contract.
330
+ returns: v.any(),
331
+ handler: async (ctx) => {
332
+ const auth = createAuth(ctx);
333
+ return auth.api.rotateKeys();
334
+ },
335
+ });