create-z3 0.0.47 → 0.0.48

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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Isaiah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/dist/index.js CHANGED
@@ -37,7 +37,7 @@ async function isDirectoryEmpty(dirPath) {
37
37
  return true;
38
38
  }
39
39
  }
40
- function resolveProjectName(input2, cwd) {
40
+ function resolveProjectName(input2, _cwd) {
41
41
  return input2;
42
42
  }
43
43
 
@@ -1798,9 +1798,11 @@ function generateCredentialsValue(enabled) {
1798
1798
  }
1799
1799
  function generateAuthProvidersBlock(oauthProviders, emailPasswordEnabled) {
1800
1800
  const parts = [];
1801
- parts.push(`emailAndPassword: {
1802
- enabled: ${emailPasswordEnabled}
1801
+ if (emailPasswordEnabled) {
1802
+ parts.push(`emailAndPassword: {
1803
+ enabled: true
1803
1804
  },`);
1805
+ }
1804
1806
  if (oauthProviders.length > 0) {
1805
1807
  const providersObject = oauthProviders.map((providerId) => {
1806
1808
  const provider = getProvider(providerId);
@@ -2300,13 +2302,12 @@ var TanStackInstaller = class extends FrameworkInstaller {
2300
2302
  async updateEnvExample(selectedProviders) {
2301
2303
  const envFilePath = join2(this.targetPath, ".env.example");
2302
2304
  const envVarsBlock = generateEnvVarsBlock(selectedProviders, "tanstack");
2303
- if (envVarsBlock) {
2304
- await replacePlaceholder(
2305
- envFilePath,
2306
- "# {{ENV_OAUTH_VARS}}",
2307
- envVarsBlock
2308
- );
2309
- }
2305
+ await replacePlaceholder(
2306
+ envFilePath,
2307
+ "# {{ENV_OAUTH_VARS}}",
2308
+ envVarsBlock,
2309
+ { graceful: true }
2310
+ );
2310
2311
  }
2311
2312
  /**
2312
2313
  * Update README with OAuth provider setup guides
@@ -2319,14 +2320,12 @@ var TanStackInstaller = class extends FrameworkInstaller {
2319
2320
  async updateReadme(selectedProviders) {
2320
2321
  const readmeFilePath = join2(this.targetPath, "README.md");
2321
2322
  const readmeSection = generateReadmeSection(selectedProviders);
2322
- if (readmeSection) {
2323
- await replacePlaceholder(
2324
- readmeFilePath,
2325
- "<!-- {{OAUTH_SETUP_GUIDE}} -->",
2326
- readmeSection,
2327
- { graceful: true }
2328
- );
2329
- }
2323
+ await replacePlaceholder(
2324
+ readmeFilePath,
2325
+ "<!-- {{OAUTH_SETUP_GUIDE}} -->",
2326
+ readmeSection,
2327
+ { graceful: true }
2328
+ );
2330
2329
  }
2331
2330
  /**
2332
2331
  * Apply TweakCN theme to global CSS file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-z3",
3
- "version": "0.0.47",
3
+ "version": "0.0.48",
4
4
  "type": "module",
5
5
  "description": "CLI for scaffolding Z3 Stack applications (TanStack/Next.js + Convex + Better Auth)",
6
6
  "bin": {
@@ -10,13 +10,6 @@
10
10
  "dist",
11
11
  "templates"
12
12
  ],
13
- "scripts": {
14
- "build": "tsup src/index.ts --format esm --dts",
15
- "dev": "tsup src/index.ts --format esm --watch",
16
- "typecheck": "tsc --noEmit",
17
- "test": "vitest run",
18
- "test:watch": "vitest"
19
- },
20
13
  "dependencies": {
21
14
  "commander": "^12.0.0",
22
15
  "@inquirer/prompts": "^7.2.0",
@@ -55,7 +48,14 @@
55
48
  ],
56
49
  "repository": {
57
50
  "type": "git",
58
- "url": "https://github.com/zayecq/create-z3-app"
51
+ "url": "https://github.com/ianyimi/create-z3-app"
59
52
  },
60
- "license": "MIT"
61
- }
53
+ "license": "MIT",
54
+ "scripts": {
55
+ "build": "tsup src/index.ts --format esm --dts",
56
+ "dev": "tsup src/index.ts --format esm --watch",
57
+ "typecheck": "tsc --noEmit",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest"
60
+ }
61
+ }
@@ -50,6 +50,12 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
50
50
 
51
51
  return {
52
52
  id: "convex",
53
+ // Tell the convex() plugin this context supports mutations. It checks
54
+ // ctx.context.adapter.options?.isRunMutationCtx — if falsy it replaces all
55
+ // adapter writes with silent no-ops. HTTP actions always have runMutation.
56
+ options: {
57
+ isRunMutationCtx: "runMutation" in ctx,
58
+ },
53
59
 
54
60
  create: async ({ data, model, select }): Promise<any> => {
55
61
  return await ctx.runMutation(internal.auth.db.dbCreate, {
@@ -189,34 +195,12 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
189
195
  if (data && fieldAttributes.type === "date") {
190
196
  return new Date(data).getTime();
191
197
  }
192
- // Handle array fields - Better Auth may send single values or arrays
193
- if ((fieldAttributes.type as string)?.endsWith("[]")) {
194
- // If already an array, return as-is
195
- if (Array.isArray(data)) {
196
- return data
197
- }
198
- // If it's a string that looks like JSON array, parse it
199
- if (typeof data === "string") {
200
- try {
201
- const parsed = JSON.parse(data)
202
- return Array.isArray(parsed) ? parsed : [data]
203
- } catch {
204
- // If parsing fails, wrap the string in an array
205
- return [data]
206
- }
207
- }
208
- // For any other value type, wrap in array
209
- if (data !== null && data !== undefined) {
210
- return [data]
211
- }
212
- }
213
198
  return data;
214
199
  },
215
200
  customTransformOutput: ({ data, fieldAttributes }) => {
216
201
  if (data && fieldAttributes.type === "date") {
217
202
  return new Date(data).getTime();
218
203
  }
219
- // Arrays are stored natively in Convex, no transformation needed for output
220
204
  return data;
221
205
  },
222
206
  debugLogs: config.debugLogs ?? false,
@@ -230,6 +214,7 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
230
214
  supportsDates: false,
231
215
  supportsJSON: true,
232
216
  supportsNumericIds: false,
217
+ supportsArrays: true,
233
218
  transaction: false,
234
219
  usePlural: false,
235
220
  },
@@ -39,7 +39,7 @@ export const createAuth = (
39
39
  trustedOrigins: [process.env.SITE_URL!],
40
40
  user: {
41
41
  additionalFields: {
42
- role: {
42
+ roles: {
43
43
  type: "string[]",
44
44
  defaultValue: [USER_ROLES.user],
45
45
  required: true
@@ -1,14 +1,18 @@
1
1
  import { nextCookies } from "better-auth/next-js"
2
- import { admin, apiKey } from "better-auth/plugins"
2
+ import { admin } from "better-auth/plugins"
3
+ import { apiKey } from "@better-auth/api-key"
4
+ import { convex } from "@convex-dev/better-auth/plugins"
3
5
  import { USER_ROLES } from "~/db/constants"
6
+ import authConfig from "@convex/auth.config"
4
7
 
5
8
  const plugins = [
6
9
  admin({
7
10
  adminRoles: [USER_ROLES.admin],
8
- defaultRole: USER_ROLES.user
11
+ defaultRole: USER_ROLES.user,
9
12
  }),
10
13
  apiKey(),
11
14
  nextCookies(),
15
+ convex({ authConfig }),
12
16
  ]
13
17
 
14
18
  export default plugins
@@ -53,7 +53,7 @@ export const getSessionWithUser = query({
53
53
  email: user.email,
54
54
  emailVerified: user.emailVerified,
55
55
  image: user.image,
56
- role: user.role,
56
+ roles: user.roles,
57
57
  },
58
58
  };
59
59
  },
@@ -0,0 +1,7 @@
1
+ import type { AuthConfig } from "convex/server"
2
+
3
+ import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config"
4
+
5
+ export default {
6
+ providers: [getAuthConfigProvider()],
7
+ } satisfies AuthConfig
@@ -1,7 +1,7 @@
1
1
  import { defineSchema, defineTable } from "convex/server";
2
2
  import { v } from "convex/values"
3
3
 
4
- import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_JWKS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS, TABLE_SLUG_VERIFICATIONS } from "~/db/constants";
4
+ import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_API_KEYS, TABLE_SLUG_JWKS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS, TABLE_SLUG_VERIFICATIONS } from "~/db/constants";
5
5
 
6
6
  export default defineSchema({
7
7
  // Better Auth component tables (type definitions only - actual tables are in component)
@@ -19,7 +19,8 @@ export default defineSchema({
19
19
  isAnonymous: v.optional(v.union(v.null(), v.boolean())),
20
20
  phoneNumber: v.optional(v.union(v.null(), v.string())),
21
21
  phoneNumberVerified: v.optional(v.union(v.null(), v.boolean())),
22
- role: v.array(v.string()), // admin plugin
22
+ role: v.optional(v.string()), // admin plugin — single string in BA 1.5
23
+ roles: v.array(v.string()), // our multi-role field via additionalFields
23
24
  twoFactorEnabled: v.optional(v.union(v.null(), v.boolean())),
24
25
  updatedAt: v.number(),
25
26
  userId: v.optional(v.union(v.null(), v.string()))
@@ -70,4 +71,31 @@ export default defineSchema({
70
71
  privateKey: v.optional(v.string()),
71
72
  publicKey: v.string(),
72
73
  }),
74
+
75
+ // Better Auth 1.5 — apiKey plugin (@better-auth/api-key)
76
+ [TABLE_SLUG_API_KEYS]: defineTable({
77
+ configId: v.string(),
78
+ name: v.optional(v.string()),
79
+ start: v.optional(v.string()),
80
+ referenceId: v.string(),
81
+ prefix: v.optional(v.string()),
82
+ key: v.string(),
83
+ refillInterval: v.optional(v.number()),
84
+ refillAmount: v.optional(v.number()),
85
+ lastRefillAt: v.optional(v.number()),
86
+ enabled: v.optional(v.boolean()),
87
+ rateLimitEnabled: v.optional(v.boolean()),
88
+ rateLimitTimeWindow: v.optional(v.number()),
89
+ rateLimitMax: v.optional(v.number()),
90
+ requestCount: v.optional(v.number()),
91
+ remaining: v.optional(v.number()),
92
+ lastRequest: v.optional(v.number()),
93
+ expiresAt: v.optional(v.number()),
94
+ createdAt: v.number(),
95
+ updatedAt: v.number(),
96
+ permissions: v.optional(v.string()),
97
+ metadata: v.optional(v.string()),
98
+ })
99
+ .index("by_referenceId", ["referenceId"])
100
+ .index("by_key", ["key"]),
73
101
  })
@@ -13,13 +13,14 @@
13
13
  },
14
14
  "dependencies": {
15
15
  "@base-ui/react": "^1.1.0",
16
- "@convex-dev/better-auth": "^0.10.10",
16
+ "@better-auth/api-key": "^1.0.0",
17
+ "@convex-dev/better-auth": "^0.11.0",
17
18
  "@convex-dev/react-query": "^0.1.0",
18
19
  "@daveyplate/better-auth-ui": "^3.3.15",
19
20
  "@t3-oss/env-nextjs": "^0.13.10",
20
21
  "@tanstack/react-form": "^1.27.7",
21
22
  "@tanstack/react-query": "^5.90.17",
22
- "better-auth": "^1.4.9",
23
+ "better-auth": "^1.5.0",
23
24
  "class-variance-authority": "^0.7.1",
24
25
  "clsx": "^2.1.1",
25
26
  "convex": "^1.31.5",
@@ -2,10 +2,8 @@ import type { ReactNode } from "react";
2
2
 
3
3
  import { convexClient } from "@convex-dev/better-auth/client/plugins";
4
4
  import { AuthUIProvider } from "@daveyplate/better-auth-ui";
5
- import {
6
- adminClient,
7
- apiKeyClient,
8
- } from "better-auth/client/plugins";
5
+ import { apiKeyClient } from "@better-auth/api-key/client";
6
+ import { adminClient } from "better-auth/client/plugins";
9
7
  import { createAuthClient } from 'better-auth/react'
10
8
  import Link from "next/link";
11
9
  import { useRouter } from "next/navigation";
@@ -80,9 +80,9 @@ export function hasPermission<Resource extends keyof Permissions>({
80
80
  resource: Resource;
81
81
  user?: null | User;
82
82
  }): boolean {
83
- if (!user?.role) { return false; }
83
+ if (!user?.roles) { return false; }
84
84
 
85
- return user.role.some((role) => {
85
+ return user.roles.some((role) => {
86
86
  const permission = (ROLES as RolesWithPermissions)[role as UserRole][resource]?.[action]
87
87
 
88
88
  if (!permission) { return false }
@@ -6,6 +6,7 @@ export const TABLE_SLUG_ACCOUNTS = "account" as const;
6
6
  export const TABLE_SLUG_SESSIONS = "session" as const;
7
7
  export const TABLE_SLUG_VERIFICATIONS = "verification" as const;
8
8
  export const TABLE_SLUG_JWKS = "jwks" as const;
9
+ export const TABLE_SLUG_API_KEYS = "apikey" as const;
9
10
 
10
11
  export const COLLECTION_SLUG_MEDIA = "media" as const;
11
12
 
@@ -46,6 +46,12 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
46
46
 
47
47
  return {
48
48
  id: "convex",
49
+ // Tell the convex() plugin this context supports mutations. It checks
50
+ // ctx.context.adapter.options?.isRunMutationCtx — if falsy it replaces all
51
+ // adapter writes with silent no-ops. HTTP actions always have runMutation.
52
+ options: {
53
+ isRunMutationCtx: "runMutation" in ctx,
54
+ },
49
55
 
50
56
  create: async ({ data, model, select }): Promise<any> => {
51
57
  return await ctx.runMutation(internal.auth.db.dbCreate, {
@@ -185,34 +191,12 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
185
191
  if (data && fieldAttributes.type === "date") {
186
192
  return new Date(data).getTime();
187
193
  }
188
- // Handle array fields - Better Auth may send single values or arrays
189
- if ((fieldAttributes.type as string)?.endsWith("[]")) {
190
- // If already an array, return as-is
191
- if (Array.isArray(data)) {
192
- return data
193
- }
194
- // If it's a string that looks like JSON array, parse it
195
- if (typeof data === "string") {
196
- try {
197
- const parsed = JSON.parse(data)
198
- return Array.isArray(parsed) ? parsed : [data]
199
- } catch {
200
- // If parsing fails, wrap the string in an array
201
- return [data]
202
- }
203
- }
204
- // For any other value type, wrap in array
205
- if (data !== null && data !== undefined) {
206
- return [data]
207
- }
208
- }
209
194
  return data;
210
195
  },
211
196
  customTransformOutput: ({ data, fieldAttributes }) => {
212
197
  if (data && fieldAttributes.type === "date") {
213
198
  return new Date(data).getTime();
214
199
  }
215
- // Arrays are stored natively in Convex, no transformation needed for output
216
200
  return data;
217
201
  },
218
202
  debugLogs: config.debugLogs ?? false,
@@ -226,6 +210,7 @@ export const convexAdapter = <DataModel extends GenericDataModel>(
226
210
  supportsDates: false,
227
211
  supportsJSON: true,
228
212
  supportsNumericIds: false,
213
+ supportsArrays: true,
229
214
  transaction: false,
230
215
  usePlural: false,
231
216
  },
@@ -39,7 +39,7 @@ export const createAuth = (
39
39
  trustedOrigins: [process.env.SITE_URL!],
40
40
  user: {
41
41
  additionalFields: {
42
- role: {
42
+ roles: {
43
43
  type: "string[]",
44
44
  defaultValue: [USER_ROLES.user],
45
45
  required: true
@@ -1,4 +1,7 @@
1
- import { admin, apiKey } from "better-auth/plugins"
1
+ import { admin } from "better-auth/plugins"
2
+ import { apiKey } from "@better-auth/api-key"
3
+ import { convex } from "@convex-dev/better-auth/plugins"
4
+ import authConfig from "@convex/auth.config"
2
5
  import { USER_ROLES } from "~/db/constants"
3
6
 
4
7
  const plugins = [
@@ -7,6 +10,7 @@ const plugins = [
7
10
  defaultRole: USER_ROLES.user
8
11
  }),
9
12
  apiKey(),
13
+ convex({ authConfig }),
10
14
  ]
11
15
 
12
16
  export default plugins
@@ -53,7 +53,7 @@ export const getSessionWithUser = query({
53
53
  email: user.email,
54
54
  emailVerified: user.emailVerified,
55
55
  image: user.image,
56
- role: user.role,
56
+ roles: user.roles,
57
57
  },
58
58
  };
59
59
  },
@@ -0,0 +1,7 @@
1
+ import type { AuthConfig } from "convex/server"
2
+
3
+ import { getAuthConfigProvider } from "@convex-dev/better-auth/auth-config"
4
+
5
+ export default {
6
+ providers: [getAuthConfigProvider()],
7
+ } satisfies AuthConfig
@@ -1,7 +1,14 @@
1
1
  import { defineSchema, defineTable } from "convex/server";
2
2
  import { v } from "convex/values"
3
3
 
4
- import { TABLE_SLUG_ACCOUNTS, TABLE_SLUG_JWKS, TABLE_SLUG_SESSIONS, TABLE_SLUG_USERS, TABLE_SLUG_VERIFICATIONS } from "~/db/constants";
4
+ import {
5
+ TABLE_SLUG_ACCOUNTS,
6
+ TABLE_SLUG_API_KEYS,
7
+ TABLE_SLUG_JWKS,
8
+ TABLE_SLUG_SESSIONS,
9
+ TABLE_SLUG_USERS,
10
+ TABLE_SLUG_VERIFICATIONS,
11
+ } from "~/db/constants";
5
12
 
6
13
  export default defineSchema({
7
14
  // Better Auth component tables (type definitions only - actual tables are in component)
@@ -22,7 +29,8 @@ export default defineSchema({
22
29
  banExpires: v.optional(v.number()), // admin plugin
23
30
  banned: v.optional(v.boolean()), // admin plugin
24
31
  banReason: v.optional(v.string()), // admin plugin
25
- role: v.array(v.string()), // admin plugin
32
+ role: v.optional(v.string()), // admin plugin — single string in BA 1.5
33
+ roles: v.array(v.string()), // our multi-role field via additionalFields
26
34
  })
27
35
  .index("by_email", ["email"]),
28
36
 
@@ -70,4 +78,31 @@ export default defineSchema({
70
78
  privateKey: v.optional(v.string()),
71
79
  publicKey: v.string(),
72
80
  }),
81
+
82
+ // Better Auth 1.5 — apiKey plugin (@better-auth/api-key)
83
+ [TABLE_SLUG_API_KEYS]: defineTable({
84
+ configId: v.string(), // new in 1.5, default "default"
85
+ name: v.optional(v.string()),
86
+ start: v.optional(v.string()),
87
+ referenceId: v.string(), // replaces userId from 1.4
88
+ prefix: v.optional(v.string()),
89
+ key: v.string(),
90
+ refillInterval: v.optional(v.number()),
91
+ refillAmount: v.optional(v.number()),
92
+ lastRefillAt: v.optional(v.number()),
93
+ enabled: v.optional(v.boolean()),
94
+ rateLimitEnabled: v.optional(v.boolean()),
95
+ rateLimitTimeWindow: v.optional(v.number()),
96
+ rateLimitMax: v.optional(v.number()),
97
+ requestCount: v.optional(v.number()),
98
+ remaining: v.optional(v.number()),
99
+ lastRequest: v.optional(v.number()),
100
+ expiresAt: v.optional(v.number()),
101
+ createdAt: v.number(),
102
+ updatedAt: v.number(),
103
+ permissions: v.optional(v.string()),
104
+ metadata: v.optional(v.string()),
105
+ })
106
+ .index("by_referenceId", ["referenceId"])
107
+ .index("by_key", ["key"]),
73
108
  })
@@ -17,7 +17,8 @@
17
17
  },
18
18
  "dependencies": {
19
19
  "@base-ui/react": "^1.0.0",
20
- "@convex-dev/better-auth": "^0.10.9",
20
+ "@better-auth/api-key": "^1.0.0",
21
+ "@convex-dev/better-auth": "^0.11.0",
21
22
  "@convex-dev/react-query": "^0.1.0",
22
23
  "@daveyplate/better-auth-tanstack": "^1.3.6",
23
24
  "@daveyplate/better-auth-ui": "^3.3.10",
@@ -32,7 +33,7 @@
32
33
  "@tanstack/react-router-with-query": "^1.130.17",
33
34
  "@tanstack/react-start": "^1.132.0",
34
35
  "@tanstack/router-plugin": "^1.132.0",
35
- "better-auth": "^1.4.9",
36
+ "better-auth": "^1.5.0",
36
37
  "class-variance-authority": "^0.7.1",
37
38
  "clsx": "^2.1.1",
38
39
  "convex": "^1.31.2",
@@ -1,472 +1,53 @@
1
- import * as React from 'react'
2
- import {
3
- BellIcon,
4
- BluetoothIcon,
5
- CreditCardIcon,
6
- DownloadIcon,
7
- EyeIcon,
8
- FileCodeIcon,
9
- FileIcon,
10
- FileTextIcon,
11
- FolderIcon,
12
- FolderOpenIcon,
13
- FolderSearchIcon,
14
- HelpCircleIcon,
15
- KeyboardIcon,
16
- LanguagesIcon,
17
- LayoutIcon,
18
- LogOutIcon,
19
- MailIcon,
20
- MonitorIcon,
21
- MoonIcon,
22
- MoreHorizontalIcon,
23
- MoreVerticalIcon,
24
- PaletteIcon,
25
- PlusIcon,
26
- SaveIcon,
27
- SettingsIcon,
28
- ShieldIcon,
29
- SunIcon,
30
- UserIcon,
31
- } from 'lucide-react'
1
+ import { Link } from '@tanstack/react-router'
2
+ import { TerminalIcon, UserIcon } from 'lucide-react'
32
3
 
33
- import { Example, ExampleWrapper } from '~/components/example'
34
- import {
35
- AlertDialog,
36
- AlertDialogAction,
37
- AlertDialogCancel,
38
- AlertDialogContent,
39
- AlertDialogDescription,
40
- AlertDialogFooter,
41
- AlertDialogHeader,
42
- AlertDialogMedia,
43
- AlertDialogTitle,
44
- AlertDialogTrigger,
45
- } from '~/components/ui/alert-dialog'
46
- import { Badge } from '~/components/ui/badge'
4
+ import { signOut, useSession } from '~/lib/auth/client'
47
5
  import { Button } from '~/components/ui/button'
48
- import {
49
- Card,
50
- CardAction,
51
- CardContent,
52
- CardDescription,
53
- CardFooter,
54
- CardHeader,
55
- CardTitle,
56
- } from '~/components/ui/card'
57
- import {
58
- Combobox,
59
- ComboboxContent,
60
- ComboboxEmpty,
61
- ComboboxInput,
62
- ComboboxItem,
63
- ComboboxList,
64
- } from '~/components/ui/combobox'
65
- import {
66
- DropdownMenu,
67
- DropdownMenuCheckboxItem,
68
- DropdownMenuContent,
69
- DropdownMenuGroup,
70
- DropdownMenuItem,
71
- DropdownMenuLabel,
72
- DropdownMenuPortal,
73
- DropdownMenuRadioGroup,
74
- DropdownMenuRadioItem,
75
- DropdownMenuSeparator,
76
- DropdownMenuShortcut,
77
- DropdownMenuSub,
78
- DropdownMenuSubContent,
79
- DropdownMenuSubTrigger,
80
- DropdownMenuTrigger,
81
- } from '~/components/ui/dropdown-menu'
82
- import { Field, FieldGroup, FieldLabel } from '~/components/ui/field'
83
- import { Input } from '~/components/ui/input'
84
- import {
85
- Select,
86
- SelectContent,
87
- SelectGroup,
88
- SelectItem,
89
- SelectTrigger,
90
- SelectValue,
91
- } from '~/components/ui/select'
92
- import { Textarea } from '~/components/ui/textarea'
93
- import { ThemeToggle } from '~/components/ui/theme-toggle'
94
6
 
95
7
  export function ComponentExample() {
96
- return (
97
- <ExampleWrapper>
98
- <ThemeToggle className="absolute w-md:top-10 right-10 top-2" />
99
- <CardExample />
100
- <FormExample />
101
- </ExampleWrapper>
102
- )
103
- }
8
+ const { data: session } = useSession()
104
9
 
105
- function CardExample() {
106
10
  return (
107
- <Example title="Card" className="items-center justify-center">
108
- <Card className="relative w-full max-w-sm overflow-hidden pt-0">
109
- <div className="bg-primary absolute inset-0 z-30 aspect-video opacity-50 mix-blend-color" />
110
- <img
111
- src="https://images.unsplash.com/photo-1604076850742-4c7221f3101b?q=80&w=1887&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D"
112
- alt="Photo by mymind on Unsplash"
113
- title="Photo by mymind on Unsplash"
114
- className="relative z-20 aspect-video w-full object-cover brightness-60 grayscale"
115
- />
116
- <CardHeader>
117
- <CardTitle>Observability Plus is replacing Monitoring</CardTitle>
118
- <CardDescription>
119
- Switch to the improved way to explore your data, with natural language. Monitoring will
120
- no longer be available on the Pro plan in November, 2025
121
- </CardDescription>
122
- </CardHeader>
123
- <CardFooter>
124
- <AlertDialog>
125
- <AlertDialogTrigger render={<Button />}>
126
- <PlusIcon data-icon="inline-start" />
127
- Show Dialog
128
- </AlertDialogTrigger>
129
- <AlertDialogContent size="sm">
130
- <AlertDialogHeader>
131
- <AlertDialogMedia>
132
- <BluetoothIcon />
133
- </AlertDialogMedia>
134
- <AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
135
- <AlertDialogDescription>
136
- Do you want to allow the USB accessory to connect to this device?
137
- </AlertDialogDescription>
138
- </AlertDialogHeader>
139
- <AlertDialogFooter>
140
- <AlertDialogCancel>Don&apos;t allow</AlertDialogCancel>
141
- <AlertDialogAction>Allow</AlertDialogAction>
142
- </AlertDialogFooter>
143
- </AlertDialogContent>
144
- </AlertDialog>
145
- <Badge variant="secondary" className="ml-auto">
146
- Warning
147
- </Badge>
148
- </CardFooter>
149
- </Card>
150
- </Example>
151
- )
152
- }
153
-
154
- const frameworks = ['Next.js', 'SvelteKit', 'Nuxt.js', 'Remix', 'Astro'] as const
11
+ <div className="bg-background flex min-h-screen flex-col items-center justify-center gap-8 p-6 text-center">
12
+ {session?.user && (
13
+ <div className="absolute top-4 right-4 flex items-center gap-2">
14
+ <div className="bg-muted flex size-8 items-center justify-center rounded-full">
15
+ <UserIcon className="text-muted-foreground size-4" />
16
+ </div>
17
+ <span className="text-sm font-medium">{session.user.name}</span>
18
+ </div>
19
+ )}
155
20
 
156
- const roleItems = [
157
- { label: 'Developer', value: 'developer' },
158
- { label: 'Designer', value: 'designer' },
159
- { label: 'Manager', value: 'manager' },
160
- { label: 'Other', value: 'other' },
161
- ]
21
+ <div className="flex flex-col items-center gap-4">
22
+ <div className="bg-foreground text-background flex size-14 items-center justify-center rounded-2xl">
23
+ <TerminalIcon className="size-7" />
24
+ </div>
25
+ <div className="flex flex-col gap-1">
26
+ <h1 className="text-3xl font-bold tracking-tight">create-z3-app</h1>
27
+ <p className="text-muted-foreground max-w-sm text-base">
28
+ A full-stack starter with TanStack Start, Convex, and Better Auth — ready to ship.
29
+ </p>
30
+ </div>
31
+ </div>
162
32
 
163
- function FormExample() {
164
- const [notifications, setNotifications] = React.useState({
165
- email: true,
166
- sms: false,
167
- push: true,
168
- })
169
- const [theme, setTheme] = React.useState('light')
33
+ <div className="flex gap-3">
34
+ {session?.user ? (
35
+ <Button variant="outline" onClick={() => signOut()}>Sign out</Button>
36
+ ) : (
37
+ <>
38
+ <Button asChild>
39
+ <Link to="/auth/$authView" params={{ authView: 'sign-up' }}>Sign up</Link>
40
+ </Button>
41
+ <Button variant="outline" asChild>
42
+ <Link to="/auth/$authView" params={{ authView: 'sign-in' }}>Sign in</Link>
43
+ </Button>
44
+ </>
45
+ )}
46
+ </div>
170
47
 
171
- return (
172
- <Example title="Form">
173
- <Card className="w-full max-w-md">
174
- <CardHeader>
175
- <CardTitle>User Information</CardTitle>
176
- <CardDescription>Please fill in your details below</CardDescription>
177
- <CardAction>
178
- <DropdownMenu>
179
- <DropdownMenuTrigger render={<Button variant="ghost" size="icon" />}>
180
- <MoreVerticalIcon />
181
- <span className="sr-only">More options</span>
182
- </DropdownMenuTrigger>
183
- <DropdownMenuContent align="end" className="w-56">
184
- <DropdownMenuGroup>
185
- <DropdownMenuLabel>File</DropdownMenuLabel>
186
- <DropdownMenuItem>
187
- <FileIcon />
188
- New File
189
- <DropdownMenuShortcut>⌘N</DropdownMenuShortcut>
190
- </DropdownMenuItem>
191
- <DropdownMenuItem>
192
- <FolderIcon />
193
- New Folder
194
- <DropdownMenuShortcut>⇧⌘N</DropdownMenuShortcut>
195
- </DropdownMenuItem>
196
- <DropdownMenuSub>
197
- <DropdownMenuSubTrigger>
198
- <FolderOpenIcon />
199
- Open Recent
200
- </DropdownMenuSubTrigger>
201
- <DropdownMenuPortal>
202
- <DropdownMenuSubContent>
203
- <DropdownMenuGroup>
204
- <DropdownMenuLabel>Recent Projects</DropdownMenuLabel>
205
- <DropdownMenuItem>
206
- <FileCodeIcon />
207
- Project Alpha
208
- </DropdownMenuItem>
209
- <DropdownMenuItem>
210
- <FileCodeIcon />
211
- Project Beta
212
- </DropdownMenuItem>
213
- <DropdownMenuSub>
214
- <DropdownMenuSubTrigger>
215
- <MoreHorizontalIcon />
216
- More Projects
217
- </DropdownMenuSubTrigger>
218
- <DropdownMenuPortal>
219
- <DropdownMenuSubContent>
220
- <DropdownMenuItem>
221
- <FileCodeIcon />
222
- Project Gamma
223
- </DropdownMenuItem>
224
- <DropdownMenuItem>
225
- <FileCodeIcon />
226
- Project Delta
227
- </DropdownMenuItem>
228
- </DropdownMenuSubContent>
229
- </DropdownMenuPortal>
230
- </DropdownMenuSub>
231
- </DropdownMenuGroup>
232
- <DropdownMenuSeparator />
233
- <DropdownMenuGroup>
234
- <DropdownMenuItem>
235
- <FolderSearchIcon />
236
- Browse...
237
- </DropdownMenuItem>
238
- </DropdownMenuGroup>
239
- </DropdownMenuSubContent>
240
- </DropdownMenuPortal>
241
- </DropdownMenuSub>
242
- <DropdownMenuSeparator />
243
- <DropdownMenuItem>
244
- <SaveIcon />
245
- Save
246
- <DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
247
- </DropdownMenuItem>
248
- <DropdownMenuItem>
249
- <DownloadIcon />
250
- Export
251
- <DropdownMenuShortcut>⇧⌘E</DropdownMenuShortcut>
252
- </DropdownMenuItem>
253
- </DropdownMenuGroup>
254
- <DropdownMenuSeparator />
255
- <DropdownMenuGroup>
256
- <DropdownMenuLabel>View</DropdownMenuLabel>
257
- <DropdownMenuCheckboxItem
258
- checked={notifications.email}
259
- onCheckedChange={(checked) =>
260
- setNotifications({
261
- ...notifications,
262
- email: checked === true,
263
- })
264
- }
265
- >
266
- <EyeIcon />
267
- Show Sidebar
268
- </DropdownMenuCheckboxItem>
269
- <DropdownMenuCheckboxItem
270
- checked={notifications.sms}
271
- onCheckedChange={(checked) =>
272
- setNotifications({
273
- ...notifications,
274
- sms: checked === true,
275
- })
276
- }
277
- >
278
- <LayoutIcon />
279
- Show Status Bar
280
- </DropdownMenuCheckboxItem>
281
- <DropdownMenuSub>
282
- <DropdownMenuSubTrigger>
283
- <PaletteIcon />
284
- Theme
285
- </DropdownMenuSubTrigger>
286
- <DropdownMenuPortal>
287
- <DropdownMenuSubContent>
288
- <DropdownMenuGroup>
289
- <DropdownMenuLabel>Appearance</DropdownMenuLabel>
290
- <DropdownMenuRadioGroup value={theme} onValueChange={setTheme}>
291
- <DropdownMenuRadioItem value="light">
292
- <SunIcon />
293
- Light
294
- </DropdownMenuRadioItem>
295
- <DropdownMenuRadioItem value="dark">
296
- <MoonIcon />
297
- Dark
298
- </DropdownMenuRadioItem>
299
- <DropdownMenuRadioItem value="system">
300
- <MonitorIcon />
301
- System
302
- </DropdownMenuRadioItem>
303
- </DropdownMenuRadioGroup>
304
- </DropdownMenuGroup>
305
- </DropdownMenuSubContent>
306
- </DropdownMenuPortal>
307
- </DropdownMenuSub>
308
- </DropdownMenuGroup>
309
- <DropdownMenuSeparator />
310
- <DropdownMenuGroup>
311
- <DropdownMenuLabel>Account</DropdownMenuLabel>
312
- <DropdownMenuItem>
313
- <UserIcon />
314
- Profile
315
- <DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
316
- </DropdownMenuItem>
317
- <DropdownMenuItem>
318
- <CreditCardIcon />
319
- Billing
320
- </DropdownMenuItem>
321
- <DropdownMenuSub>
322
- <DropdownMenuSubTrigger>
323
- <SettingsIcon />
324
- Settings
325
- </DropdownMenuSubTrigger>
326
- <DropdownMenuPortal>
327
- <DropdownMenuSubContent>
328
- <DropdownMenuGroup>
329
- <DropdownMenuLabel>Preferences</DropdownMenuLabel>
330
- <DropdownMenuItem>
331
- <KeyboardIcon />
332
- Keyboard Shortcuts
333
- </DropdownMenuItem>
334
- <DropdownMenuItem>
335
- <LanguagesIcon />
336
- Language
337
- </DropdownMenuItem>
338
- <DropdownMenuSub>
339
- <DropdownMenuSubTrigger>
340
- <BellIcon />
341
- Notifications
342
- </DropdownMenuSubTrigger>
343
- <DropdownMenuPortal>
344
- <DropdownMenuSubContent>
345
- <DropdownMenuGroup>
346
- <DropdownMenuLabel>Notification Types</DropdownMenuLabel>
347
- <DropdownMenuCheckboxItem
348
- checked={notifications.push}
349
- onCheckedChange={(checked) =>
350
- setNotifications({
351
- ...notifications,
352
- push: checked === true,
353
- })
354
- }
355
- >
356
- <BellIcon />
357
- Push Notifications
358
- </DropdownMenuCheckboxItem>
359
- <DropdownMenuCheckboxItem
360
- checked={notifications.email}
361
- onCheckedChange={(checked) =>
362
- setNotifications({
363
- ...notifications,
364
- email: checked === true,
365
- })
366
- }
367
- >
368
- <MailIcon />
369
- Email Notifications
370
- </DropdownMenuCheckboxItem>
371
- </DropdownMenuGroup>
372
- </DropdownMenuSubContent>
373
- </DropdownMenuPortal>
374
- </DropdownMenuSub>
375
- </DropdownMenuGroup>
376
- <DropdownMenuSeparator />
377
- <DropdownMenuGroup>
378
- <DropdownMenuItem>
379
- <ShieldIcon />
380
- Privacy & Security
381
- </DropdownMenuItem>
382
- </DropdownMenuGroup>
383
- </DropdownMenuSubContent>
384
- </DropdownMenuPortal>
385
- </DropdownMenuSub>
386
- </DropdownMenuGroup>
387
- <DropdownMenuSeparator />
388
- <DropdownMenuGroup>
389
- <DropdownMenuItem>
390
- <HelpCircleIcon />
391
- Help & Support
392
- </DropdownMenuItem>
393
- <DropdownMenuItem>
394
- <FileTextIcon />
395
- Documentation
396
- </DropdownMenuItem>
397
- </DropdownMenuGroup>
398
- <DropdownMenuSeparator />
399
- <DropdownMenuGroup>
400
- <DropdownMenuItem variant="destructive">
401
- <LogOutIcon />
402
- Sign Out
403
- <DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
404
- </DropdownMenuItem>
405
- </DropdownMenuGroup>
406
- </DropdownMenuContent>
407
- </DropdownMenu>
408
- </CardAction>
409
- </CardHeader>
410
- <CardContent>
411
- <form>
412
- <FieldGroup>
413
- <div className="grid grid-cols-2 gap-4">
414
- <Field>
415
- <FieldLabel htmlFor="small-form-name">Name</FieldLabel>
416
- <Input id="small-form-name" placeholder="Enter your name" required />
417
- </Field>
418
- <Field>
419
- <FieldLabel htmlFor="small-form-role">Role</FieldLabel>
420
- <Select items={roleItems} defaultValue={null}>
421
- <SelectTrigger id="small-form-role">
422
- <SelectValue />
423
- </SelectTrigger>
424
- <SelectContent>
425
- <SelectGroup>
426
- {roleItems.map((item) => (
427
- <SelectItem key={item.value} value={item.value}>
428
- {item.label}
429
- </SelectItem>
430
- ))}
431
- </SelectGroup>
432
- </SelectContent>
433
- </Select>
434
- </Field>
435
- </div>
436
- <Field>
437
- <FieldLabel htmlFor="small-form-framework">Framework</FieldLabel>
438
- <Combobox items={frameworks}>
439
- <ComboboxInput
440
- id="small-form-framework"
441
- placeholder="Select a framework"
442
- required
443
- />
444
- <ComboboxContent>
445
- <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
446
- <ComboboxList>
447
- {(item) => (
448
- <ComboboxItem key={item} value={item}>
449
- {item}
450
- </ComboboxItem>
451
- )}
452
- </ComboboxList>
453
- </ComboboxContent>
454
- </Combobox>
455
- </Field>
456
- <Field>
457
- <FieldLabel htmlFor="small-form-comments">Comments</FieldLabel>
458
- <Textarea id="small-form-comments" placeholder="Add any additional comments" />
459
- </Field>
460
- <Field orientation="horizontal">
461
- <Button type="submit">Submit</Button>
462
- <Button variant="outline" type="button">
463
- Cancel
464
- </Button>
465
- </Field>
466
- </FieldGroup>
467
- </form>
468
- </CardContent>
469
- </Card>
470
- </Example>
48
+ <code className="bg-muted text-muted-foreground rounded-lg px-4 py-2 text-sm font-mono">
49
+ npm create z3-app@latest
50
+ </code>
51
+ </div>
471
52
  )
472
53
  }
@@ -6,3 +6,4 @@ export const TABLE_SLUG_ACCOUNTS = "account" as const;
6
6
  export const TABLE_SLUG_SESSIONS = "session" as const;
7
7
  export const TABLE_SLUG_VERIFICATIONS = "verification" as const;
8
8
  export const TABLE_SLUG_JWKS = "jwks" as const;
9
+ export const TABLE_SLUG_API_KEYS = "apikey" as const;
@@ -1,5 +1,6 @@
1
1
  import { createAuthClient } from "better-auth/react"
2
- import { adminClient, apiKeyClient } from "better-auth/client/plugins"
2
+ import { adminClient } from "better-auth/client/plugins"
3
+ import { apiKeyClient } from "@better-auth/api-key/client"
3
4
  import { env } from '~/env';
4
5
 
5
6
  export const authClient = createAuthClient({
@@ -80,9 +80,9 @@ export function hasPermission<Resource extends keyof Permissions>({
80
80
  resource: Resource;
81
81
  user?: null | User;
82
82
  }): boolean {
83
- if (!user?.role) { return false; }
83
+ if (!user?.roles) { return false; }
84
84
 
85
- return user.role.some((role) => {
85
+ return user.roles.some((role) => {
86
86
  const permission = (ROLES as RolesWithPermissions)[role as UserRole][resource]?.[action]
87
87
 
88
88
  if (!permission) { return false }
@@ -8,7 +8,7 @@ export const Route = createFileRoute('/auth/$authView')({
8
8
  function RouteComponent() {
9
9
  const { authView } = Route.useParams()
10
10
  return (
11
- <main className="container mx-auto flex grow flex-col items-center justify-center gap-3 self-center p-4 md:p-6">
11
+ <main className="mx-auto flex h-[100svh] grow flex-col items-center justify-center gap-3 self-center p-4 md:p-6">
12
12
  <AuthView pathname={authView} />
13
13
  </main>
14
14
  )