create-vexcms 0.0.20 → 0.1.0-alpha.2

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 (153) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +93 -83
  3. package/dist/index.js +246 -81
  4. package/dist/index.mjs +30976 -0
  5. package/package.json +35 -15
  6. package/templates/base-nextjs/README.md +53 -205
  7. package/templates/base-nextjs/_env.example +7 -9
  8. package/templates/base-nextjs/_gitignore +41 -1
  9. package/templates/base-nextjs/convex/_generated/api.d.ts +6 -62
  10. package/templates/base-nextjs/convex/_generated/server.d.ts +19 -0
  11. package/templates/base-nextjs/convex/_generated/server.js +8 -0
  12. package/templates/base-nextjs/convex/auth/api.ts +49 -3
  13. package/templates/base-nextjs/convex/auth/db.ts +42 -300
  14. package/templates/base-nextjs/convex/auth/index.ts +11 -7
  15. package/templates/base-nextjs/convex/auth/options.ts +5 -29
  16. package/templates/base-nextjs/convex/auth/plugins/index.ts +15 -5
  17. package/templates/base-nextjs/convex/auth/sessions.ts +9 -15
  18. package/templates/base-nextjs/convex/http.ts +18 -3
  19. package/templates/base-nextjs/convex/schema.ts +104 -2
  20. package/templates/base-nextjs/convex/vex/firstUser.ts +10 -84
  21. package/templates/base-nextjs/convex/vex/globals.ts +19 -82
  22. package/templates/base-nextjs/convex/vex/media.ts +19 -246
  23. package/templates/base-nextjs/convex/vex.schema.ts +138 -0
  24. package/templates/base-nextjs/convex/vex.ts +19 -0
  25. package/templates/base-nextjs/convex.json +6 -0
  26. package/templates/base-nextjs/eslint.config.mjs +1 -21
  27. package/templates/base-nextjs/next.config.ts +10 -0
  28. package/templates/base-nextjs/package.json +47 -48
  29. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +17 -5
  30. package/templates/base-nextjs/src/app/(frontend)/auth/[pathname]/view.tsx +2 -4
  31. package/templates/base-nextjs/src/app/(frontend)/layout.tsx +12 -2
  32. package/templates/base-nextjs/src/app/(frontend)/page.tsx +2 -113
  33. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +40 -0
  34. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +22 -0
  35. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +19 -0
  36. package/templates/base-nextjs/src/app/globals.css +240 -104
  37. package/templates/base-nextjs/src/app/layout.tsx +23 -13
  38. package/templates/base-nextjs/src/app/unauthorized/page.tsx +24 -0
  39. package/templates/base-nextjs/src/auth/access.ts +46 -0
  40. package/templates/base-nextjs/src/auth/client.tsx +17 -5
  41. package/templates/base-nextjs/src/auth/hasPermission.ts +39 -0
  42. package/templates/base-nextjs/src/auth/serverUtils.ts +15 -25
  43. package/templates/base-nextjs/src/components/WelcomePage.tsx +116 -0
  44. package/templates/base-nextjs/src/components/component-example.tsx +56 -464
  45. package/templates/base-nextjs/src/components/providers/auth.tsx +13 -0
  46. package/templates/base-nextjs/src/components/providers/client.tsx +6 -1
  47. package/templates/base-nextjs/src/components/providers/convex.tsx +53 -21
  48. package/templates/base-nextjs/src/components/providers/server.tsx +10 -7
  49. package/templates/{marketing-site → base-nextjs}/src/components/ui/accordion.tsx +30 -32
  50. package/templates/base-nextjs/src/components/ui/badge.tsx +10 -13
  51. package/templates/base-nextjs/src/components/ui/input.tsx +35 -5
  52. package/templates/base-nextjs/src/context/AuthContext.tsx +29 -0
  53. package/templates/base-nextjs/src/db/constants/auth.ts +0 -1
  54. package/templates/base-nextjs/src/db/constants/index.ts +22 -35
  55. package/templates/base-nextjs/src/lib/utils.ts +0 -12
  56. package/templates/base-nextjs/src/proxy.ts +97 -12
  57. package/templates/base-nextjs/src/vex.config.ts +35 -0
  58. package/templates/base-nextjs/src/vex.types.ts +293 -0
  59. package/templates/base-nextjs/src/vexcms/collections/images.ts +15 -0
  60. package/templates/base-nextjs/src/vexcms/collections/index.ts +2 -1
  61. package/templates/base-nextjs/src/vexcms/collections/users.ts +7 -35
  62. package/templates/base-nextjs/tsconfig.json +3 -0
  63. package/templates/marketing-site/convex/_generated/api.d.ts +81 -0
  64. package/templates/marketing-site/convex/_generated/api.js +23 -0
  65. package/templates/marketing-site/convex/_generated/dataModel.d.ts +60 -0
  66. package/templates/marketing-site/convex/_generated/server.d.ts +162 -0
  67. package/templates/marketing-site/convex/_generated/server.js +101 -0
  68. package/templates/marketing-site/convex/footers.ts +11 -7
  69. package/templates/marketing-site/convex/headers.ts +15 -5
  70. package/templates/marketing-site/convex/pages.ts +20 -35
  71. package/templates/marketing-site/convex/schema.ts +119 -0
  72. package/templates/marketing-site/convex/seed.ts +582 -35
  73. package/templates/marketing-site/convex/siteSettings.ts +19 -5
  74. package/templates/marketing-site/convex/theme.ts +55 -61
  75. package/templates/marketing-site/convex/vex.schema.ts +355 -0
  76. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +27 -160
  77. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +6 -9
  78. package/templates/marketing-site/src/app/(frontend)/layout.tsx +4 -2
  79. package/templates/marketing-site/src/app/(frontend)/page.tsx +2 -5
  80. package/templates/marketing-site/src/components/SiteFooter.tsx +6 -3
  81. package/templates/marketing-site/src/components/SiteHeader.tsx +6 -3
  82. package/templates/marketing-site/src/components/ThemeLive.tsx +59 -0
  83. package/templates/marketing-site/src/components/ThemeStyle.tsx +45 -89
  84. package/templates/marketing-site/src/lib/metadata.ts +51 -72
  85. package/templates/marketing-site/src/vex.config.ts +35 -0
  86. package/templates/marketing-site/src/vex.types.ts +634 -0
  87. package/templates/marketing-site/src/vexcms/api.ts +23 -0
  88. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -3
  89. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +2 -2
  90. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -4
  91. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +2 -2
  92. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +4 -8
  93. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +2 -2
  94. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +7 -11
  95. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +1 -1
  96. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +9 -9
  97. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +4 -5
  98. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +3 -4
  99. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +10 -19
  100. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +4 -9
  101. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +3 -6
  102. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +28 -68
  103. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +13 -43
  104. package/templates/marketing-site/src/vexcms/blocks/config.ts +7 -22
  105. package/templates/marketing-site/src/vexcms/blocks/constants.ts +2 -3
  106. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -8
  107. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -1
  108. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -1
  109. package/templates/marketing-site/src/vexcms/collections/index.ts +2 -1
  110. package/templates/marketing-site/src/vexcms/collections/pages.ts +12 -21
  111. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +118 -0
  112. package/templates/marketing-site/src/vexcms/collections/themes.ts +29 -111
  113. package/templates/marketing-site/src/vexcms/globals/index.ts +1 -1
  114. package/templates/marketing-site/src/vexcms/globals/siteSettings.ts +43 -27
  115. package/templates/base-nextjs/convex/auth/adapter/index.ts +0 -230
  116. package/templates/base-nextjs/convex/auth/adapter/utils.ts +0 -547
  117. package/templates/base-nextjs/convex/auth/config.ts +0 -10
  118. package/templates/base-nextjs/convex/vex/auth.ts +0 -38
  119. package/templates/base-nextjs/convex/vex/collections.ts +0 -333
  120. package/templates/base-nextjs/convex/vex/helpers.ts +0 -33
  121. package/templates/base-nextjs/convex/vex/impersonation.ts +0 -51
  122. package/templates/base-nextjs/convex/vex/migrate.ts +0 -74
  123. package/templates/base-nextjs/convex/vex/model/collections.ts +0 -196
  124. package/templates/base-nextjs/convex/vex/model/media.ts +0 -87
  125. package/templates/base-nextjs/convex/vex/model/versions.ts +0 -254
  126. package/templates/base-nextjs/convex/vex/previewSnapshot.ts +0 -53
  127. package/templates/base-nextjs/convex/vex/versions.ts +0 -648
  128. package/templates/base-nextjs/src/app/admin/AdminLayoutWrapper.tsx +0 -33
  129. package/templates/base-nextjs/src/app/admin/AdminPageWrapper.tsx +0 -38
  130. package/templates/base-nextjs/src/app/admin/RichTextFieldWithMedia.tsx +0 -101
  131. package/templates/base-nextjs/src/app/admin/[[...path]]/page.tsx +0 -15
  132. package/templates/base-nextjs/src/app/admin/layout.tsx +0 -51
  133. package/templates/base-nextjs/src/app/favicon.ico +0 -0
  134. package/templates/base-nextjs/src/components/providers/theme.tsx +0 -11
  135. package/templates/base-nextjs/src/components/ui/theme-toggle.tsx +0 -67
  136. package/templates/base-nextjs/src/db/types.ts +0 -6
  137. package/templates/base-nextjs/src/vexcms/access.ts +0 -44
  138. package/templates/base-nextjs/src/vexcms/auth.ts +0 -4
  139. package/templates/base-nextjs/src/vexcms/collections/media.ts +0 -15
  140. package/templates/base-nextjs/vex.config.ts +0 -25
  141. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  142. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/PreviewPageContent.tsx +0 -55
  143. package/templates/marketing-site/src/app/(frontend)/preview/[slug]/page.tsx +0 -27
  144. package/templates/marketing-site/src/app/(frontend)/preview/layout.tsx +0 -18
  145. package/templates/marketing-site/src/app/admin/AdminLayoutWrapper.tsx +0 -71
  146. package/templates/marketing-site/src/components/ThemeInjector.tsx +0 -128
  147. package/templates/marketing-site/src/components/admin/IconPickerField.tsx +0 -134
  148. package/templates/marketing-site/src/components/admin/ThemeImport.tsx +0 -174
  149. package/templates/marketing-site/src/components/admin/ThemeImportField.tsx +0 -33
  150. package/templates/marketing-site/src/db/constants/index.ts +0 -52
  151. package/templates/marketing-site/src/lib/colorConvert.ts +0 -171
  152. package/templates/marketing-site/src/vexcms/access.ts +0 -36
  153. package/templates/marketing-site/vex.config.ts +0 -32
@@ -1,303 +1,45 @@
1
- import type { BetterAuthDBSchema } from "better-auth/db"
2
- import type { GenericId } from "convex/values"
1
+ import { internalMutation, internalQuery } from "@convex/_generated/server"
2
+ import { authDbApi } from "@vexcms/better-auth"
3
3
 
4
- import { v } from "convex/values"
5
-
6
- import type { WhereClause } from "./adapter/utils"
7
-
8
- import { internalMutation, internalQuery } from "../_generated/server"
9
4
  import schema from "../schema"
10
- import { checkUniqueFields, listOne, paginate, selectFields } from "./adapter/utils"
11
-
12
- // Helper to get Better Auth schema - we'll pass it from the adapter
13
- const getBetterAuthSchema = (schemaJson: string): BetterAuthDBSchema => {
14
- return JSON.parse(schemaJson)
15
- }
16
-
17
- // Create (insert) operation
18
- export const dbCreate = internalMutation({
19
- args: {
20
- betterAuthSchema: v.string(),
21
- data: v.any(),
22
- model: v.string(),
23
- select: v.optional(v.array(v.string())),
24
- },
25
- handler: async (ctx, { betterAuthSchema, data, model, select }) => {
26
- const authSchema = getBetterAuthSchema(betterAuthSchema)
27
-
28
- // Check unique fields before insert
29
- await checkUniqueFields(ctx, schema, authSchema, model, data)
30
-
31
- const id = await ctx.db.insert(model as any, data)
32
- const doc = await ctx.db.get(id)
33
- if (!doc) {
34
- throw new Error(`Failed to create ${model}`)
35
- }
36
-
37
- return selectFields(doc, select)
38
- },
39
- })
40
-
41
- // Find one operation
42
- export const dbFindOne = internalQuery({
43
- args: {
44
- betterAuthSchema: v.string(),
45
- model: v.string(),
46
- select: v.optional(v.array(v.string())),
47
- where: v.array(v.any()),
48
- },
49
- handler: async (ctx, { betterAuthSchema, model, select, where }) => {
50
- const authSchema = getBetterAuthSchema(betterAuthSchema)
51
- // @ts-expect-error GenericCtx type is a superset
52
- const result = await listOne(ctx, schema, authSchema, {
53
- model,
54
- select,
55
- where: where as WhereClause[],
56
- })
57
-
58
- return result
59
- },
60
- })
61
-
62
- // Find many operation
63
- export const dbFindMany = internalQuery({
64
- args: {
65
- betterAuthSchema: v.string(),
66
- limit: v.optional(v.number()),
67
- model: v.string(),
68
- sortBy: v.optional(
69
- v.object({
70
- direction: v.union(v.literal("asc"), v.literal("desc")),
71
- field: v.string(),
72
- })
73
- ),
74
- where: v.optional(v.array(v.any())),
75
- },
76
- handler: async (ctx, { betterAuthSchema, limit, model, sortBy, where }) => {
77
- const authSchema = getBetterAuthSchema(betterAuthSchema)
78
- const parsedWhere = (where ?? []) as WhereClause[]
79
-
80
- // Handle OR connector by running parallel queries
81
- if (parsedWhere.some((w) => w.connector === "OR")) {
82
- const results = await Promise.all(
83
- parsedWhere.map(async (w) => {
84
- // @ts-expect-error GenericCtx type is a superset
85
- const result = await paginate(ctx, schema, authSchema, {
86
- model,
87
- paginationOpts: { cursor: null, numItems: limit ?? 200 },
88
- sortBy,
89
- where: [w],
90
- })
91
- return result.page
92
- })
93
- )
94
-
95
- // De-duplicate and flatten
96
- const seen = new Set<string>()
97
- const uniqueDocs: any[] = []
98
- for (const docs of results) {
99
- for (const doc of docs) {
100
- const docId = doc._id as string
101
- if (!seen.has(docId)) {
102
- seen.add(docId)
103
- uniqueDocs.push(doc)
104
- }
105
- }
106
- }
107
-
108
- // Apply sorting if needed
109
- if (sortBy) {
110
- uniqueDocs.sort((a, b) => {
111
- const aVal = a[sortBy.field]
112
- const bVal = b[sortBy.field]
113
- if (aVal === bVal) {
114
- return 0
115
- }
116
- const comparison = aVal > bVal ? 1 : -1
117
- return sortBy.direction === "desc" ? -comparison : comparison
118
- })
119
- }
120
-
121
- return uniqueDocs.slice(0, limit)
122
- }
123
-
124
- // Normal case without OR
125
- // @ts-expect-error GenericCtx type is a superset
126
- const result = await paginate(ctx, schema, authSchema, {
127
- model,
128
- paginationOpts: { cursor: null, numItems: limit ?? 200 },
129
- sortBy,
130
- where: parsedWhere,
131
- })
132
-
133
- return result.page
134
- },
135
- })
136
-
137
- // Count operation
138
- export const dbCount = internalQuery({
139
- args: {
140
- betterAuthSchema: v.string(),
141
- model: v.string(),
142
- where: v.optional(v.array(v.any())),
143
- },
144
- handler: async (ctx, { betterAuthSchema, model, where }) => {
145
- const authSchema = getBetterAuthSchema(betterAuthSchema)
146
- const parsedWhere = (where ?? []) as WhereClause[]
147
-
148
- // Handle OR connector
149
- if (parsedWhere.some((w) => w.connector === "OR")) {
150
- const results = await Promise.all(
151
- parsedWhere.map(async (w) => {
152
- // @ts-expect-error GenericCtx type is a superset
153
- const result = await paginate(ctx, schema, authSchema, {
154
- model,
155
- paginationOpts: { cursor: null, numItems: 200 },
156
- where: [w],
157
- })
158
- return result.page
159
- })
160
- )
161
-
162
- // De-duplicate and count
163
- const seen = new Set<string>()
164
- for (const docs of results) {
165
- for (const doc of docs) {
166
- const docId = doc._id as string
167
- seen.add(docId)
168
- }
169
- }
170
- return seen.size
171
- }
172
-
173
- // Normal case
174
- // @ts-expect-error GenericCtx type is a superset
175
- const result = await paginate(ctx, schema, authSchema, {
176
- model,
177
- paginationOpts: { cursor: null, numItems: 200 },
178
- where: parsedWhere,
179
- })
180
-
181
- return result.page.length
182
- },
183
- })
184
-
185
- // Update operation
186
- export const dbUpdate = internalMutation({
187
- args: {
188
- betterAuthSchema: v.string(),
189
- model: v.string(),
190
- update: v.any(),
191
- where: v.array(v.any()),
192
- },
193
- handler: async (ctx, { betterAuthSchema, model, update, where }) => {
194
- const authSchema = getBetterAuthSchema(betterAuthSchema)
195
- const parsedWhere = where as WhereClause[]
196
-
197
- // Find the document to update
198
- const doc = await listOne(ctx, schema, authSchema, {
199
- model,
200
- where: parsedWhere,
201
- })
202
-
203
- if (!doc) {
204
- return null
205
- }
206
-
207
- // Check unique fields before update
208
- await checkUniqueFields(ctx, schema, authSchema, model, update, doc)
209
-
210
- await ctx.db.patch(doc._id as GenericId<any>, update)
211
- return await ctx.db.get(doc._id as GenericId<any>)
212
- },
213
- })
214
-
215
- // Update many operation
216
- export const dbUpdateMany = internalMutation({
217
- args: {
218
- betterAuthSchema: v.string(),
219
- model: v.string(),
220
- update: v.any(),
221
- where: v.array(v.any()),
222
- },
223
- handler: async (ctx, { betterAuthSchema, model, update, where }) => {
224
- const authSchema = getBetterAuthSchema(betterAuthSchema)
225
- const parsedWhere = where as WhereClause[]
226
-
227
- const result = await paginate(ctx, schema, authSchema, {
228
- model,
229
- paginationOpts: { cursor: null, numItems: 200 },
230
- where: parsedWhere,
231
- })
232
-
233
- // Check unique fields if updating multiple docs
234
- if (result.page.length > 1) {
235
- const uniqueFieldKeys = Object.keys(update).filter(
236
- (key) => authSchema[model]?.fields?.[key]?.unique
237
- )
238
- if (uniqueFieldKeys.length > 0) {
239
- throw new Error(
240
- `Attempted to set unique fields in multiple documents in ${model} with the same value. Fields: ${uniqueFieldKeys.join(", ")}`
241
- )
242
- }
243
- }
244
-
245
- // Update each document
246
- for (const doc of result.page) {
247
- await checkUniqueFields(ctx, schema, authSchema, model, update, doc)
248
- await ctx.db.patch(doc._id as GenericId<any>, update)
249
- }
250
-
251
- return result.page.length
252
- },
253
- })
254
-
255
- // Delete operation
256
- export const dbDelete = internalMutation({
257
- args: {
258
- betterAuthSchema: v.string(),
259
- model: v.string(),
260
- where: v.array(v.any()),
261
- },
262
- handler: async (ctx, { betterAuthSchema, model, where }) => {
263
- const authSchema = getBetterAuthSchema(betterAuthSchema)
264
- const parsedWhere = where as WhereClause[]
265
-
266
- const doc = await listOne(ctx, schema, authSchema, {
267
- model,
268
- where: parsedWhere,
269
- })
270
-
271
- if (!doc) {
272
- return
273
- }
274
-
275
- await ctx.db.delete(doc._id as GenericId<any>)
276
- },
277
- })
278
-
279
- // Delete many operation
280
- export const dbDeleteMany = internalMutation({
281
- args: {
282
- betterAuthSchema: v.string(),
283
- model: v.string(),
284
- where: v.array(v.any()),
285
- },
286
- handler: async (ctx, { betterAuthSchema, model, where }) => {
287
- const authSchema = getBetterAuthSchema(betterAuthSchema)
288
- const parsedWhere = where as WhereClause[]
289
-
290
- const result = await paginate(ctx, schema, authSchema, {
291
- model,
292
- paginationOpts: { cursor: null, numItems: 200 },
293
- where: parsedWhere,
294
- })
295
-
296
- // Delete each document
297
- for (const doc of result.page) {
298
- await ctx.db.delete(doc._id as GenericId<any>)
299
- }
300
5
 
301
- return result.page.length
302
- },
303
- })
6
+ /**
7
+ * Convex internal mutations and queries that implement Better Auth's DB interface.
8
+ *
9
+ * Wire these into your Convex schema so `@vexcms/better-auth` can call them
10
+ * via `ctx.runMutation(anyApi.auth.db.dbCreate, ...)`:
11
+ *
12
+ * ```ts
13
+ * // convex/schema.ts
14
+ * import { dbCreate, dbFindOne, ... } from "./auth/db";
15
+ *
16
+ * // register as internal (visibility: internal) in your schema tables:
17
+ * export default defineSchema({
18
+ * users: defineTable(...).index("by_email", ["email"]),
19
+ * sessions: defineTable(...).index("by_token", ["token"]),
20
+ * }, {
21
+ * internal: {
22
+ * dbCreate,
23
+ * dbFindOne,
24
+ * dbFindMany,
25
+ * dbCount,
26
+ * dbUpdate,
27
+ * dbUpdateMany,
28
+ * dbDelete,
29
+ * dbDeleteMany,
30
+ * },
31
+ * });
32
+ * ```
33
+ *
34
+ * @see {@link authDbApi} for the factory that produces these functions
35
+ */
36
+ export const {
37
+ dbCreate,
38
+ dbFindOne,
39
+ dbFindMany,
40
+ dbCount,
41
+ dbUpdate,
42
+ dbUpdateMany,
43
+ dbDelete,
44
+ dbDeleteMany,
45
+ } = authDbApi({ schema, internalQuery, internalMutation })
@@ -1,14 +1,18 @@
1
1
  import type { GenericActionCtx } from "convex/server"
2
2
 
3
+ import { createBetterAuthAdapter } from "@vexcms/better-auth"
3
4
  import { betterAuth } from "better-auth"
4
5
 
5
- import type { DataModel } from "../_generated/dataModel"
6
+ import { authOptions } from "./options"
6
7
 
7
- import { buildBetterAuthOptions } from "./options"
8
+ import type { DataModel } from "../_generated/dataModel"
8
9
 
9
- export const createAuth = (
10
- ctx: GenericActionCtx<DataModel>,
11
- { optionsOnly } = { optionsOnly: false }
12
- ) => {
13
- return betterAuth(buildBetterAuthOptions({ ctx, optionsOnly }))
10
+ export const createAuth = (ctx: GenericActionCtx<DataModel>, { optionsOnly } = { optionsOnly: false }) => {
11
+ return betterAuth({
12
+ database: createBetterAuthAdapter(ctx),
13
+ logger: {
14
+ disabled: optionsOnly,
15
+ },
16
+ ...authOptions,
17
+ })
14
18
  }
@@ -1,7 +1,4 @@
1
- import { type DataModel } from "@convex/_generated/dataModel"
2
- import schema from "@convex/schema"
3
- import { type BetterAuthOptions } from "better-auth"
4
- import { type GenericActionCtx } from "convex/server"
1
+ import type { BetterAuthOptions } from "better-auth"
5
2
 
6
3
  import {
7
4
  TABLE_SLUG_ACCOUNTS,
@@ -11,20 +8,16 @@ import {
11
8
  USER_ROLES,
12
9
  } from "~/db/constants"
13
10
 
14
- import { convexAdapter } from "./adapter"
15
- import betterAuthPlugins from "./plugins"
11
+ import { createPlugins } from "./plugins"
16
12
 
17
- export const betterAuthOptions: BetterAuthOptions = {
13
+ export const authOptions: BetterAuthOptions = {
18
14
  account: {
19
15
  modelName: TABLE_SLUG_ACCOUNTS,
20
16
  },
21
17
  baseURL: process.env.SITE_URL,
22
18
  // {{EMAIL_PASSWORD_AUTH}}
23
19
  // {{OAUTH_PROVIDERS}}
24
- logger: {
25
- disabled: false,
26
- },
27
- plugins: betterAuthPlugins,
20
+ plugins: createPlugins(),
28
21
  secret: process.env.BETTER_AUTH_SECRET,
29
22
  session: {
30
23
  modelName: TABLE_SLUG_SESSIONS,
@@ -32,7 +25,7 @@ export const betterAuthOptions: BetterAuthOptions = {
32
25
  trustedOrigins: [process.env.SITE_URL!],
33
26
  user: {
34
27
  additionalFields: {
35
- role: {
28
+ roles: {
36
29
  type: "string[]",
37
30
  defaultValue: [USER_ROLES.user],
38
31
  required: true,
@@ -44,20 +37,3 @@ export const betterAuthOptions: BetterAuthOptions = {
44
37
  modelName: TABLE_SLUG_VERIFICATIONS,
45
38
  },
46
39
  }
47
-
48
- export function buildBetterAuthOptions({
49
- ctx,
50
- optionsOnly = false,
51
- }: {
52
- ctx: GenericActionCtx<DataModel>
53
- optionsOnly?: boolean
54
- }) {
55
- return {
56
- ...betterAuthOptions,
57
- database: convexAdapter(ctx, schema),
58
- logger: {
59
- ...betterAuthOptions.logger,
60
- disabled: optionsOnly,
61
- },
62
- }
63
- }
@@ -1,20 +1,30 @@
1
+ import { apiKey } from "@better-auth/api-key"
1
2
  import { convex } from "@convex-dev/better-auth/plugins"
2
3
  import authConfig from "@convex/auth.config"
3
4
  import { nextCookies } from "better-auth/next-js"
4
- import { admin, apiKey } from "better-auth/plugins"
5
+ import { admin, anonymous } from "better-auth/plugins"
5
6
  // {{ORGANIZATIONS_IMPORT}}
6
7
 
7
8
  import { USER_ROLES } from "~/db/constants"
8
9
 
9
- const plugins = [
10
+ /**
11
+ * Returns a fresh array of Better Auth plugins for each VexCMS auth session.
12
+ *
13
+ * Returns a new array on every call so that plugin initialization — including
14
+ * the `convex()` factory, which internally calls the deprecated oidc-provider
15
+ * plugin — runs inside `createAuth()` rather than at module-eval time. This
16
+ * lets `http.ts`'s console.warn filter suppress the deprecation noise before
17
+ * it fires.
18
+ */
19
+ export const createPlugins = () => [
10
20
  admin({
11
21
  adminRoles: [USER_ROLES.admin],
12
22
  defaultRole: USER_ROLES.user,
13
23
  }),
24
+ anonymous(),
14
25
  apiKey(),
15
26
  convex({ authConfig }),
16
- nextCookies(),
17
27
  // {{ORGANIZATIONS_PLUGIN}}
28
+ // this plugin must be last, per the convex dev CLI's own warnings
29
+ nextCookies(),
18
30
  ]
19
-
20
- export default plugins
@@ -1,9 +1,8 @@
1
+ import { type Id } from "@convex/_generated/dataModel"
1
2
  import { v } from "convex/values"
2
3
 
3
4
  import { TABLE_SLUG_SESSIONS, type TABLE_SLUG_USERS } from "~/db/constants"
4
5
 
5
- import type { Id } from "../_generated/dataModel"
6
-
7
6
  import { query } from "../_generated/server"
8
7
 
9
8
  /**
@@ -15,28 +14,23 @@ export const getSessionWithUser = query({
15
14
  sessionToken: v.string(),
16
15
  },
17
16
  handler: async (ctx, args) => {
18
- // Find the session by token
19
17
  const session = await ctx.db
20
18
  .query(TABLE_SLUG_SESSIONS)
21
19
  .withIndex("by_token", (q) => q.eq("token", args.sessionToken))
22
20
  .first()
23
21
 
24
- if (!session) {
25
- return null
26
- }
22
+ if (!session) return null
27
23
 
28
- // Check if session is expired
29
24
  if (session.expiresAt < Date.now()) {
30
- console.error("Session Expired")
25
+ console.error("[getSessionWithUser] Session Expired")
31
26
  return null
32
27
  }
33
28
 
34
- // Get the user data
35
- const user = await ctx.db.get(session.userId as Id<typeof TABLE_SLUG_USERS>)
29
+ const userId = session.userId
30
+ if (!userId) return null
36
31
 
37
- if (!user) {
38
- return null
39
- }
32
+ const user = await ctx.db.get(userId as Id<typeof TABLE_SLUG_USERS>)
33
+ if (!user) return null
40
34
 
41
35
  return {
42
36
  session: {
@@ -48,12 +42,12 @@ export const getSessionWithUser = query({
48
42
  userId: session.userId,
49
43
  },
50
44
  user: {
51
- id: user._id,
45
+ id: user.userId ?? user._id,
52
46
  name: user.name,
53
47
  email: user.email,
54
48
  emailVerified: user.emailVerified,
55
49
  image: user.image,
56
- role: user.role,
50
+ roles: user.roles,
57
51
  },
58
52
  }
59
53
  },
@@ -7,9 +7,24 @@ const http = httpRouter()
7
7
 
8
8
  // Auth request handler - basePath hardcoded to /api/auth for now
9
9
  const authRequestHandler = httpAction(async (ctx, request) => {
10
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
11
- const auth = createAuth(ctx)
12
- return await auth.handler(request)
10
+ // Suppress the oidc-provider deprecation warning spammed by @convex-dev/better-auth
11
+ // on every request. Convex wraps console.warn per-invocation, so this must run
12
+ // inside the handler (not at module level) to intercept after Convex's wrapper is set.
13
+ // Remove once @convex-dev/better-auth migrates to @better-auth/oauth-provider.
14
+ const _warn = console.warn
15
+ console.warn = (...args: unknown[]) => {
16
+ if (typeof args[0] === "string" && args[0].includes("oidc-provider")) {
17
+ return
18
+ }
19
+ _warn(...args)
20
+ }
21
+ try {
22
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
23
+ const auth = createAuth(ctx)
24
+ return await auth.handler(request)
25
+ } finally {
26
+ console.warn = _warn
27
+ }
13
28
  })
14
29
 
15
30
  // Register auth routes for GET and POST
@@ -1,3 +1,105 @@
1
- import { defineSchema } from "convex/server"
1
+ import { defineSchema, defineTable } from "convex/server"
2
+ import { v } from "convex/values"
2
3
 
3
- export default defineSchema({})
4
+ import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_USERS } from "~/db/constants"
5
+
6
+ import { apikey, images, jwks, session, verification } from "./vex.schema"
7
+
8
+ export default defineSchema({
9
+ // `organization`/`team`/`teamMember`/`member`/`invitation`/`vex_globals` are
10
+ // hand-written, not imported from `./vex.schema` — the generator only emits
11
+ // them when the `organization()` plugin (`--orgs`) or a registered global is
12
+ // actually present in `vex.config.ts`. `access.ts` and every `getAuth`
13
+ // caller (`convex/vex.ts`, `convex/vex/media.ts`, `convex/vex/globals.ts`)
14
+ // reference `orgCollectionSlug`/`resolveOrgs: true` unconditionally, so
15
+ // these tables must exist in the schema from day one — enabling `--orgs`
16
+ // or registering a global later needs no schema migration.
17
+ vex_globals: defineTable({
18
+ slug: v.string(),
19
+ data: v.any(),
20
+ }).index("by_slug", ["slug"]),
21
+ images,
22
+ team: defineTable({
23
+ name: v.string(),
24
+ organizationId: v.string(),
25
+ createdAt: v.number(),
26
+ updatedAt: v.optional(v.number()),
27
+ }).index("by_organizationId", ["organizationId"]),
28
+ teamMember: defineTable({
29
+ teamId: v.string(),
30
+ userId: v.string(),
31
+ createdAt: v.optional(v.number()),
32
+ })
33
+ .index("by_teamId", ["teamId"])
34
+ .index("by_userId", ["userId"]),
35
+ organization: defineTable({
36
+ name: v.string(),
37
+ slug: v.string(),
38
+ logo: v.optional(v.string()),
39
+ createdAt: v.number(),
40
+ metadata: v.optional(v.string()),
41
+ }).index("by_slug", ["slug"]),
42
+ member: defineTable({
43
+ organizationId: v.string(),
44
+ userId: v.string(),
45
+ role: v.string(),
46
+ createdAt: v.number(),
47
+ })
48
+ .index("by_organizationId", ["organizationId"])
49
+ .index("by_userId", ["userId"]),
50
+ invitation: defineTable({
51
+ organizationId: v.string(),
52
+ email: v.string(),
53
+ role: v.optional(v.string()),
54
+ teamId: v.optional(v.string()),
55
+ status: v.string(),
56
+ expiresAt: v.number(),
57
+ createdAt: v.number(),
58
+ inviterId: v.string(),
59
+ roles: v.array(v.string()),
60
+ })
61
+ .index("by_organizationId", ["organizationId"])
62
+ .index("by_email", ["email"]),
63
+ session,
64
+ verification,
65
+ apikey,
66
+ jwks,
67
+
68
+ [TABLE_SLUG_USERS]: defineTable({
69
+ name: v.string(),
70
+ banExpires: v.optional(v.number()), // admin plugin
71
+ banned: v.optional(v.boolean()), // admin plugin
72
+ banReason: v.optional(v.string()), // admin plugin
73
+ createdAt: v.number(),
74
+ displayUsername: v.optional(v.union(v.null(), v.string())),
75
+ email: v.string(),
76
+ emailVerified: v.boolean(),
77
+ image: v.optional(v.string()),
78
+ isAnonymous: v.optional(v.union(v.null(), v.boolean())),
79
+ phoneNumber: v.optional(v.union(v.null(), v.string())),
80
+ phoneNumberVerified: v.optional(v.union(v.null(), v.boolean())),
81
+ role: v.optional(v.string()), // admin plugin — single string in BA 1.6
82
+ roles: v.array(v.string()), // our multi-role field via additionalFields
83
+ twoFactorEnabled: v.optional(v.union(v.null(), v.boolean())),
84
+ updatedAt: v.number(),
85
+ userId: v.optional(v.union(v.null(), v.string())),
86
+ username: v.optional(v.union(v.null(), v.string())),
87
+ }).index("by_email", ["email"]),
88
+
89
+ [TABLE_SLUG_ACCOUNTS]: defineTable({
90
+ accessToken: v.optional(v.string()),
91
+ accessTokenExpiresAt: v.optional(v.number()),
92
+ accountId: v.string(),
93
+ createdAt: v.number(),
94
+ idToken: v.optional(v.string()),
95
+ password: v.optional(v.string()),
96
+ providerId: v.string(),
97
+ refreshToken: v.optional(v.string()),
98
+ refreshTokenExpiresAt: v.optional(v.number()),
99
+ scope: v.optional(v.string()),
100
+ updatedAt: v.number(),
101
+ userId: v.string(),
102
+ })
103
+ .index("by_userId", ["userId"])
104
+ .index("by_accountId", ["accountId"]),
105
+ })