create-vexcms 0.1.0-alpha.2 → 0.1.0-alpha.21

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 (115) hide show
  1. package/README.md +48 -14
  2. package/dist/index.js +1714 -1504
  3. package/package.json +2 -2
  4. package/templates/base-nextjs/README.md +18 -5
  5. package/templates/base-nextjs/convex/auth/index.ts +2 -2
  6. package/templates/base-nextjs/convex/auth/options.ts +27 -26
  7. package/templates/base-nextjs/convex/auth/schema.ts +51 -0
  8. package/templates/base-nextjs/convex/auth/sessions.ts +3 -3
  9. package/templates/base-nextjs/convex/tsconfig.json +2 -2
  10. package/templates/base-nextjs/convex/vex/firstUser.ts +2 -2
  11. package/templates/base-nextjs/convex/vex/globals.ts +3 -2
  12. package/templates/base-nextjs/convex/vex/media.ts +3 -2
  13. package/templates/base-nextjs/convex/vex.schema.ts +1 -0
  14. package/templates/base-nextjs/convex/vex.ts +17 -4
  15. package/templates/base-nextjs/next.config.ts +52 -6
  16. package/templates/base-nextjs/package.json +18 -7
  17. package/templates/base-nextjs/public/favicons/favicon.ico +0 -0
  18. package/templates/base-nextjs/src/app/(frontend)/@auth/(...)auth/[pathname]/view.tsx +27 -15
  19. package/templates/base-nextjs/src/app/(vexcms)/admin/[[...path]]/page.tsx +2 -2
  20. package/templates/base-nextjs/src/app/(vexcms)/admin/clientProviders.tsx +9 -14
  21. package/templates/base-nextjs/src/app/(vexcms)/admin/layout.tsx +8 -6
  22. package/templates/base-nextjs/src/app/api/vex/revalidate/route.ts +19 -0
  23. package/templates/base-nextjs/src/app/globals.css +10 -9
  24. package/templates/base-nextjs/src/app/layout.tsx +6 -0
  25. package/templates/base-nextjs/src/auth/access.ts +14 -2
  26. package/templates/base-nextjs/src/auth/serverUtils.ts +20 -6
  27. package/templates/base-nextjs/src/components/WelcomePage.tsx +54 -43
  28. package/templates/base-nextjs/src/components/component-example.tsx +1 -1
  29. package/templates/base-nextjs/src/components/providers/server.tsx +16 -8
  30. package/templates/base-nextjs/src/vex.config.server.ts +30 -0
  31. package/templates/base-nextjs/src/vex.config.ts +14 -11
  32. package/templates/base-nextjs/src/vex.types.ts +2 -1
  33. package/templates/base-nextjs/src/vexcms/collections/images.ts +1 -1
  34. package/templates/base-nextjs/src/vexcms/collections/users.ts +3 -1
  35. package/templates/marketing-site/convex/pages.ts +20 -1
  36. package/templates/marketing-site/convex/schema.ts +4 -4
  37. package/templates/marketing-site/convex/seed.ts +515 -70
  38. package/templates/marketing-site/convex/siteSettings.ts +1 -1
  39. package/templates/marketing-site/convex/theme.ts +3 -3
  40. package/templates/marketing-site/convex/vex.schema.ts +75 -38
  41. package/templates/marketing-site/public/favicon.svg +14 -0
  42. package/templates/marketing-site/public/favicons/apple-touch-icon.png +0 -0
  43. package/templates/marketing-site/public/favicons/favicon.ico +0 -0
  44. package/templates/marketing-site/src/app/(frontend)/(site)/PageContent.tsx +64 -0
  45. package/templates/marketing-site/src/app/(frontend)/(site)/SiteLivePreviewProvider.tsx +31 -0
  46. package/templates/marketing-site/src/app/(frontend)/(site)/[slug]/page.tsx +63 -0
  47. package/templates/marketing-site/src/app/(frontend)/(site)/layout.tsx +50 -0
  48. package/templates/marketing-site/src/app/(frontend)/(site)/page.tsx +33 -0
  49. package/templates/marketing-site/src/app/(frontend)/layout.tsx +10 -24
  50. package/templates/marketing-site/src/app/(vexcms)/admin/layout.tsx +32 -0
  51. package/templates/marketing-site/src/app/globals.css +413 -0
  52. package/templates/marketing-site/src/app/layout.tsx +85 -0
  53. package/templates/marketing-site/src/app/robots.ts +13 -0
  54. package/templates/marketing-site/src/app/sitemap.ts +18 -0
  55. package/templates/marketing-site/src/components/BrandMark.tsx +27 -0
  56. package/templates/marketing-site/src/components/CodeHighlightContext.tsx +76 -0
  57. package/templates/marketing-site/src/components/CodePane.tsx +98 -0
  58. package/templates/marketing-site/src/components/Container.tsx +25 -0
  59. package/templates/marketing-site/src/components/CopyButton.tsx +80 -0
  60. package/templates/marketing-site/src/components/FirstAdminBootstrap.tsx +54 -0
  61. package/templates/marketing-site/src/components/InstallCommand.tsx +29 -0
  62. package/templates/marketing-site/src/components/MediaImage.tsx +59 -0
  63. package/templates/marketing-site/src/components/SectionHeader.tsx +51 -0
  64. package/templates/marketing-site/src/components/SiteFooter.tsx +8 -6
  65. package/templates/marketing-site/src/components/SiteHeader.tsx +8 -6
  66. package/templates/marketing-site/src/components/ThemeLive.tsx +1 -2
  67. package/templates/marketing-site/src/components/ThemeStyle.tsx +3 -3
  68. package/templates/marketing-site/src/components/motion-primitives/animated-group.tsx +44 -120
  69. package/templates/marketing-site/src/components/motion-primitives/reveal.module.css +32 -0
  70. package/templates/marketing-site/src/components/motion-primitives/text-effect.tsx +47 -266
  71. package/templates/marketing-site/src/lib/codeHighlightKey.ts +27 -0
  72. package/templates/marketing-site/src/lib/highlight.ts +134 -0
  73. package/templates/marketing-site/src/lib/highlightClient.ts +37 -0
  74. package/templates/marketing-site/src/lib/metadata.ts +61 -33
  75. package/templates/marketing-site/src/lib/resolvePagePath.ts +8 -0
  76. package/templates/marketing-site/src/lib/vex.ts +18 -0
  77. package/templates/marketing-site/src/proxy.ts +179 -0
  78. package/templates/marketing-site/src/vex.config.server.ts +27 -0
  79. package/templates/marketing-site/src/vex.config.ts +27 -9
  80. package/templates/marketing-site/src/vex.types.ts +102 -76
  81. package/templates/marketing-site/src/vexcms/api.ts +2 -2
  82. package/templates/marketing-site/src/vexcms/blocks/CTA/config.ts +3 -2
  83. package/templates/marketing-site/src/vexcms/blocks/CTA/index.tsx +40 -24
  84. package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/config.ts +62 -0
  85. package/templates/marketing-site/src/vexcms/blocks/CodeShowcase/index.tsx +106 -0
  86. package/templates/marketing-site/src/vexcms/blocks/FAQ/config.ts +4 -3
  87. package/templates/marketing-site/src/vexcms/blocks/FAQ/index.tsx +63 -50
  88. package/templates/marketing-site/src/vexcms/blocks/Features/config.ts +1 -0
  89. package/templates/marketing-site/src/vexcms/blocks/Features/index.tsx +43 -58
  90. package/templates/marketing-site/src/vexcms/blocks/Footer/config.ts +6 -3
  91. package/templates/marketing-site/src/vexcms/blocks/Footer/index.tsx +90 -53
  92. package/templates/marketing-site/src/vexcms/blocks/Header/config.ts +10 -3
  93. package/templates/marketing-site/src/vexcms/blocks/Header/index.tsx +205 -138
  94. package/templates/marketing-site/src/vexcms/blocks/Hero/config.ts +27 -12
  95. package/templates/marketing-site/src/vexcms/blocks/Hero/index.tsx +174 -119
  96. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/config.ts +2 -1
  97. package/templates/marketing-site/src/vexcms/blocks/HowItWorks/index.tsx +59 -51
  98. package/templates/marketing-site/src/vexcms/blocks/Roadmap/config.ts +26 -5
  99. package/templates/marketing-site/src/vexcms/blocks/Roadmap/index.tsx +256 -73
  100. package/templates/marketing-site/src/vexcms/blocks/Split/config.ts +62 -0
  101. package/templates/marketing-site/src/vexcms/blocks/Split/index.tsx +186 -0
  102. package/templates/marketing-site/src/vexcms/blocks/Stats/config.ts +35 -0
  103. package/templates/marketing-site/src/vexcms/blocks/Stats/index.tsx +77 -0
  104. package/templates/marketing-site/src/vexcms/blocks/config.ts +27 -11
  105. package/templates/marketing-site/src/vexcms/blocks/constants.ts +3 -0
  106. package/templates/marketing-site/src/vexcms/blocks/index.ts +9 -0
  107. package/templates/marketing-site/src/vexcms/collections/footers.ts +1 -0
  108. package/templates/marketing-site/src/vexcms/collections/headers.ts +1 -0
  109. package/templates/marketing-site/src/vexcms/collections/pages.ts +23 -1
  110. package/templates/marketing-site/src/vexcms/collections/themeColors.ts +14 -14
  111. package/templates/marketing-site/src/vexcms/collections/users.ts +20 -0
  112. package/dist/index.mjs +0 -30976
  113. package/templates/marketing-site/src/app/(frontend)/PageContent.tsx +0 -53
  114. package/templates/marketing-site/src/app/(frontend)/[slug]/page.tsx +0 -25
  115. package/templates/marketing-site/src/app/(frontend)/page.tsx +0 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-vexcms",
3
- "version": "0.1.0-alpha.2",
3
+ "version": "0.1.0-alpha.21",
4
4
  "description": "Scaffold a new VexCMS project",
5
5
  "keywords": [
6
6
  "cms",
@@ -51,7 +51,7 @@
51
51
  },
52
52
  "scripts": {
53
53
  "build": "tsup src/index.ts --format esm --dts",
54
- "dev": "tsup src/index.ts --format esm --watch",
54
+ "dev": "tsup src/index.ts --format esm --watch src",
55
55
  "test": "vitest run"
56
56
  }
57
57
  }
@@ -31,9 +31,21 @@ NEXT_PUBLIC_CONVEX_URL=https://your-deployment.convex.cloud
31
31
  NEXT_PUBLIC_CONVEX_SITE_URL=https://your-deployment.convex.site
32
32
  ```
33
33
 
34
- Then, in the [Convex Dashboard](https://dashboard.convex.dev) → Settings →
35
- Environment Variables, add `BETTER_AUTH_SECRET` (same value as `.env.local`)
36
- and `SITE_URL` (`http://localhost:3010` for local dev).
34
+ `next.config.ts` reads `NEXT_PUBLIC_CONVEX_URL` at build/start time and adds that
35
+ deployment's hostname to `images.remotePatterns` automatically, so `next/image` can serve
36
+ uploaded media without a manual edit.
37
+
38
+ Better Auth runs inside your Convex deployment, not in the Next.js server —
39
+ it does **not** read `.env.local`. Set the same two values on the
40
+ deployment itself:
41
+
42
+ ```bash
43
+ npx convex env set SITE_URL http://localhost:3010
44
+ npx convex env set BETTER_AUTH_SECRET <the BETTER_AUTH_SECRET value from .env.local>
45
+ ```
46
+
47
+ Skipping this step is the most common cause of a `403` on your first sign-in
48
+ attempt.
37
49
 
38
50
  ### 4. Run the dev servers
39
51
 
@@ -65,10 +77,11 @@ see `convex/vex/firstUser.ts`. From there, "Go to Admin Panel" opens `/admin`.
65
77
  │ ├── auth/ # Auth client + server helpers, access control
66
78
  │ ├── components/ # React components
67
79
  │ ├── vexcms/collections/ # Collection definitions
68
- │ ├── vex.config.ts # VexCMS config
80
+ │ ├── vex.config.ts # VexCMS client config (collections, globals, access)
81
+ │ ├── vex.config.server.ts # VexCMS server config (auth adapter, storage adapters)
69
82
  │ └── env.mjs # Typed environment variables
70
83
  ├── convex/ # Convex backend
71
- │ ├── auth/ # Better Auth config + plugins
84
+ │ ├── auth/ # Better Auth config + plugins (schema.ts, options.ts)
72
85
  │ ├── vex/ # Bootstrap, media, globals endpoints
73
86
  │ └── schema.ts # Database schema
74
87
  └── .env.local # Environment variables (not committed)
@@ -3,10 +3,10 @@ import type { GenericActionCtx } from "convex/server"
3
3
  import { createBetterAuthAdapter } from "@vexcms/better-auth"
4
4
  import { betterAuth } from "better-auth"
5
5
 
6
- import { authOptions } from "./options"
7
-
8
6
  import type { DataModel } from "../_generated/dataModel"
9
7
 
8
+ import { authOptions } from "./options"
9
+
10
10
  export const createAuth = (ctx: GenericActionCtx<DataModel>, { optionsOnly } = { optionsOnly: false }) => {
11
11
  return betterAuth({
12
12
  database: createBetterAuthAdapter(ctx),
@@ -1,39 +1,40 @@
1
1
  import type { BetterAuthOptions } from "better-auth"
2
2
 
3
- import {
4
- TABLE_SLUG_ACCOUNTS,
5
- TABLE_SLUG_SESSIONS,
6
- TABLE_SLUG_USERS,
7
- TABLE_SLUG_VERIFICATIONS,
8
- USER_ROLES,
9
- } from "~/db/constants"
3
+ import { anonRoleDatabaseHook } from "@vexcms/better-auth"
4
+
5
+ import { USER_ROLES } from "~/db/constants"
10
6
 
11
7
  import { createPlugins } from "./plugins"
8
+ import { authSchema } from "./schema"
9
+
10
+ // `plugins` on the shared schema is a descriptor map for
11
+ // `betterAuthCollections`, not Better Auth plugin instances — dropped by name
12
+ // so the intent survives a reordering of the object literal below.
13
+ const { plugins: _pluginDescriptors, ...schemaOptions } = authSchema
12
14
 
15
+ /**
16
+ * Better Auth options for this project.
17
+ *
18
+ * Model names and `user.additionalFields` come from `./schema`, the one module
19
+ * both this file and `src/vex.config.ts` read, so a field added there reaches
20
+ * the database and the admin panel without being written twice. Everything
21
+ * below the spread is server-only.
22
+ *
23
+ * @see ./schema for the shared, client-safe half
24
+ */
13
25
  export const authOptions: BetterAuthOptions = {
14
- account: {
15
- modelName: TABLE_SLUG_ACCOUNTS,
16
- },
26
+ ...schemaOptions,
17
27
  baseURL: process.env.SITE_URL,
28
+ databaseHooks: {
29
+ // Ties Better Auth's anonymous-plugin users to `access.anonRole` (see
30
+ // `~/auth/access.ts`) by stamping the same role explicitly, rather than
31
+ // relying solely on `roles`' `defaultValue` below — which every new user
32
+ // gets regardless of `isAnonymous`, anon-plugin or not.
33
+ user: anonRoleDatabaseHook(USER_ROLES.user),
34
+ },
18
35
  // {{EMAIL_PASSWORD_AUTH}}
19
36
  // {{OAUTH_PROVIDERS}}
20
37
  plugins: createPlugins(),
21
38
  secret: process.env.BETTER_AUTH_SECRET,
22
- session: {
23
- modelName: TABLE_SLUG_SESSIONS,
24
- },
25
39
  trustedOrigins: [process.env.SITE_URL!],
26
- user: {
27
- additionalFields: {
28
- roles: {
29
- type: "string[]",
30
- defaultValue: [USER_ROLES.user],
31
- required: true,
32
- },
33
- },
34
- modelName: TABLE_SLUG_USERS,
35
- },
36
- verification: {
37
- modelName: TABLE_SLUG_VERIFICATIONS,
38
- },
39
40
  }
@@ -0,0 +1,51 @@
1
+ import type { BetterAuthSchemaInput } from "@vexcms/better-auth/client"
2
+
3
+ import {
4
+ TABLE_SLUG_ACCOUNTS,
5
+ TABLE_SLUG_SESSIONS,
6
+ TABLE_SLUG_USERS,
7
+ TABLE_SLUG_VERIFICATIONS,
8
+ USER_ROLES,
9
+ } from "~/db/constants"
10
+
11
+ /**
12
+ * Everything about your Better Auth setup that determines which collections
13
+ * exist and which fields they carry — and nothing else.
14
+ *
15
+ * This is the one auth module the browser reads: `src/vex.config.ts` passes it
16
+ * to `betterAuthCollections()` to build the admin panel's auth collections,
17
+ * and `./options` spreads it so the same model names and fields reach the
18
+ * database. Add a `user.additionalFields` entry here and it shows up in both.
19
+ *
20
+ * Keep it inert. `./options` reads `process.env` and `./plugins` pulls
21
+ * `better-auth/next-js` (→ `next/headers`), which is why they are separate
22
+ * files — importing either from the client config would drag the server into
23
+ * your browser bundle. Runtime plugin options (admin roles, mailers, OAuth
24
+ * secrets) belong on the real plugin instances in `./plugins`; only options
25
+ * that change which TABLES exist belong here.
26
+ *
27
+ * @see betterAuthCollections in @vexcms/better-auth/client
28
+ * @see ./options for the server-side options that spread this
29
+ */
30
+ export const authSchema = {
31
+ account: { modelName: TABLE_SLUG_ACCOUNTS },
32
+ session: { modelName: TABLE_SLUG_SESSIONS },
33
+ user: {
34
+ modelName: TABLE_SLUG_USERS,
35
+ additionalFields: {
36
+ roles: {
37
+ type: "string[]",
38
+ defaultValue: [USER_ROLES.user],
39
+ required: true,
40
+ },
41
+ },
42
+ },
43
+ verification: { modelName: TABLE_SLUG_VERIFICATIONS },
44
+ plugins: {
45
+ admin: true,
46
+ anonymous: true,
47
+ apiKey: true,
48
+ convex: true,
49
+ // {{ORGANIZATIONS_SCHEMA}}
50
+ },
51
+ } satisfies BetterAuthSchemaInput
@@ -19,7 +19,7 @@ export const getSessionWithUser = query({
19
19
  .withIndex("by_token", (q) => q.eq("token", args.sessionToken))
20
20
  .first()
21
21
 
22
- if (!session) return null
22
+ if (!session) {return null}
23
23
 
24
24
  if (session.expiresAt < Date.now()) {
25
25
  console.error("[getSessionWithUser] Session Expired")
@@ -27,10 +27,10 @@ export const getSessionWithUser = query({
27
27
  }
28
28
 
29
29
  const userId = session.userId
30
- if (!userId) return null
30
+ if (!userId) {return null}
31
31
 
32
32
  const user = await ctx.db.get(userId as Id<typeof TABLE_SLUG_USERS>)
33
- if (!user) return null
33
+ if (!user) {return null}
34
34
 
35
35
  return {
36
36
  session: {
@@ -8,16 +8,16 @@
8
8
  "allowSyntheticDefaultImports": true,
9
9
  "target": "ESNext",
10
10
  "lib": ["ES2023", "dom", "dom.iterable"],
11
+ "types": ["node"],
11
12
  "forceConsistentCasingInFileNames": true,
12
13
  "module": "ESNext",
13
14
  "isolatedModules": true,
14
15
  "noEmit": true,
15
- "baseUrl": ".",
16
16
  "paths": {
17
17
  "~/*": ["../src/*"],
18
18
  "@convex/*": ["./*"]
19
19
  }
20
20
  },
21
- "include": ["./**/*"],
21
+ "include": ["./**/*", "../src/vex.types.ts"],
22
22
  "exclude": ["./_generated"]
23
23
  }
@@ -1,10 +1,10 @@
1
1
  import { ConvexError } from "convex/values"
2
2
 
3
- import { mutation, query } from "../_generated/server"
4
-
5
3
  import { TABLE_SLUG_USERS } from "~/db/constants"
6
4
  import { USER_ROLES } from "~/db/constants/auth"
7
5
 
6
+ import { mutation, query } from "../_generated/server"
7
+
8
8
  /**
9
9
  * Check whether the admin panel has been bootstrapped (at least one admin
10
10
  * exists). Used by `WelcomePage` to decide between "Sign Up" and "Sign In".
@@ -2,9 +2,10 @@ import { createGetAuth } from "@vexcms/better-auth";
2
2
  import { globalsApi } from "@vexcms/core/server";
3
3
 
4
4
  import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants";
5
- import config from "~/vex.config";
5
+ import config from "~/vex.config.server";
6
6
 
7
- import { mutation, query } from "../_generated/server";
7
+ import { query } from "../_generated/server";
8
+ import { vexMutation as mutation } from "../vex";
8
9
 
9
10
  export const { get, find, upsert } = globalsApi({
10
11
  config,
@@ -2,9 +2,10 @@ import { createGetAuth } from "@vexcms/better-auth";
2
2
  import { mediaApi } from "@vexcms/core";
3
3
 
4
4
  import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants";
5
- import config from "~/vex.config";
5
+ import config from "~/vex.config.server";
6
6
 
7
- import { mutation, query } from "../_generated/server";
7
+ import { query } from "../_generated/server";
8
+ import { vexMutation as mutation } from "../vex";
8
9
 
9
10
  export const { getUrl, generateUploadUrl, createMediaDocument, deleteMedia } = mediaApi({
10
11
  config,
@@ -130,6 +130,7 @@ export const images = defineTable({
130
130
  src: v.string(),
131
131
  width: v.optional(v.number()),
132
132
  height: v.optional(v.number()),
133
+ updatedAt: v.optional(v.number()),
133
134
  })
134
135
  .index("by_deleted", ["deleted"])
135
136
  .searchIndex("search_filename", {
@@ -1,15 +1,28 @@
1
1
  import { createGetAuth } from "@vexcms/better-auth";
2
- import { collectionsApi } from "@vexcms/core/server";
2
+ import { collectionsApi, createVexMutations } from "@vexcms/core/server";
3
3
 
4
4
  import { TABLE_SLUG_ORGANIZATIONS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS } from "~/db/constants";
5
- import config from "~/vex.config";
5
+ import config from "~/vex.config.server";
6
6
 
7
- import { mutation, query } from "./_generated/server";
7
+ import { internalMutation, mutation, query } from "./_generated/server";
8
+
9
+ /**
10
+ * Trigger-wrapped `mutation`/`internalMutation` builders — every write made
11
+ * through them fires the written collection's `afterChange`/`afterDelete`
12
+ * hooks (`@vexcms/core`'s `createVexMutations`). Used, not exported: this
13
+ * file's own `create`/`update`/`remove` (below) and `globals.ts`/`media.ts`
14
+ * build their mutations on `vexMutation`, never the raw builder above.
15
+ */
16
+ export const { mutation: vexMutation, internalMutation: vexInternalMutation } = createVexMutations({
17
+ config,
18
+ mutation,
19
+ internalMutation,
20
+ });
8
21
 
9
22
  export const { find, get, search, create, update, remove } = collectionsApi({
10
23
  config,
11
24
  query,
12
- mutation,
25
+ mutation: vexMutation,
13
26
  getAuth: createGetAuth({
14
27
  orgCollectionSlug: TABLE_SLUG_ORGANIZATIONS,
15
28
  userCollectionSlug: TABLE_SLUG_USERS,
@@ -1,6 +1,38 @@
1
1
  import type { NextConfig } from "next"
2
2
 
3
- import "./src/env.mjs"
3
+ import { existsSync } from "node:fs"
4
+ import { dirname, join, resolve } from "node:path"
5
+
6
+ import { env } from "./src/env.mjs"
7
+
8
+ /**
9
+ * Walks up from the app directory looking for `pnpm-workspace.yaml`.
10
+ *
11
+ * @param from - Directory to start from.
12
+ * @returns The workspace root when this app is a workspace member, otherwise
13
+ * `from` unchanged.
14
+ */
15
+ function findWorkspaceRoot(from: string): string {
16
+ let dir = resolve(from)
17
+ for (;;) {
18
+ if (existsSync(join(dir, "pnpm-workspace.yaml"))) {return dir}
19
+ const parent = dirname(dir)
20
+ if (parent === dir) {return resolve(from)}
21
+ dir = parent
22
+ }
23
+ }
24
+
25
+ // Convex file storage URLs (`next/image` sources for uploaded media) are
26
+ // served from `<deployment>.convex.cloud` — derive the hostname from the
27
+ // validated NEXT_PUBLIC_CONVEX_URL so scaffolds work without a manual
28
+ // next.config.ts edit. Falls back to an empty list when the URL is unset or
29
+ // unparsable (e.g. a deployment-less build with SKIP_ENV_VALIDATION set).
30
+ let convexImageRemotePatterns: NonNullable<NextConfig["images"]>["remotePatterns"] = []
31
+ try {
32
+ convexImageRemotePatterns = [{ hostname: new URL(env.NEXT_PUBLIC_CONVEX_URL).hostname }]
33
+ } catch {
34
+ // No deployment yet — leave remotePatterns empty.
35
+ }
4
36
 
5
37
  const nextConfig: NextConfig = {
6
38
  devIndicators: {
@@ -8,12 +40,26 @@ const nextConfig: NextConfig = {
8
40
  },
9
41
  allowedDevOrigins: ["127.0.01", "localhost"],
10
42
  reactCompiler: true,
43
+ // Turbopack's root sets the file-tracing root AND Tailwind's source-scan
44
+ // root, so it has to be the directory that actually owns `node_modules`.
45
+ //
46
+ // Standalone scaffold: that is this app. Pinning it stops Next walking up
47
+ // and adopting an unrelated outer lockfile.
48
+ //
49
+ // pnpm workspace member: it is the workspace root — dependencies are
50
+ // hoisted into the root virtual store and `next` is only reachable from
51
+ // there. Pinning to the app dir makes the build fail with "Could not find
52
+ // the Next.js package".
53
+ //
54
+ // Detecting `pnpm-workspace.yaml` picks the right answer in both cases
55
+ // without the template needing a monorepo-specific variant.
56
+ turbopack: {
57
+ root: findWorkspaceRoot(import.meta.dirname),
58
+ },
11
59
  images: {
12
- remotePatterns: [
13
- // Add your Convex deployment's hostname here once `npx convex dev` has
14
- // run — Convex file storage URLs are served from `<deployment>.convex.cloud`,
15
- // e.g. { hostname: "your-deployment-575.convex.cloud" }.
16
- ],
60
+ // Additional remote image hosts (e.g. an external CMS or CDN) can be
61
+ // added here alongside the auto-derived Convex hostname above.
62
+ remotePatterns: convexImageRemotePatterns,
17
63
  },
18
64
  }
19
65
 
@@ -24,18 +24,17 @@
24
24
  "@t3-oss/env-nextjs": "0.13.11",
25
25
  "@tanstack/react-form": "1.33.1",
26
26
  "@tanstack/react-query": "5.101.2",
27
- "@vexcms/better-auth": "~0.1.0-alpha.2",
28
- "@vexcms/core": "~0.1.0-alpha.2",
29
- "@vexcms/file-storage-convex": "~0.1.0-alpha.2",
30
- "@vexcms/next": "~0.1.0-alpha.2",
31
- "@vexcms/react": "~0.1.0-alpha.2",
27
+ "@vexcms/better-auth": "~0.1.0-alpha.21",
28
+ "@vexcms/core": "~0.1.0-alpha.21",
29
+ "@vexcms/file-storage-convex": "~0.1.0-alpha.21",
30
+ "@vexcms/next": "~0.1.0-alpha.21",
31
+ "@vexcms/react": "~0.1.0-alpha.21",
32
32
  "better-auth": "1.6.23",
33
33
  "class-variance-authority": "0.7.1",
34
34
  "clsx": "2.1.1",
35
35
  "convex": "1.44.0",
36
36
  "convex-helpers": "0.1.120",
37
37
  "lucide-react": "0.577.0",
38
- "motion": "13.1.1",
39
38
  "next": "16.3.3",
40
39
  "nuqs": "2.9.0",
41
40
  "react": "19.2.7",
@@ -53,7 +52,7 @@
53
52
  "@types/node": "20.19.43",
54
53
  "@types/react": "19.2.17",
55
54
  "@types/react-dom": "19.2.3",
56
- "@vexcms/cli": "~0.1.0-alpha.2",
55
+ "@vexcms/cli": "~0.1.0-alpha.21",
57
56
  "babel-plugin-react-compiler": "1.0.0",
58
57
  "eslint": "9.39.5",
59
58
  "eslint-config-next": "15.5.9",
@@ -64,5 +63,17 @@
64
63
  "tailwindcss": "4.3.2",
65
64
  "typescript": "6.0.3",
66
65
  "typescript-eslint": "8.63.0"
66
+ },
67
+ "pnpm": {
68
+ "overrides": {
69
+ "@better-auth/passkey": "1.6.23"
70
+ },
71
+ "peerDependencyRules": {
72
+ "allowedVersions": {
73
+ "@convex-dev/better-auth>better-auth": "1.6.23",
74
+ "better-call": "2",
75
+ "typescript": "6"
76
+ }
77
+ }
67
78
  }
68
79
  }
@@ -1,12 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import { AuthView } from "@daveyplate/better-auth-ui";
4
- import { useSearchParams } from "next/navigation";
4
+ import { useRouter, useSearchParams } from "next/navigation";
5
5
  import { Activity } from "react";
6
6
 
7
7
  import { Dialog, DialogContent, DialogTitle } from "~/components/ui/dialog";
8
8
 
9
9
  export default function AuthCard({ pathname }: { pathname: string }) {
10
+ const router = useRouter();
10
11
  const searchParams = useSearchParams();
11
12
  const isGuardRedirect = searchParams.has("redirectTo");
12
13
  if (isGuardRedirect) {
@@ -16,20 +17,31 @@ export default function AuthCard({ pathname }: { pathname: string }) {
16
17
  </main>
17
18
  );
18
19
  }
20
+ // No in-flow wrapper here on purpose. The Dialog portals into <body>, so a
21
+ // wrapping <main> would be an empty element sitting after the footer (the
22
+ // @auth slot renders last in the frontend layout). Next's post-navigation
23
+ // scroll handler targets the new segment's first DOM node and would scroll
24
+ // that empty element into view — i.e. jump the page to the bottom behind
25
+ // the modal. With only a portal rendered there is no node to target.
19
26
  return (
20
- <main>
21
- <Dialog open>
22
- <Activity mode="hidden">
23
- <DialogTitle />
24
- </Activity>
25
- <DialogContent
26
- aria-describedby={undefined}
27
- className="grid place-items-center bg-transparent shadow-none ring-transparent"
28
- showCloseButton={false}
29
- >
30
- <AuthView path={pathname} />
31
- </DialogContent>
32
- </Dialog>
33
- </main>
27
+ <Dialog
28
+ onOpenChange={(open) => {
29
+ if (!open) {
30
+ router.back();
31
+ }
32
+ }}
33
+ open
34
+ >
35
+ <Activity mode="hidden">
36
+ <DialogTitle />
37
+ </Activity>
38
+ <DialogContent
39
+ aria-describedby={undefined}
40
+ className="grid place-items-center bg-transparent shadow-none ring-transparent"
41
+ showCloseButton={false}
42
+ >
43
+ <AuthView path={pathname} />
44
+ </DialogContent>
45
+ </Dialog>
34
46
  );
35
47
  }
@@ -1,10 +1,10 @@
1
1
  import { api } from "@convex/_generated/api";
2
- import { NextAdminPage } from "@vexcms/next/server";
3
2
  import { canAccessAdminPanel } from "@vexcms/core";
3
+ import { NextAdminPage } from "@vexcms/next/server";
4
4
  import { redirect } from "next/navigation";
5
5
 
6
6
  import { fetchAuthQuery, getToken } from "~/auth/server";
7
- import config from "~/vex.config";
7
+ import config from "~/vex.config.server";
8
8
 
9
9
  export const dynamic = "force-dynamic";
10
10
 
@@ -1,22 +1,17 @@
1
- "use client";
1
+ "use client"
2
2
 
3
- import { uploadFile } from "@vexcms/file-storage-convex";
4
- import { StorageAdapterContextProvider, VexAccessProvider } from "@vexcms/react";
3
+ import { VexConfigProvider } from "@vexcms/react"
5
4
 
6
- import { access } from "~/auth/access";
5
+ import config from "~/vex.config"
7
6
 
8
7
  /**
9
- * Provides client-side upload functions for VexCMS storage adapters.
8
+ * Mounts the single `VexConfigProvider` for the admin panel's client tree.
10
9
  *
11
- * This is a "use client" component so that uploadFile is imported and
12
- * owned on the client — it never crosses the RSC boundary as a prop.
13
- *
14
- * Add a new entry here when registering an additional storage adapter.
10
+ * This is a "use client" component so `vex.config` is imported and owned on
11
+ * the client — it never crosses the RSC boundary as a prop. `access` and
12
+ * `storage.clientUploads` (the two fields that used to need their own
13
+ * providers) are already part of the config this imports.
15
14
  */
16
15
  export function ClientProviders({ children }: { children: React.ReactNode }) {
17
- return (
18
- <StorageAdapterContextProvider adapterClients={{ convex: uploadFile }}>
19
- <VexAccessProvider access={access}>{children}</VexAccessProvider>
20
- </StorageAdapterContextProvider>
21
- );
16
+ return <VexConfigProvider config={config}>{children}</VexConfigProvider>
22
17
  }
@@ -2,18 +2,20 @@ import type { ReactNode } from "react"
2
2
 
3
3
  import { NextAdminLayout } from "@vexcms/next/client"
4
4
 
5
+ import { AuthServerProvider } from "~/components/providers/auth"
5
6
  import { getCurrentUser } from "~/auth/serverUtils"
6
- import config from "~/vex.config"
7
7
 
8
8
  import { ClientProviders } from "./clientProviders"
9
9
 
10
10
  export default async function AdminLayout({ children }: { children: ReactNode }) {
11
11
  const user = await getCurrentUser()
12
12
  return (
13
- <ClientProviders>
14
- <NextAdminLayout config={config} user={user ?? undefined}>
15
- {children}
16
- </NextAdminLayout>
17
- </ClientProviders>
13
+ <AuthServerProvider>
14
+ <ClientProviders>
15
+ <NextAdminLayout user={user ?? undefined}>
16
+ {children}
17
+ </NextAdminLayout>
18
+ </ClientProviders>
19
+ </AuthServerProvider>
18
20
  )
19
21
  }
@@ -0,0 +1,19 @@
1
+ import { api } from "@convex/_generated/api"
2
+ import { createVexRevalidateRoute } from "@vexcms/next/cache"
3
+
4
+ import { fetchAuthQuery, getToken } from "~/auth/server"
5
+ import config from "~/vex.config.server"
6
+
7
+ /**
8
+ * `POST /api/vex/revalidate` — session-authorized path purge.
9
+ *
10
+ * Called by `useVexMutation` after every admin-panel write, same origin, and
11
+ * by the admin panel's Revalidate control over the same session. Never
12
+ * secret-authorized: the caller must hold a real session with write
13
+ * permission on the affected collection.
14
+ */
15
+ export const { POST } = createVexRevalidateRoute({
16
+ config,
17
+ getToken,
18
+ getAuth: () => fetchAuthQuery(api.auth.api.getUserOrg, {}),
19
+ })
@@ -25,15 +25,16 @@
25
25
  @import "shadcn/tailwind.css";
26
26
  @import "@vexcms/next/styles";
27
27
 
28
- /* Workspace source scan (belt-and-suspenders — normal dev relies on tsup
29
- watchers rebuilding each package dist/, which Tailwind already scans via
30
- @vexcms/next/styles). These two lines also point Tailwind at the raw
31
- .tsx so classes appear immediately even if a tsup watcher dies. They
32
- are workspace-relative and resolve only inside this monorepo. */
33
- @source "../../../../packages/react/src/**/*.ts";
34
- @source "../../../../packages/react/src/**/*.tsx";
35
- @source "../../../../packages/next/src/**/*.ts";
36
- @source "../../../../packages/next/src/**/*.tsx";
28
+ /* Scan the installed @vexcms packages for class candidates (their dist output
29
+ ships alongside this app in node_modules; @vexcms/next/styles above pulls
30
+ the stylesheet, these make sure every utility used by the admin UI exists).
31
+ Globbed to `*.js` only — the same `dist` directories also carry `.map`
32
+ sourcemaps and `.d.ts` declaration files, and Tailwind's scanner extracts
33
+ garbage "class candidates" from their base64/binary-ish content and emits
34
+ unparseable CSS, exactly like the raw `public/` binary assets below. */
35
+ @source "../../node_modules/@vexcms/react/dist/**/*.js";
36
+ @source "../../node_modules/@vexcms/next/dist/**/*.js";
37
+ @source not "../../public";
37
38
 
38
39
  @custom-variant dark (&:is(.dark *));
39
40
 
@@ -2,6 +2,7 @@ import type { Metadata } from "next"
2
2
 
3
3
  import "./globals.css"
4
4
 
5
+ import { ThemeScript } from "@vexcms/react"
5
6
  import { Geist, Geist_Mono } from "next/font/google"
6
7
 
7
8
  import ClientProviders from "~/components/providers/client"
@@ -30,6 +31,11 @@ export default function RootLayout({
30
31
  }>) {
31
32
  return (
32
33
  <html className={`${geistSans.variable} ${geistMono.variable} antialiased`} lang="en" suppressHydrationWarning>
34
+ <head>
35
+ {/* Applies the persisted light/dark class before first paint —
36
+ without it the admin flashes light before the saved mode lands. */}
37
+ <ThemeScript />
38
+ </head>
33
39
  <body>
34
40
  <ServerProviders>
35
41
  <ClientProviders>{children}</ClientProviders>