@getjack/jack 0.1.32 → 0.1.33

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 (193) hide show
  1. package/package.json +1 -1
  2. package/src/commands/deploys.ts +95 -0
  3. package/src/commands/link.ts +8 -0
  4. package/src/commands/mcp.ts +179 -4
  5. package/src/commands/rollback.ts +53 -0
  6. package/src/commands/services.ts +11 -1
  7. package/src/commands/ship.ts +3 -1
  8. package/src/commands/tokens.ts +16 -1
  9. package/src/commands/whoami.ts +43 -8
  10. package/src/index.ts +16 -0
  11. package/src/lib/agent-files.ts +54 -4
  12. package/src/lib/agent-integration.ts +4 -166
  13. package/src/lib/claude-hooks-installer.ts +55 -0
  14. package/src/lib/control-plane.ts +78 -40
  15. package/src/lib/debug.ts +2 -1
  16. package/src/lib/deploy-upload.ts +6 -0
  17. package/src/lib/hooks.ts +3 -1
  18. package/src/lib/managed-deploy.ts +12 -9
  19. package/src/lib/project-link.ts +6 -0
  20. package/src/lib/project-operations.ts +68 -22
  21. package/src/lib/telemetry.ts +2 -0
  22. package/src/mcp/README.md +1 -1
  23. package/src/mcp/resources/index.ts +1 -16
  24. package/src/mcp/server.ts +23 -0
  25. package/src/mcp/tools/index.ts +133 -17
  26. package/src/mcp/types.ts +1 -0
  27. package/src/mcp/utils.ts +2 -1
  28. package/src/templates/index.ts +25 -73
  29. package/templates/CLAUDE.md +41 -0
  30. package/templates/ai-chat/.jack.json +10 -5
  31. package/templates/ai-chat/bun.lock +50 -1
  32. package/templates/ai-chat/package.json +5 -0
  33. package/templates/ai-chat/public/app.js +73 -0
  34. package/templates/ai-chat/public/index.html +14 -197
  35. package/templates/ai-chat/schema.sql +14 -0
  36. package/templates/ai-chat/src/index.ts +86 -102
  37. package/templates/ai-chat/wrangler.jsonc +8 -1
  38. package/templates/cron/.jack.json +66 -0
  39. package/templates/cron/bun.lock +23 -0
  40. package/templates/cron/package.json +16 -0
  41. package/templates/cron/schema.sql +24 -0
  42. package/templates/cron/src/index.ts +117 -0
  43. package/templates/cron/src/jobs.ts +139 -0
  44. package/templates/cron/src/webhooks.ts +95 -0
  45. package/templates/cron/tsconfig.json +17 -0
  46. package/templates/cron/wrangler.jsonc +11 -0
  47. package/templates/miniapp/.jack.json +1 -1
  48. package/templates/nextjs/.jack.json +1 -1
  49. package/templates/nextjs-auth/.jack.json +44 -0
  50. package/templates/nextjs-auth/app/api/auth/[...all]/route.ts +11 -0
  51. package/templates/nextjs-auth/app/dashboard/loading.tsx +53 -0
  52. package/templates/nextjs-auth/app/dashboard/page.tsx +73 -0
  53. package/templates/nextjs-auth/app/error.tsx +44 -0
  54. package/templates/nextjs-auth/app/globals.css +1 -0
  55. package/templates/nextjs-auth/app/health/route.ts +3 -0
  56. package/templates/nextjs-auth/app/layout.tsx +24 -0
  57. package/templates/nextjs-auth/app/login/page.tsx +10 -0
  58. package/templates/nextjs-auth/app/page.tsx +86 -0
  59. package/templates/nextjs-auth/app/signup/page.tsx +10 -0
  60. package/templates/nextjs-auth/bun.lock +1065 -0
  61. package/templates/nextjs-auth/cloudflare-env.d.ts +8 -0
  62. package/templates/nextjs-auth/components/auth-form.tsx +191 -0
  63. package/templates/nextjs-auth/components/header.tsx +50 -0
  64. package/templates/nextjs-auth/components/user-menu.tsx +23 -0
  65. package/templates/nextjs-auth/lib/auth-client.ts +3 -0
  66. package/templates/nextjs-auth/lib/auth.ts +43 -0
  67. package/templates/nextjs-auth/lib/utils.ts +6 -0
  68. package/templates/nextjs-auth/middleware.ts +33 -0
  69. package/templates/nextjs-auth/next.config.ts +8 -0
  70. package/templates/nextjs-auth/open-next.config.ts +6 -0
  71. package/templates/nextjs-auth/package.json +33 -0
  72. package/templates/nextjs-auth/postcss.config.mjs +8 -0
  73. package/templates/nextjs-auth/schema.sql +49 -0
  74. package/templates/nextjs-auth/tsconfig.json +28 -0
  75. package/templates/nextjs-auth/wrangler.jsonc +23 -0
  76. package/templates/nextjs-clerk/.jack.json +54 -0
  77. package/templates/nextjs-clerk/app/dashboard/page.tsx +69 -0
  78. package/templates/nextjs-clerk/app/globals.css +1 -0
  79. package/templates/nextjs-clerk/app/health/route.ts +3 -0
  80. package/templates/nextjs-clerk/app/layout.tsx +26 -0
  81. package/templates/nextjs-clerk/app/page.tsx +86 -0
  82. package/templates/nextjs-clerk/app/sign-in/[[...sign-in]]/page.tsx +9 -0
  83. package/templates/nextjs-clerk/app/sign-up/[[...sign-up]]/page.tsx +9 -0
  84. package/templates/nextjs-clerk/bun.lock +1055 -0
  85. package/templates/nextjs-clerk/cloudflare-env.d.ts +3 -0
  86. package/templates/nextjs-clerk/components/header.tsx +40 -0
  87. package/templates/nextjs-clerk/lib/utils.ts +6 -0
  88. package/templates/nextjs-clerk/middleware.ts +18 -0
  89. package/templates/nextjs-clerk/next.config.ts +8 -0
  90. package/templates/nextjs-clerk/open-next.config.ts +6 -0
  91. package/templates/nextjs-clerk/package.json +31 -0
  92. package/templates/nextjs-clerk/postcss.config.mjs +8 -0
  93. package/templates/nextjs-clerk/tsconfig.json +28 -0
  94. package/templates/nextjs-clerk/wrangler.jsonc +17 -0
  95. package/templates/nextjs-shadcn/.jack.json +34 -0
  96. package/templates/nextjs-shadcn/app/dashboard/data.json +614 -0
  97. package/templates/nextjs-shadcn/app/dashboard/page.tsx +55 -0
  98. package/templates/nextjs-shadcn/app/globals.css +126 -0
  99. package/templates/nextjs-shadcn/app/health/route.ts +3 -0
  100. package/templates/nextjs-shadcn/app/layout.tsx +24 -0
  101. package/templates/nextjs-shadcn/app/login/page.tsx +19 -0
  102. package/templates/nextjs-shadcn/app/page.tsx +180 -0
  103. package/templates/nextjs-shadcn/app/showcase.tsx +1262 -0
  104. package/templates/nextjs-shadcn/bun.lock +1789 -0
  105. package/templates/nextjs-shadcn/cloudflare-env.d.ts +4 -0
  106. package/templates/nextjs-shadcn/components/app-sidebar.tsx +175 -0
  107. package/templates/nextjs-shadcn/components/chart-area-interactive.tsx +291 -0
  108. package/templates/nextjs-shadcn/components/data-table.tsx +807 -0
  109. package/templates/nextjs-shadcn/components/login-form.tsx +95 -0
  110. package/templates/nextjs-shadcn/components/nav-documents.tsx +92 -0
  111. package/templates/nextjs-shadcn/components/nav-main.tsx +73 -0
  112. package/templates/nextjs-shadcn/components/nav-projects.tsx +89 -0
  113. package/templates/nextjs-shadcn/components/nav-secondary.tsx +42 -0
  114. package/templates/nextjs-shadcn/components/nav-user.tsx +114 -0
  115. package/templates/nextjs-shadcn/components/section-cards.tsx +102 -0
  116. package/templates/nextjs-shadcn/components/site-header.tsx +30 -0
  117. package/templates/nextjs-shadcn/components/team-switcher.tsx +91 -0
  118. package/templates/nextjs-shadcn/components/ui/accordion.tsx +66 -0
  119. package/templates/nextjs-shadcn/components/ui/alert-dialog.tsx +196 -0
  120. package/templates/nextjs-shadcn/components/ui/alert.tsx +66 -0
  121. package/templates/nextjs-shadcn/components/ui/aspect-ratio.tsx +11 -0
  122. package/templates/nextjs-shadcn/components/ui/avatar.tsx +109 -0
  123. package/templates/nextjs-shadcn/components/ui/badge.tsx +48 -0
  124. package/templates/nextjs-shadcn/components/ui/breadcrumb.tsx +109 -0
  125. package/templates/nextjs-shadcn/components/ui/button-group.tsx +83 -0
  126. package/templates/nextjs-shadcn/components/ui/button.tsx +64 -0
  127. package/templates/nextjs-shadcn/components/ui/calendar.tsx +220 -0
  128. package/templates/nextjs-shadcn/components/ui/card.tsx +92 -0
  129. package/templates/nextjs-shadcn/components/ui/carousel.tsx +241 -0
  130. package/templates/nextjs-shadcn/components/ui/chart.tsx +357 -0
  131. package/templates/nextjs-shadcn/components/ui/checkbox.tsx +32 -0
  132. package/templates/nextjs-shadcn/components/ui/collapsible.tsx +33 -0
  133. package/templates/nextjs-shadcn/components/ui/combobox.tsx +310 -0
  134. package/templates/nextjs-shadcn/components/ui/command.tsx +184 -0
  135. package/templates/nextjs-shadcn/components/ui/context-menu.tsx +252 -0
  136. package/templates/nextjs-shadcn/components/ui/dialog.tsx +158 -0
  137. package/templates/nextjs-shadcn/components/ui/direction.tsx +22 -0
  138. package/templates/nextjs-shadcn/components/ui/drawer.tsx +135 -0
  139. package/templates/nextjs-shadcn/components/ui/dropdown-menu.tsx +257 -0
  140. package/templates/nextjs-shadcn/components/ui/empty.tsx +104 -0
  141. package/templates/nextjs-shadcn/components/ui/field.tsx +248 -0
  142. package/templates/nextjs-shadcn/components/ui/form.tsx +167 -0
  143. package/templates/nextjs-shadcn/components/ui/hover-card.tsx +44 -0
  144. package/templates/nextjs-shadcn/components/ui/input-group.tsx +170 -0
  145. package/templates/nextjs-shadcn/components/ui/input-otp.tsx +77 -0
  146. package/templates/nextjs-shadcn/components/ui/input.tsx +21 -0
  147. package/templates/nextjs-shadcn/components/ui/item.tsx +193 -0
  148. package/templates/nextjs-shadcn/components/ui/kbd.tsx +28 -0
  149. package/templates/nextjs-shadcn/components/ui/label.tsx +24 -0
  150. package/templates/nextjs-shadcn/components/ui/menubar.tsx +276 -0
  151. package/templates/nextjs-shadcn/components/ui/native-select.tsx +53 -0
  152. package/templates/nextjs-shadcn/components/ui/navigation-menu.tsx +168 -0
  153. package/templates/nextjs-shadcn/components/ui/pagination.tsx +127 -0
  154. package/templates/nextjs-shadcn/components/ui/popover.tsx +89 -0
  155. package/templates/nextjs-shadcn/components/ui/progress.tsx +31 -0
  156. package/templates/nextjs-shadcn/components/ui/radio-group.tsx +45 -0
  157. package/templates/nextjs-shadcn/components/ui/resizable.tsx +53 -0
  158. package/templates/nextjs-shadcn/components/ui/scroll-area.tsx +58 -0
  159. package/templates/nextjs-shadcn/components/ui/select.tsx +190 -0
  160. package/templates/nextjs-shadcn/components/ui/separator.tsx +28 -0
  161. package/templates/nextjs-shadcn/components/ui/sheet.tsx +143 -0
  162. package/templates/nextjs-shadcn/components/ui/sidebar.tsx +726 -0
  163. package/templates/nextjs-shadcn/components/ui/skeleton.tsx +13 -0
  164. package/templates/nextjs-shadcn/components/ui/slider.tsx +63 -0
  165. package/templates/nextjs-shadcn/components/ui/sonner.tsx +40 -0
  166. package/templates/nextjs-shadcn/components/ui/spinner.tsx +16 -0
  167. package/templates/nextjs-shadcn/components/ui/switch.tsx +35 -0
  168. package/templates/nextjs-shadcn/components/ui/table.tsx +116 -0
  169. package/templates/nextjs-shadcn/components/ui/tabs.tsx +91 -0
  170. package/templates/nextjs-shadcn/components/ui/textarea.tsx +18 -0
  171. package/templates/nextjs-shadcn/components/ui/toggle-group.tsx +83 -0
  172. package/templates/nextjs-shadcn/components/ui/toggle.tsx +47 -0
  173. package/templates/nextjs-shadcn/components/ui/tooltip.tsx +57 -0
  174. package/templates/nextjs-shadcn/components.json +23 -0
  175. package/templates/nextjs-shadcn/hooks/use-mobile.ts +19 -0
  176. package/templates/nextjs-shadcn/lib/utils.ts +6 -0
  177. package/templates/nextjs-shadcn/next-env.d.ts +6 -0
  178. package/templates/nextjs-shadcn/next.config.ts +8 -0
  179. package/templates/nextjs-shadcn/open-next.config.ts +6 -0
  180. package/templates/nextjs-shadcn/package.json +55 -0
  181. package/templates/nextjs-shadcn/postcss.config.mjs +8 -0
  182. package/templates/nextjs-shadcn/tsconfig.json +28 -0
  183. package/templates/nextjs-shadcn/wrangler.jsonc +23 -0
  184. package/templates/resend/.jack.json +64 -0
  185. package/templates/resend/bun.lock +23 -0
  186. package/templates/resend/package.json +16 -0
  187. package/templates/resend/schema.sql +13 -0
  188. package/templates/resend/src/email.ts +165 -0
  189. package/templates/resend/src/index.ts +108 -0
  190. package/templates/resend/tsconfig.json +17 -0
  191. package/templates/resend/wrangler.jsonc +11 -0
  192. package/templates/saas/.jack.json +1 -1
  193. package/templates/ai-chat/public/chat.js +0 -149
@@ -0,0 +1,8 @@
1
+ interface CloudflareEnv {
2
+ DB: D1Database;
3
+ BETTER_AUTH_SECRET: string;
4
+ GITHUB_CLIENT_ID?: string;
5
+ GITHUB_CLIENT_SECRET?: string;
6
+ GOOGLE_CLIENT_ID?: string;
7
+ GOOGLE_CLIENT_SECRET?: string;
8
+ }
@@ -0,0 +1,191 @@
1
+ "use client";
2
+
3
+ import { authClient } from "@/lib/auth-client";
4
+ import { Check, Eye, EyeOff, Loader2, Mail } from "lucide-react";
5
+ import Link from "next/link";
6
+ import { useSearchParams } from "next/navigation";
7
+ import { useState } from "react";
8
+
9
+ interface AuthFormProps {
10
+ mode: "login" | "signup";
11
+ }
12
+
13
+ export function AuthForm({ mode }: AuthFormProps) {
14
+ const searchParams = useSearchParams();
15
+ const [email, setEmail] = useState("");
16
+ const [password, setPassword] = useState("");
17
+ const [name, setName] = useState("");
18
+ const [error, setError] = useState("");
19
+ const [loading, setLoading] = useState(false);
20
+ const [showPassword, setShowPassword] = useState(false);
21
+ const [success, setSuccess] = useState(false);
22
+
23
+ const isLogin = mode === "login";
24
+ const rawCallback = isLogin ? searchParams.get("callbackUrl") : null;
25
+ const callbackUrl =
26
+ rawCallback?.startsWith("/") && !rawCallback.startsWith("//") ? rawCallback : "/dashboard";
27
+ const title = isLogin ? "Sign in to your account" : "Create your account";
28
+ const submitLabel = isLogin ? "Sign In" : "Sign Up";
29
+ const switchText = isLogin ? "Don't have an account?" : "Already have an account?";
30
+ const switchHref = isLogin ? "/signup" : "/login";
31
+ const switchLabel = isLogin ? "Sign up" : "Sign in";
32
+
33
+ async function handleSubmit(e: React.FormEvent) {
34
+ e.preventDefault();
35
+ setError("");
36
+ setLoading(true);
37
+
38
+ try {
39
+ if (isLogin) {
40
+ const result = await authClient.signIn.email({
41
+ email,
42
+ password,
43
+ });
44
+
45
+ if (result.error) {
46
+ setError(result.error.message || "Invalid email or password");
47
+ return;
48
+ }
49
+
50
+ // Full reload ensures middleware + server components re-evaluate with new auth state
51
+ window.location.href = callbackUrl;
52
+ } else {
53
+ const result = await authClient.signUp.email({
54
+ email,
55
+ password,
56
+ name: name || email.split("@")[0],
57
+ });
58
+
59
+ if (result.error) {
60
+ setError(result.error.message || "Failed to create account");
61
+ return;
62
+ }
63
+
64
+ setSuccess(true);
65
+ setTimeout(() => {
66
+ window.location.href = "/dashboard";
67
+ }, 800);
68
+ return;
69
+ }
70
+ } catch {
71
+ setError("Something went wrong. Please try again.");
72
+ } finally {
73
+ setLoading(false);
74
+ }
75
+ }
76
+
77
+ if (success) {
78
+ return (
79
+ <div className="flex min-h-screen items-center justify-center px-4">
80
+ <div className="w-full max-w-sm text-center">
81
+ <div className="mx-auto mb-4 flex size-12 items-center justify-center rounded-full bg-green-100">
82
+ <Check className="size-6 text-green-600" />
83
+ </div>
84
+ <h1 className="text-xl font-bold">Account created!</h1>
85
+ <p className="mt-2 text-sm text-gray-500">Redirecting to your dashboard...</p>
86
+ </div>
87
+ </div>
88
+ );
89
+ }
90
+
91
+ return (
92
+ <div className="flex min-h-screen items-center justify-center px-4">
93
+ <div className="w-full max-w-sm">
94
+ <div className="mb-8 text-center">
95
+ <Link
96
+ href="/"
97
+ className="mb-6 inline-flex items-center gap-2 text-sm text-gray-500 transition hover:text-gray-700"
98
+ >
99
+ &larr; Back
100
+ </Link>
101
+ <h1 className="text-2xl font-bold">{title}</h1>
102
+ </div>
103
+
104
+ <form onSubmit={handleSubmit} className="space-y-4">
105
+ {!isLogin && (
106
+ <div>
107
+ <label htmlFor="name" className="mb-1.5 block text-sm font-medium text-gray-700">
108
+ Name
109
+ </label>
110
+ <input
111
+ id="name"
112
+ type="text"
113
+ value={name}
114
+ onChange={(e) => setName(e.target.value)}
115
+ placeholder="Your name"
116
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-500 focus:ring-2 focus:ring-gray-200"
117
+ />
118
+ </div>
119
+ )}
120
+
121
+ <div>
122
+ <label htmlFor="email" className="mb-1.5 block text-sm font-medium text-gray-700">
123
+ Email
124
+ </label>
125
+ <input
126
+ id="email"
127
+ type="email"
128
+ value={email}
129
+ onChange={(e) => setEmail(e.target.value)}
130
+ placeholder="you@example.com"
131
+ required
132
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 text-sm outline-none transition focus:border-gray-500 focus:ring-2 focus:ring-gray-200"
133
+ />
134
+ </div>
135
+
136
+ <div>
137
+ <label htmlFor="password" className="mb-1.5 block text-sm font-medium text-gray-700">
138
+ Password
139
+ </label>
140
+ <div className="relative">
141
+ <input
142
+ id="password"
143
+ type={showPassword ? "text" : "password"}
144
+ value={password}
145
+ onChange={(e) => setPassword(e.target.value)}
146
+ placeholder="Enter your password"
147
+ required
148
+ minLength={8}
149
+ className="w-full rounded-lg border border-gray-300 px-3 py-2 pr-9 text-sm outline-none transition focus:border-gray-500 focus:ring-2 focus:ring-gray-200"
150
+ />
151
+ <button
152
+ type="button"
153
+ onClick={() => setShowPassword(!showPassword)}
154
+ className="absolute right-2.5 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
155
+ tabIndex={-1}
156
+ >
157
+ {showPassword ? <EyeOff className="size-4" /> : <Eye className="size-4" />}
158
+ </button>
159
+ </div>
160
+ </div>
161
+
162
+ {error && (
163
+ <div className="rounded-lg border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-600">
164
+ {error}
165
+ </div>
166
+ )}
167
+
168
+ <button
169
+ type="submit"
170
+ disabled={loading}
171
+ className="flex w-full items-center justify-center gap-2 rounded-lg bg-gray-900 px-4 py-2.5 text-sm font-medium text-white transition hover:bg-gray-800 disabled:cursor-not-allowed disabled:opacity-50"
172
+ >
173
+ {loading ? <Loader2 className="size-4 animate-spin" /> : <Mail className="size-4" />}
174
+ {submitLabel}
175
+ </button>
176
+ </form>
177
+
178
+ {/* Social login (GitHub, Google) is pre-wired in lib/auth.ts.
179
+ To enable: add GITHUB_CLIENT_ID + GITHUB_CLIENT_SECRET or
180
+ GOOGLE_CLIENT_ID + GOOGLE_CLIENT_SECRET via `jack secrets` */}
181
+
182
+ <p className="mt-6 text-center text-sm text-gray-500">
183
+ {switchText}{" "}
184
+ <Link href={switchHref} className="font-medium text-gray-900 hover:underline">
185
+ {switchLabel}
186
+ </Link>
187
+ </p>
188
+ </div>
189
+ </div>
190
+ );
191
+ }
@@ -0,0 +1,50 @@
1
+ import { Shield } from "lucide-react";
2
+ import Link from "next/link";
3
+
4
+ interface HeaderProps {
5
+ user?: {
6
+ name: string | null;
7
+ email: string;
8
+ };
9
+ }
10
+
11
+ export function Header({ user }: HeaderProps) {
12
+ return (
13
+ <header className="border-b border-gray-200 bg-white">
14
+ <div className="mx-auto flex h-14 max-w-3xl items-center justify-between px-6">
15
+ <Link href="/" className="flex items-center gap-2 font-semibold">
16
+ <div className="flex size-7 items-center justify-center rounded-md bg-gray-900 text-white text-xs">
17
+ <Shield className="size-4" />
18
+ </div>
19
+ jack-template
20
+ </Link>
21
+
22
+ <nav className="flex items-center gap-1">
23
+ {user ? (
24
+ <Link
25
+ href="/dashboard"
26
+ className="rounded-md px-3 py-1.5 text-sm font-medium text-gray-600 transition hover:bg-gray-100 hover:text-gray-900"
27
+ >
28
+ Dashboard
29
+ </Link>
30
+ ) : (
31
+ <>
32
+ <Link
33
+ href="/login"
34
+ className="rounded-md px-3 py-1.5 text-sm font-medium text-gray-600 transition hover:bg-gray-100 hover:text-gray-900"
35
+ >
36
+ Sign In
37
+ </Link>
38
+ <Link
39
+ href="/signup"
40
+ className="rounded-md bg-gray-900 px-3 py-1.5 text-sm font-medium text-white transition hover:bg-gray-800"
41
+ >
42
+ Sign Up
43
+ </Link>
44
+ </>
45
+ )}
46
+ </nav>
47
+ </div>
48
+ </header>
49
+ );
50
+ }
@@ -0,0 +1,23 @@
1
+ "use client";
2
+
3
+ import { authClient } from "@/lib/auth-client";
4
+ import { LogOut } from "lucide-react";
5
+
6
+ export function UserMenu() {
7
+ async function handleSignOut() {
8
+ await authClient.signOut();
9
+ // Full reload after auth state change ensures middleware + server components re-evaluate
10
+ window.location.href = "/";
11
+ }
12
+
13
+ return (
14
+ <button
15
+ type="button"
16
+ onClick={handleSignOut}
17
+ className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white px-4 py-2 text-sm font-medium text-gray-700 transition hover:bg-gray-50"
18
+ >
19
+ <LogOut className="size-4" />
20
+ Sign Out
21
+ </button>
22
+ );
23
+ }
@@ -0,0 +1,3 @@
1
+ import { createAuthClient } from "better-auth/react";
2
+
3
+ export const authClient = createAuthClient();
@@ -0,0 +1,43 @@
1
+ import { getCloudflareContext } from "@opennextjs/cloudflare";
2
+ import { betterAuth } from "better-auth";
3
+ import { Kysely } from "kysely";
4
+ import { D1Dialect } from "kysely-d1";
5
+
6
+ export function createAuth(d1: D1Database, env: Record<string, string | undefined>) {
7
+ // biome-ignore lint/suspicious/noExplicitAny: D1 has no typed schema
8
+ const db = new Kysely<any>({
9
+ dialect: new D1Dialect({ database: d1 }),
10
+ });
11
+
12
+ return betterAuth({
13
+ database: {
14
+ db,
15
+ type: "sqlite",
16
+ },
17
+ secret: env.BETTER_AUTH_SECRET,
18
+ emailAndPassword: { enabled: true },
19
+ socialProviders: {
20
+ ...(env.GITHUB_CLIENT_ID && env.GITHUB_CLIENT_SECRET
21
+ ? {
22
+ github: {
23
+ clientId: env.GITHUB_CLIENT_ID,
24
+ clientSecret: env.GITHUB_CLIENT_SECRET,
25
+ },
26
+ }
27
+ : {}),
28
+ ...(env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SECRET
29
+ ? {
30
+ google: {
31
+ clientId: env.GOOGLE_CLIENT_ID,
32
+ clientSecret: env.GOOGLE_CLIENT_SECRET,
33
+ },
34
+ }
35
+ : {}),
36
+ },
37
+ });
38
+ }
39
+
40
+ export async function getAuth() {
41
+ const { env } = await getCloudflareContext();
42
+ return createAuth(env.DB, env as unknown as Record<string, string | undefined>);
43
+ }
@@ -0,0 +1,6 @@
1
+ import { type ClassValue, clsx } from "clsx";
2
+ import { twMerge } from "tailwind-merge";
3
+
4
+ export function cn(...inputs: ClassValue[]) {
5
+ return twMerge(clsx(inputs));
6
+ }
@@ -0,0 +1,33 @@
1
+ import { type NextRequest, NextResponse } from "next/server";
2
+
3
+ const protectedPaths = ["/dashboard"];
4
+
5
+ export function middleware(request: NextRequest) {
6
+ const { pathname } = request.nextUrl;
7
+
8
+ const isProtected = protectedPaths.some(
9
+ (path) => pathname === path || pathname.startsWith(`${path}/`),
10
+ );
11
+
12
+ if (!isProtected) {
13
+ return NextResponse.next();
14
+ }
15
+
16
+ // Edge-safe session check: only inspect the cookie, do not call auth.api.getSession()
17
+ // because that requires Node.js APIs (perf_hooks) unavailable in edge middleware.
18
+ const sessionCookie =
19
+ request.cookies.get("better-auth.session_token") ??
20
+ request.cookies.get("__Secure-better-auth.session_token");
21
+
22
+ if (!sessionCookie?.value) {
23
+ const loginUrl = new URL("/login", request.url);
24
+ loginUrl.searchParams.set("callbackUrl", pathname);
25
+ return NextResponse.redirect(loginUrl);
26
+ }
27
+
28
+ return NextResponse.next();
29
+ }
30
+
31
+ export const config = {
32
+ matcher: ["/dashboard/:path*"],
33
+ };
@@ -0,0 +1,8 @@
1
+ import { initOpenNextCloudflareForDev } from "@opennextjs/cloudflare";
2
+ import type { NextConfig } from "next";
3
+
4
+ initOpenNextCloudflareForDev();
5
+
6
+ const nextConfig: NextConfig = {};
7
+
8
+ export default nextConfig;
@@ -0,0 +1,6 @@
1
+ import { defineCloudflareConfig } from "@opennextjs/cloudflare";
2
+ import r2IncrementalCache from "@opennextjs/cloudflare/overrides/incremental-cache/r2-incremental-cache";
3
+
4
+ export default defineCloudflareConfig({
5
+ incrementalCache: r2IncrementalCache,
6
+ });
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "jack-template",
3
+ "type": "module",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "preview": "opennextjs-cloudflare build && opennextjs-cloudflare preview",
9
+ "deploy": "opennextjs-cloudflare build && opennextjs-cloudflare deploy",
10
+ "cf-typegen": "wrangler types --env-interface CloudflareEnv cloudflare-env.d.ts"
11
+ },
12
+ "dependencies": {
13
+ "@opennextjs/cloudflare": "^1.0.0",
14
+ "@tailwindcss/postcss": "^4.1.18",
15
+ "better-auth": "^1",
16
+ "clsx": "^2.1.1",
17
+ "kysely": "^0.27",
18
+ "kysely-d1": "^0.3",
19
+ "lucide-react": "^0.563.0",
20
+ "next": "^15.0.0",
21
+ "postcss": "^8.5.6",
22
+ "react": "^19.0.0",
23
+ "react-dom": "^19.0.0",
24
+ "tailwind-merge": "^3.4.0",
25
+ "tailwindcss": "^4.1.18"
26
+ },
27
+ "devDependencies": {
28
+ "@cloudflare/workers-types": "^4.20241205.0",
29
+ "@types/react": "^19.0.0",
30
+ "@types/react-dom": "^19.0.0",
31
+ "typescript": "^5.6.0"
32
+ }
33
+ }
@@ -0,0 +1,8 @@
1
+ /** @type {import('postcss-load-config').Config} */
2
+ const config = {
3
+ plugins: {
4
+ "@tailwindcss/postcss": {},
5
+ },
6
+ };
7
+
8
+ export default config;
@@ -0,0 +1,49 @@
1
+ CREATE TABLE IF NOT EXISTS user (
2
+ id TEXT PRIMARY KEY,
3
+ name TEXT NOT NULL,
4
+ email TEXT NOT NULL UNIQUE,
5
+ emailVerified INTEGER NOT NULL DEFAULT 0,
6
+ image TEXT,
7
+ createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
8
+ updatedAt INTEGER NOT NULL DEFAULT (unixepoch())
9
+ );
10
+
11
+ CREATE TABLE IF NOT EXISTS session (
12
+ id TEXT PRIMARY KEY,
13
+ expiresAt INTEGER NOT NULL,
14
+ token TEXT NOT NULL UNIQUE,
15
+ ipAddress TEXT,
16
+ userAgent TEXT,
17
+ userId TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
18
+ createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
19
+ updatedAt INTEGER NOT NULL DEFAULT (unixepoch())
20
+ );
21
+
22
+ CREATE TABLE IF NOT EXISTS account (
23
+ id TEXT PRIMARY KEY,
24
+ accountId TEXT NOT NULL,
25
+ providerId TEXT NOT NULL,
26
+ userId TEXT NOT NULL REFERENCES user(id) ON DELETE CASCADE,
27
+ accessToken TEXT,
28
+ refreshToken TEXT,
29
+ idToken TEXT,
30
+ accessTokenExpiresAt INTEGER,
31
+ refreshTokenExpiresAt INTEGER,
32
+ scope TEXT,
33
+ password TEXT,
34
+ createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
35
+ updatedAt INTEGER NOT NULL DEFAULT (unixepoch())
36
+ );
37
+
38
+ CREATE TABLE IF NOT EXISTS verification (
39
+ id TEXT PRIMARY KEY,
40
+ identifier TEXT NOT NULL,
41
+ value TEXT NOT NULL,
42
+ expiresAt INTEGER NOT NULL,
43
+ createdAt INTEGER NOT NULL DEFAULT (unixepoch()),
44
+ updatedAt INTEGER NOT NULL DEFAULT (unixepoch())
45
+ );
46
+
47
+ CREATE INDEX IF NOT EXISTS idx_session_token ON session(token);
48
+ CREATE INDEX IF NOT EXISTS idx_session_userId ON session(userId);
49
+ CREATE INDEX IF NOT EXISTS idx_account_userId ON account(userId);
@@ -0,0 +1,28 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "jsx": "preserve",
8
+ "strict": true,
9
+ "skipLibCheck": true,
10
+ "noEmit": true,
11
+ "incremental": true,
12
+ "esModuleInterop": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "paths": {
16
+ "@/*": ["./*"]
17
+ },
18
+ "plugins": [
19
+ {
20
+ "name": "next"
21
+ }
22
+ ],
23
+ "types": ["@cloudflare/workers-types"],
24
+ "allowJs": true
25
+ },
26
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", "cloudflare-env.d.ts"],
27
+ "exclude": ["node_modules"]
28
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "node_modules/wrangler/config-schema.json",
3
+ "name": "jack-template",
4
+ "main": ".open-next/worker.js",
5
+ "compatibility_date": "2024-12-30",
6
+ "compatibility_flags": ["nodejs_compat"],
7
+ "assets": {
8
+ "directory": ".open-next/assets",
9
+ "binding": "ASSETS"
10
+ },
11
+ "d1_databases": [
12
+ {
13
+ "binding": "DB",
14
+ "database_name": "jack-template-db"
15
+ }
16
+ ],
17
+ "r2_buckets": [
18
+ {
19
+ "binding": "NEXT_INC_CACHE_R2_BUCKET",
20
+ "bucket_name": "jack-template-cache"
21
+ }
22
+ ]
23
+ }
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "nextjs-clerk",
3
+ "description": "Next.js + Clerk (managed auth with pre-built UI components)",
4
+ "secrets": ["NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY", "CLERK_SECRET_KEY"],
5
+ "capabilities": [],
6
+ "intent": {
7
+ "keywords": ["clerk", "auth", "login", "nextjs", "managed-auth", "sso"],
8
+ "examples": ["app with managed auth", "clerk login", "quick auth setup"]
9
+ },
10
+ "agentContext": {
11
+ "summary": "A Next.js app with Clerk managed authentication, pre-built sign-in/sign-up components, and route protection middleware.",
12
+ "full_text": "## Project Structure\n\n- `app/layout.tsx` - Root layout with ClerkProvider\n- `app/page.tsx` - Landing page with sign-in CTA\n- `app/sign-in/[[...sign-in]]/page.tsx` - Clerk sign-in page\n- `app/sign-up/[[...sign-up]]/page.tsx` - Clerk sign-up page\n- `app/dashboard/page.tsx` - Protected dashboard page\n- `middleware.ts` - Clerk auth middleware for route protection\n- `components/header.tsx` - Header with UserButton for profile/logout\n\n## Authentication\n\nUses Clerk for fully managed auth. No database tables needed for auth.\n\n### Route Protection\n\nThe `middleware.ts` uses `clerkMiddleware()` with `createRouteMatcher()` to protect routes. By default, `/dashboard` and `/dashboard/*` are protected.\n\n### Client Components\n\n```tsx\nimport { SignInButton, SignedIn, SignedOut, UserButton } from '@clerk/nextjs';\n\n// Show sign-in button when signed out\n<SignedOut><SignInButton /></SignedOut>\n\n// Show user button when signed in\n<SignedIn><UserButton /></SignedIn>\n```\n\n### Server Components\n\n```tsx\nimport { auth, currentUser } from '@clerk/nextjs/server';\n\n// Get auth state\nconst { userId } = await auth();\n\n// Get full user object\nconst user = await currentUser();\n```\n\n## Environment Variables\n\n- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` - Clerk publishable key (pk_test_...)\n- `CLERK_SECRET_KEY` - Clerk secret key (sk_test_...)\n\nGet these from https://dashboard.clerk.com\n\n## OpenNext on Cloudflare — Important Rules\n\nThis app runs via OpenNext on Cloudflare Workers. Follow these rules:\n\n### Use window.location.href instead of router.push() for full-page transitions\nOpenNext has a broken webpack chunk URL resolver. `router.push()` to pages whose chunks aren't loaded fails with `ChunkLoadError`. Use `window.location.href` for auth state changes or cross-section navigation. `<Link>` components work fine.\n\n### Add `export const dynamic = 'force-dynamic'` to pages using getCloudflareContext()\nWithout this, static prerendering fails because `getCloudflareContext()` is only available at request time.\n\n### Edge middleware cannot use Node.js APIs\nDo not import Node.js built-in modules in middleware. Clerk's `clerkMiddleware()` is edge-compatible.\n\n## Resources\n\n- [Clerk Docs](https://clerk.com/docs)\n- [Clerk Next.js Quickstart](https://clerk.com/docs/quickstarts/nextjs)\n- [OpenNext Docs](https://opennext.js.org/cloudflare)"
13
+ },
14
+ "hooks": {
15
+ "preCreate": [
16
+ {
17
+ "action": "require",
18
+ "source": "secret",
19
+ "key": "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
20
+ "message": "Clerk publishable key",
21
+ "setupUrl": "https://dashboard.clerk.com",
22
+ "onMissing": "prompt",
23
+ "promptMessage": "Enter Clerk Publishable Key (pk_test_...):"
24
+ },
25
+ {
26
+ "action": "require",
27
+ "source": "secret",
28
+ "key": "CLERK_SECRET_KEY",
29
+ "message": "Clerk secret key",
30
+ "setupUrl": "https://dashboard.clerk.com",
31
+ "onMissing": "prompt",
32
+ "promptMessage": "Enter Clerk Secret Key (sk_test_...):"
33
+ }
34
+ ],
35
+ "postDeploy": [
36
+ {
37
+ "action": "clipboard",
38
+ "text": "{{url}}",
39
+ "message": "Deploy URL copied to clipboard"
40
+ },
41
+ {
42
+ "action": "box",
43
+ "title": "Auth ready: {{name}}",
44
+ "lines": [
45
+ "{{url}}",
46
+ "",
47
+ "Clerk dashboard: https://dashboard.clerk.com",
48
+ "Sign-up and login pages work immediately.",
49
+ "Configure social providers in Clerk dashboard."
50
+ ]
51
+ }
52
+ ]
53
+ }
54
+ }
@@ -0,0 +1,69 @@
1
+ import { currentUser } from "@clerk/nextjs/server";
2
+ import { redirect } from "next/navigation";
3
+
4
+ export default async function DashboardPage() {
5
+ const user = await currentUser();
6
+
7
+ if (!user) {
8
+ redirect("/sign-in");
9
+ }
10
+
11
+ return (
12
+ <main className="mx-auto max-w-3xl px-6 py-12">
13
+ <h1 className="text-2xl font-bold">Dashboard</h1>
14
+ <p className="mt-1 text-gray-500">Welcome back. This page is protected by Clerk middleware.</p>
15
+
16
+ <div className="mt-8 rounded-xl border border-gray-200 bg-white p-6">
17
+ <h2 className="text-lg font-semibold">Your Profile</h2>
18
+ <dl className="mt-4 space-y-3">
19
+ <div className="flex gap-2">
20
+ <dt className="w-28 shrink-0 text-sm font-medium text-gray-500">Name</dt>
21
+ <dd className="text-sm">
22
+ {user.firstName} {user.lastName}
23
+ </dd>
24
+ </div>
25
+ <div className="flex gap-2">
26
+ <dt className="w-28 shrink-0 text-sm font-medium text-gray-500">Email</dt>
27
+ <dd className="text-sm">{user.emailAddresses[0]?.emailAddress}</dd>
28
+ </div>
29
+ <div className="flex gap-2">
30
+ <dt className="w-28 shrink-0 text-sm font-medium text-gray-500">User ID</dt>
31
+ <dd className="text-sm font-mono text-gray-400">{user.id}</dd>
32
+ </div>
33
+ <div className="flex gap-2">
34
+ <dt className="w-28 shrink-0 text-sm font-medium text-gray-500">Joined</dt>
35
+ <dd className="text-sm">
36
+ {user.createdAt ? new Date(user.createdAt).toLocaleDateString() : "Unknown"}
37
+ </dd>
38
+ </div>
39
+ </dl>
40
+ </div>
41
+
42
+ <div className="mt-6 rounded-xl border border-gray-200 bg-white p-6">
43
+ <h2 className="text-lg font-semibold">Next Steps</h2>
44
+ <ul className="mt-3 space-y-2 text-sm text-gray-600">
45
+ <li>
46
+ Add social providers in the{" "}
47
+ <a
48
+ href="https://dashboard.clerk.com"
49
+ target="_blank"
50
+ rel="noopener noreferrer"
51
+ className="font-medium text-gray-900 underline underline-offset-2"
52
+ >
53
+ Clerk dashboard
54
+ </a>
55
+ </li>
56
+ <li>Protect more routes by editing <code className="rounded bg-gray-100 px-1 py-0.5 text-xs">middleware.ts</code></li>
57
+ <li>
58
+ Access user data in server components with{" "}
59
+ <code className="rounded bg-gray-100 px-1 py-0.5 text-xs">currentUser()</code>
60
+ </li>
61
+ <li>
62
+ Access auth state in client components with{" "}
63
+ <code className="rounded bg-gray-100 px-1 py-0.5 text-xs">useUser()</code>
64
+ </li>
65
+ </ul>
66
+ </div>
67
+ </main>
68
+ );
69
+ }
@@ -0,0 +1 @@
1
+ @import "tailwindcss";
@@ -0,0 +1,3 @@
1
+ export function GET() {
2
+ return Response.json({ status: "ok", timestamp: Date.now() });
3
+ }