@xk2800/nextjs-template 0.7.0-beta.0 → 0.8.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +55 -322
  2. package/components/site/cookie-banner.tsx +57 -0
  3. package/dist/activity/logger.js +3 -0
  4. package/dist/activity/logger.js.map +1 -1
  5. package/dist/activity/logger.mjs +3 -3
  6. package/dist/activity/queries.d.mts +1 -1
  7. package/dist/activity/queries.d.ts +1 -1
  8. package/dist/activity/queries.js +3 -0
  9. package/dist/activity/queries.js.map +1 -1
  10. package/dist/activity/queries.mjs +2 -2
  11. package/dist/admin/queries.js +3 -0
  12. package/dist/admin/queries.js.map +1 -1
  13. package/dist/admin/queries.mjs +2 -2
  14. package/dist/auth/helpers.js +4 -1
  15. package/dist/auth/helpers.js.map +1 -1
  16. package/dist/auth/helpers.mjs +5 -5
  17. package/dist/auth/index.d.mts +2 -2
  18. package/dist/auth/index.d.ts +2 -2
  19. package/dist/auth/index.js +4 -1
  20. package/dist/auth/index.js.map +1 -1
  21. package/dist/auth/index.mjs +5 -5
  22. package/dist/{chunk-GTPTVNY2.mjs → chunk-2GFPALUD.mjs} +5 -2
  23. package/dist/chunk-2GFPALUD.mjs.map +1 -0
  24. package/dist/{chunk-7Q5T4NZ7.mjs → chunk-BWUSJSHC.mjs} +7 -7
  25. package/dist/chunk-BWUSJSHC.mjs.map +1 -0
  26. package/dist/{chunk-OHAUKE2Q.mjs → chunk-KZQNXX2M.mjs} +3 -3
  27. package/dist/{chunk-OHAUKE2Q.mjs.map → chunk-KZQNXX2M.mjs.map} +1 -1
  28. package/dist/{chunk-5PYDAD4P.mjs → chunk-V4UMTNGB.mjs} +3 -3
  29. package/dist/{chunk-5PYDAD4P.mjs.map → chunk-V4UMTNGB.mjs.map} +1 -1
  30. package/dist/{chunk-IF3JZRSZ.mjs → chunk-X6CBULEC.mjs} +4 -4
  31. package/dist/{chunk-IF3JZRSZ.mjs.map → chunk-X6CBULEC.mjs.map} +1 -1
  32. package/dist/config/env.d.mts +1 -0
  33. package/dist/config/env.d.ts +1 -0
  34. package/dist/config/env.js +3 -0
  35. package/dist/config/env.js.map +1 -1
  36. package/dist/config/env.mjs +1 -1
  37. package/dist/db/index.d.mts +1 -1
  38. package/dist/db/index.d.ts +1 -1
  39. package/dist/db/index.js +3 -0
  40. package/dist/db/index.js.map +1 -1
  41. package/dist/db/index.mjs +2 -2
  42. package/dist/db/schema.d.mts +1 -1
  43. package/dist/db/schema.d.ts +1 -1
  44. package/dist/{schema-BrBmQFkn.d.mts → schema-BghJ8N4X.d.mts} +1 -1
  45. package/dist/{schema-BrBmQFkn.d.ts → schema-BghJ8N4X.d.ts} +1 -1
  46. package/dist/sessions/queries.js +3 -0
  47. package/dist/sessions/queries.js.map +1 -1
  48. package/dist/sessions/queries.mjs +2 -2
  49. package/dist/settings/queries.d.mts +1 -1
  50. package/dist/settings/queries.d.ts +1 -1
  51. package/dist/settings/queries.js +3 -0
  52. package/dist/settings/queries.js.map +1 -1
  53. package/dist/settings/queries.mjs +3 -3
  54. package/dist/users/queries.js +3 -0
  55. package/dist/users/queries.js.map +1 -1
  56. package/dist/users/queries.mjs +2 -2
  57. package/package.json +3 -1
  58. package/dist/chunk-7Q5T4NZ7.mjs.map +0 -1
  59. package/dist/chunk-GTPTVNY2.mjs.map +0 -1
package/README.md CHANGED
@@ -1,365 +1,98 @@
1
- This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
1
+ # Next.js Template
2
2
 
3
- This is a template for Next.js projects using Drizzle ORM, self hosted PostgreSQL for development and Neon DB for production.
3
+ A production-ready Next.js 16 starter: Better-Auth (email/password, Google, One Tap, passkeys, TOTP 2FA), Drizzle + PostgreSQL, an admin dashboard, security features and shadcn/ui. The reusable parts ship as the npm package [`@xk2800/nextjs-template`](#using-as-a-package), so projects get fixes with a version bump.
4
4
 
5
- ## Basic Folder Structure
5
+ **📚 Full documentation:** [`docs/content`](docs/content), or run it locally with `bun run docs:dev` (<http://localhost:3001>).
6
6
 
7
- ```plaintext {5}
8
- 📦 <project root>
9
- ├── 📂 server/
10
- │ ├── 📂 db/
11
- │ │ ├── 📜 schema.ts
12
- │ │ └── 📜 index.ts
13
- │ ├── 📂 drizzle/
14
- │ ├── 📜 auth.ts
15
- │ └── 📂 test-connection/
16
- │ └── 📜 index.ts
17
- ├── 📜 .env.production
18
- ├── 📜 .env.development
19
- ├── 📜 drizzle.config.ts
20
- ├── 📜 package.json
21
- └── 📜 tsconfig.json
22
- ```
23
-
24
- ## Getting Started
7
+ ## Quick start
25
8
 
26
- 1. Install the packages:
9
+ Requires [Bun](https://bun.sh), Git and a PostgreSQL database. Run this from the folder you want the project created in:
27
10
 
28
11
  ```bash
29
- bun install
12
+ npx @xk2800/create-nextjs
13
+ # or
14
+ bunx @xk2800/create-nextjs
30
15
  ```
31
16
 
32
- 2. Setup the `.env.development` and `.env.production` files based on `.env.development.example` and `.env.production.example` respectively.
33
-
34
- **Important:** Generate an `AUTH_SECRET` using:
35
-
36
- ```bash
37
- openssl rand -base64 32
38
- ```
39
-
40
- ## Migrating Schema
41
-
42
- 1. Generating schema, running this one command works for development and production modes `bun run generate`
43
-
44
- 2. Migrating schema into:
17
+ The CLI asks for a project name, a database driver (`pg` or `neon`) and optional modules (Doppler, Resend, Docker). It then creates `.env.development` with a generated secret, installs dependencies and makes the first commit. After that:
45
18
 
46
19
  ```bash
47
- # development
20
+ cd my-app
21
+ # set DATABASE_URL in .env.development
22
+ bun run doctor # checks env, auth secret, DB connection, pending migrations
48
23
  bun run migrate:dev
49
-
50
- # production
51
- bun run migrate:prod
52
- ```
53
-
54
- ## Secrets management with Doppler (optional)
55
-
56
- `.env.*` files work as described above by default. If you'd rather manage secrets in [Doppler](https://doppler.com) instead of local `.env` files, that's supported as an opt-in alternative — every `*:doppler` script does the same thing as its `.env`-based counterpart, just sourcing secrets from Doppler instead:
57
-
58
- ```bash
59
- bun run dev:doppler # instead of bun run dev
60
- bun run build:doppler # instead of bun run build
61
- bun run start:doppler # instead of bun run start
62
- bun run generate:doppler # instead of bun run generate
63
- bun run migrate:doppler # instead of bun run migrate:dev / migrate:prod / migrate:test
64
- bun run studio:doppler # instead of bun run studio:dev / studio:prod
65
- ```
66
-
67
- Setup (one-time, per machine):
68
-
69
- ```bash
70
- brew install dopplerhq/cli/doppler # or see https://docs.doppler.com/docs/install-cli
71
- doppler login
72
- doppler setup # run in the project root; select your Doppler project + config
24
+ bun dev
73
25
  ```
74
26
 
75
- `doppler setup` writes a `doppler.yaml` that maps this directory to a Doppler project/config — it holds no secrets itself, so it's safe to commit. `doppler run -- <command>` injects your Doppler secrets into `process.env` before the command starts; nothing in `config/env.ts` or `drizzle.config.ts` needed to change, since they just read `process.env` regardless of where it came from. Both Next.js and Bun treat already-set `process.env` values as higher priority than `.env` file values, so this layers safely even if `.env.development` is also present.
76
-
77
- To target a specific Doppler config without re-running `doppler setup` (e.g. in CI), pass `--config`: `doppler run --config prd -- bun run migrate:doppler`.
78
-
79
- ## Testing Connection
80
-
81
- 1. To test and ensure that the connection and database works, run:
82
-
83
- ```bash
84
- # development
85
- bun --env-file=.env.development server/test-connection/index.ts
27
+ Sign up at <http://localhost:3000>, then make yourself an admin:
86
28
 
87
- # production
88
- bun --env-file=.env.production server/test-connection/index.ts
29
+ ```sql
30
+ UPDATE "user" SET roles = 'admin' WHERE email = 'you@example.com';
89
31
  ```
90
32
 
91
33
  ## Features
92
34
 
93
- 1. Basic Next.js + Tailwind CSS boilerplate
94
- 2. Shadcn/ui
95
- 3. Better-Auth with Google OAuth + email/password
96
- 4. Basic login flow using Better-Auth
97
- 5. Connection template to a postgresql database for development, testing and production
98
- 6. Two-factor auth (TOTP + backup codes) and passkeys (WebAuthn), managed from the dashboard Settings page
99
- 7. Password reset and self-serve email verification, both via Resend-delivered React Email templates
100
- 8. Security email alerts — new-device sign-ins and passkey add/remove, mirrored to the activity log
101
- 9. Cookie-consent banner — "Necessary only" / "Accept all", with a "Cookie settings" link in the footer to change it later. The device fingerprint and Google One Tap only run after "Accept all"
102
- 10. Self-serve account deletion from the dashboard Settings page (password-confirmed, cascades all user data)
103
-
104
- ## WIP
35
+ - **Auth:** email/password, Google OAuth, Google One Tap, passkeys, TOTP 2FA with backup codes, password reset, email verification
36
+ - **Security:** new-device email alerts, per-device sign-in/sign-up throttle, active-session revocation, self-serve account deletion, cookie-consent banner
37
+ - **Admin:** user management (roles, bans, bulk actions, CSV export), impersonation, system-wide activity log, device-security cards, live auth toggles, maintenance mode
38
+ - **Ops:** `bun run doctor`, Docker image, Doppler support, separate env files for dev, prod and test
105
39
 
106
- 1. Nothing in flight
40
+ ## Docs
107
41
 
108
- ## Using this as a package
42
+ | Topic | |
43
+ |---|---|
44
+ | Every env var, env vs. admin settings, Doppler | [Configuration](docs/content/configuration.mdx) |
45
+ | Folder layout, request flow, adding protected pages/routes | [Architecture](docs/content/architecture.mdx) |
46
+ | Tables, column gotchas, migrations | [Database](docs/content/database.mdx) |
47
+ | Providers, sessions, helpers, client usage | [Authentication](docs/content/authentication.mdx) |
48
+ | 2FA, passkeys, device alerts, throttle, cookie consent | [Security Features](docs/content/security.mdx) |
49
+ | Admin dashboard, impersonation, maintenance mode | [Admin & Maintenance](docs/content/admin.mdx) |
50
+ | Every route, body and guard | [API Reference](docs/content/api.mdx) |
51
+ | UI, auth, dashboard and email components | [Components](docs/content/components.mdx) |
52
+ | Installing the npm package | [Using as a Package](docs/content/npm-package.mdx) |
53
+ | Production checklist, migrations, Docker | [Deployment](docs/content/deployment.mdx) |
54
+ | Common errors and fixes | [Troubleshooting](docs/content/troubleshooting.mdx) |
109
55
 
110
- This repo also publishes its reusable auth/db/UI pieces as a public npm package, `@xk2800/nextjs-template`, to the npm registry. Use this in new projects instead of copy-pasting the whole template.
56
+ ## Using as a package
111
57
 
112
- ### 1. Install
58
+ For a **new** project, use the CLI above. It wires all of this up for you. To add the package to an **existing** app:
113
59
 
114
60
  ```bash
115
61
  bun add @xk2800/nextjs-template
116
62
  ```
117
63
 
118
- This has peer dependencies on `next`, `react`, `react-dom`, and the Radix/shadcn packages the UI components use (`@radix-ui/react-*`, `class-variance-authority`, `clsx`, `tailwind-merge`, `lucide-react`, `react-icons`). If your project already uses shadcn/ui, these are typically already installed. Three more peer deps are optional, only needed if you use the specific feature: `next-themes` (theme toggle/provider), `sonner` (dashboard session-revoke toasts), `@react-email/components` (email templates).
119
-
120
- ### 2. Enable `transpilePackages`
121
-
122
- UI components ship as raw `.tsx` source so `'use client'` boundaries survive. Add this to your `next.config.ts`:
123
-
124
64
  ```ts
125
- const nextConfig: NextConfig = {
126
- transpilePackages: ["@xk2800/nextjs-template"],
127
- };
65
+ // next.config.ts — components ship as raw .tsx
66
+ const nextConfig = { transpilePackages: ["@xk2800/nextjs-template"] }
128
67
  ```
129
68
 
130
- ### 3. Tailwind theme + content scanning
131
-
132
- The UI components use shadcn's CSS variable-based theme (`bg-primary`, `border-input`, etc.) and Tailwind v4 doesn't scan `node_modules` for class names by default. Add both to your `app/globals.css`, after the existing `@import "tailwindcss";`:
133
-
134
69
  ```css
70
+ /* app/globals.css */
135
71
  @import "tailwindcss";
136
72
  @import "@xk2800/nextjs-template/styles/theme.css";
137
73
  @source "../node_modules/@xk2800/nextjs-template";
138
74
  ```
139
75
 
140
- If you use `Dialog`, `AlertDialog`, or `DropdownMenu`, also install and import `tw-animate-css` for their open/close transitions — without it they still work, just without animation.
141
-
142
- ### 4. Adding a social provider (e.g. Apple) in one project only
143
-
144
- `auth` is the package's default instance (Google + email/password). To add another provider in a specific project — without forking the template — use the `createAuth` factory. This needs three changes, all in your own project, none in the package.
145
-
146
- **a. Create your own `server/auth.ts`** that wraps the factory:
147
-
148
- ```ts
149
- // server/auth.ts (your project, not the package)
150
- import { createAuth } from "@xk2800/nextjs-template/auth";
151
-
152
- export const auth = createAuth({
153
- socialProviders: {
154
- apple: {
155
- clientId: process.env.APPLE_CLIENT_ID!, // your Apple Services ID
156
- clientSecret: process.env.APPLE_CLIENT_SECRET!, // generated signed JWT, not a static secret
157
- appBundleIdentifier: process.env.APPLE_APP_BUNDLE_IDENTIFIER!, // only if you also support native/iOS sign-in
158
- },
159
- },
160
- });
161
- ```
162
-
163
- The `socialProviders` you pass are **merged** with the default `google` provider, not a replacement — `auth.socialProviders` ends up as `{ google, apple }`. Other config (session policy, password hashing, schema wiring) isn't exposed as an override; those stay fixed since they're shared assumptions, not per-project knobs.
164
-
165
- **b. Point your API route at your own `auth`, not the package's:**
166
-
167
76
  ```ts
168
77
  // app/api/auth/[...all]/route.ts
169
- import { auth } from "@/server/auth"; // your local file from step a — not "@xk2800/nextjs-template/auth"
170
- import { toNextJsHandler } from "better-auth/next-js";
171
-
172
- export const { GET, POST } = toNextJsHandler(auth);
173
- ```
174
-
175
- If you skip this and leave the route importing the package's `auth` directly, the server will never see your `apple` provider — the route handler is what actually serves the OAuth callback, so it has to be backed by the instance you customized.
176
-
177
- **c. Trigger it client-side.** The shipped `components/auth/socialLogin.tsx` is hardcoded to Google only (it's meant as a starting point, not a generic multi-provider switcher) — it won't grow an Apple button on its own. Add your own trigger using the same `authClient` the package already exports:
178
-
179
- ```tsx
180
- "use client";
181
- import { Button } from "@xk2800/nextjs-template/components/ui/button";
182
- import { authClient } from "@xk2800/nextjs-template/auth-client";
183
-
184
- export function AppleSignInButton({ callbackUrl }: { callbackUrl?: string }) {
185
- return (
186
- <Button variant="outline" className="w-full" onClick={() => authClient.signIn.social({ provider: "apple", callbackURL: callbackUrl || "/dashboard" })}>
187
- Sign in with Apple
188
- </Button>
189
- );
190
- }
191
- ```
192
-
193
- Nothing else needs to change — `proxy.ts`, `auth/helpers` (`requireAuth`, `hasRole`), and `auth-client`'s `useSession` are all provider-agnostic; they work off the session cookie/token, not which provider created it.
194
-
195
- #### Disabling Google or email/password instead
196
-
197
- Going the other way — turning **off** Google or email/password in one project — doesn't need `createAuth()` at all. Both providers are gated by env vars read from `config/env.ts`, which the package's `auth` singleton, the shipped `/login` and `/signup` pages, and any project that imports `@xk2800/nextjs-template/config/env` all read from the same place:
198
-
199
- ```bash
200
- # .env.* in your project (not the package) — either or both, default is true
201
- AUTH_ENABLE_GOOGLE=false
202
- AUTH_ENABLE_EMAIL_PASSWORD=false
78
+ import { auth } from "@xk2800/nextjs-template/auth"
79
+ import { toNextJsHandler } from "better-auth/next-js"
80
+ export const { GET, POST } = toNextJsHandler(auth)
203
81
  ```
204
82
 
205
- Setting one to `false` removes it from the Better-Auth config server-side (not just from the UI) — a disabled provider's endpoints won't authenticate anyone, even if called directly. The shipped login/signup pages hide the corresponding button/form automatically since they read the same flags.
206
-
207
- **Setting both `AUTH_ENABLE_GOOGLE=false` and `AUTH_ENABLE_EMAIL_PASSWORD=false` only fails if you haven't added a replacement provider.** `createAuth()` checks the *final* merged provider list — after your own `overrides.socialProviders` (per the section above) is applied — and throws only if that list is genuinely empty and email/password is also off. So:
208
-
209
- - Both flags `false`, no override → throws. There would be zero ways to sign in.
210
- - Both flags `false`, `createAuth({ socialProviders: { apple: {...} } })` → works fine. Apple is your only provider, and that's a valid config.
211
-
212
- This check lives in `createAuth()`, not `config/env.ts` — `config/env.ts` can't see overrides (they're only known at `createAuth()` call time), so it doesn't gate this at all. It only throws for the package's own default `auth` export (used by the shipped `/login`, `/signup`, and `app/api/auth/[...all]/route.ts`) if *that* ends up providerless, since that instance never has overrides.
83
+ Peer dependencies, the full import list, migrations, adding providers and One Tap are covered in **[Using as a Package](docs/content/npm-package.mdx)**.
213
84
 
214
- #### Enabling Google One Tap
215
-
216
- [Google One Tap](https://better-auth.com/docs/plugins/one-tap) is off by default and needs **two** flags, not one — `config/env.ts` is server-only (`import "server-only"`), so `lib/auth-client.ts` and the shipped `components/auth/oneTap.tsx` can't read it. The public flag is what actually gets inlined into your browser bundle; the server one only controls whether the Better-Auth server registers the plugin.
217
-
218
- ```bash
219
- # .env.* in your project — both default to false
220
- AUTH_ENABLE_ONE_TAP=true
221
- NEXT_PUBLIC_AUTH_ENABLE_ONE_TAP=true
85
+ ## Common scripts
222
86
 
223
- # needed either way — same OAuth client as AUTH_GOOGLE_ID, but public
224
- NEXT_PUBLIC_GOOGLE_CLIENT_ID=
225
- ```
226
-
227
- One Tap also requires Google to be enabled (`AUTH_ENABLE_GOOGLE=true`, the default) — the server silently skips registering the plugin if Google isn't configured, rather than throwing, since email/password-only projects are a valid config.
228
-
229
- Mount the shipped component once, anywhere it should be able to prompt (root layout is typical):
230
-
231
- ```tsx
232
- import OneTap from "@xk2800/nextjs-template/components/auth/oneTap";
233
- ```
234
-
235
- `<OneTap />` self-gates on `NEXT_PUBLIC_AUTH_ENABLE_ONE_TAP` and on the current session (`useSession`), so it's safe to leave mounted permanently — it no-ops for signed-in users and for any project that hasn't turned the flag on, no conditional JSX needed on your end. Toggling the feature later is just flipping both env vars, no code changes.
236
-
237
- Your Google Cloud OAuth client also needs its **Authorized JavaScript origins** to include whatever origin you're testing/deploying from (e.g. `http://localhost:3000`) — One Tap validates the origin client-side, separately from the OAuth redirect URIs you already set up.
238
-
239
- **One Tap waits for cookie consent.** It only prompts once the visitor clicks "Accept all" in the cookie banner (see below). If you import `<OneTap />` without mounting `<CookieBanner />`, set `NEXT_PUBLIC_COOKIE_BANNER=false`. Otherwise nobody can consent and the prompt never shows.
240
-
241
- #### Cookie-consent banner
242
-
243
- On by default. `<CookieBanner />` (mounted in `app/layout.tsx`) asks once and stores the answer in the first-party `cookie_consent` cookie (`all` | `necessary`, 1 year). `<CookieSettingsButton />` (in the site footer) clears the answer so the banner comes back. Auth/session cookies and UI preferences are "necessary" and always on. Optional, and only after "Accept all":
244
-
245
- - **FingerprintJS device id**: `getVisitorId()` in `lib/device-fingerprint.ts` returns `null` without consent. New-device alerts are then skipped, and the sign-in/sign-up throttle falls back to its per-IP cap.
246
- - **Google One Tap**: see above.
247
-
248
- ```tsx
249
- import { CookieBanner, CookieSettingsButton } from "@xk2800/nextjs-template/components/site/cookie-banner";
250
- ```
251
-
252
- Gate your own analytics or marketing scripts the same way:
253
-
254
- ```tsx
255
- import { useCookieConsent, hasOptionalConsent } from "@xk2800/nextjs-template/lib/cookie-consent";
256
-
257
- const consent = useCookieConsent(); // "all" | "necessary" | null (not chosen yet / on the server)
258
- if (consent === "all") { /* load analytics */ }
259
- ```
260
-
261
- Don't need a banner (no EU/UK users, nothing optional)? Set `NEXT_PUBLIC_COOKIE_BANNER=false`. The banner and the footer link disappear, and consent is treated as granted, so nothing gated on it breaks.
262
-
263
- ### 5. Import what you need
264
-
265
- ```ts
266
- // auth
267
- import { auth } from "@xk2800/nextjs-template/auth";
268
- import { requireAuth, hasRole } from "@xk2800/nextjs-template/auth/helpers";
269
- import { authClient, useSession } from "@xk2800/nextjs-template/auth-client";
270
-
271
- // db
272
- import { db } from "@xk2800/nextjs-template/db";
273
- import { users, sessions } from "@xk2800/nextjs-template/db/schema";
274
-
275
- // server-only query helpers (each throws if imported into a Client Component)
276
- import { getAdminStats } from "@xk2800/nextjs-template/admin/queries";
277
- import { getUserSessions } from "@xk2800/nextjs-template/sessions/queries";
278
- import { logActivity } from "@xk2800/nextjs-template/activity/logger";
279
- import { getUserActivityLogs, getAllActivityLogs } from "@xk2800/nextjs-template/activity/queries";
280
-
281
- // config, types, isomorphic helpers
282
- import { config } from "@xk2800/nextjs-template/config/env";
283
- import { LoginSchema } from "@xk2800/nextjs-template/types/auth/loginSchema";
284
- import { SignupSchema } from "@xk2800/nextjs-template/types/auth/signupSchema";
285
- import { formatDate, formatDateTime, getUserInitials } from "@xk2800/nextjs-template/lib/formatters";
286
-
287
- // ui primitives (raw source, any file under components/ui)
288
- import { Button } from "@xk2800/nextjs-template/components/ui/button";
289
-
290
- // auth UI
291
- import AuthCard from "@xk2800/nextjs-template/components/auth/authCard";
292
- import LogoutButtons from "@xk2800/nextjs-template/components/auth/logoutButtons";
293
- import EmailPasswordLogin from "@xk2800/nextjs-template/components/auth/emailPasswordLogin";
294
- import EmailPasswordSignup from "@xk2800/nextjs-template/components/auth/emailPasswordSignup";
295
- import OneTap from "@xk2800/nextjs-template/components/auth/oneTap"; // no-ops unless NEXT_PUBLIC_AUTH_ENABLE_ONE_TAP=true
296
-
297
- // dashboard UI (each takes data as props — fetch with the helpers above in your page, then pass down)
298
- import ProfileCard from "@xk2800/nextjs-template/components/dashboard/profileCard";
299
- import AccountDetailsCard from "@xk2800/nextjs-template/components/dashboard/accountDetailsCard";
300
- import SessionsCard from "@xk2800/nextjs-template/components/dashboard/sessionsCard";
301
- import AdminSection, { type AdminStats } from "@xk2800/nextjs-template/components/dashboard/adminSection";
302
- import ActivityLogsCard from "@xk2800/nextjs-template/components/dashboard/activityLogsCard";
303
- import DashboardHeader, { type DashboardNavLink } from "@xk2800/nextjs-template/components/dashboard/dashboardHeader"; // brandName, brandHref, navLinks are optional props
304
-
305
- // optional feature UI (needs the matching optional peer dep)
306
- import { ThemeProvider } from "@xk2800/nextjs-template/components/providers/theme-provider"; // next-themes
307
- import { ThemeToggle } from "@xk2800/nextjs-template/components/theme/theme-toggle"; // next-themes
308
- import { SendEmailButton } from "@xk2800/nextjs-template/components/email/send-email-button";
309
- import { EmailTemplate } from "@xk2800/nextjs-template/components/email/email-template"; // @react-email/components
310
- ```
311
-
312
- ### 6. Not importable — copy these patterns instead
313
-
314
- `app/api/auth/[...all]/route.ts` and `proxy.ts` are Next.js file-convention code, not library exports. Copy the pattern into your own project:
315
-
316
- ```ts
317
- // app/api/auth/[...all]/route.ts
318
- import { auth } from "@xk2800/nextjs-template/auth";
319
- import { toNextJsHandler } from "better-auth/next-js";
320
-
321
- export const { GET, POST } = toNextJsHandler(auth);
322
- ```
323
-
324
- ### 7. Database migrations
325
-
326
- The package does not ship migrations. drizzle-kit needs a local file it can import directly (it can't resolve package `exports` subpaths reliably), so add a one-line re-export in your own project:
327
-
328
- ```ts
329
- // server/db/schema.ts
330
- export * from "@xk2800/nextjs-template/db/schema";
331
- ```
332
-
333
- Then point `drizzle.config.ts` at it, reading `DATABASE_URL` from `process.env` directly rather than importing `@xk2800/nextjs-template/config/env` — that module is guarded with `server-only`, which throws when resolved outside Next.js's own bundler (drizzle-kit runs via plain Node/esbuild, so the guard fires unconditionally there):
334
-
335
- ```ts
336
- // drizzle.config.ts
337
- import { defineConfig } from "drizzle-kit";
338
-
339
- export default defineConfig({
340
- out: "./server/drizzle",
341
- schema: "./server/db/schema.ts",
342
- dialect: "postgresql",
343
- dbCredentials: { url: process.env.DATABASE_URL! },
344
- });
345
- ```
346
-
347
- ### 8. Secrets management with Doppler (optional)
348
-
349
- The package's `config/env.ts` and every server-only module just read `process.env.*` — they don't care whether values came from `.env` files or were injected by Doppler, so using Doppler in a consuming project needs zero changes to `@xk2800/nextjs-template` itself. Set it up the same way as in the template repo:
350
-
351
- ```bash
352
- doppler login
353
- doppler setup # in your project root, select/create a Doppler project + config
354
- ```
355
-
356
- Then wrap any command that needs your env vars with `doppler run --`, instead of relying on `.env.development`/`.env.local`:
357
-
358
- ```bash
359
- doppler run -- bun dev
360
- doppler run -- bun run generate # drizzle-kit generate
361
- doppler run -- bun run migrate # drizzle-kit migrate
362
- doppler run -- next build
363
- ```
87
+ | | |
88
+ |---|---|
89
+ | `bun dev` | Dev server |
90
+ | `bun run doctor` | Check env, auth secret, DB connection and pending migrations |
91
+ | `bun update @xk2800/nextjs-template` | Pull template updates (then `bun run generate`) |
92
+ | `bun run generate` | Generate a migration from `server/db/schema.ts` |
93
+ | `bun run migrate:dev` / `:prod` / `:test` | Apply migrations |
94
+ | `bun run studio:dev` | Drizzle Studio |
95
+ | `bun run test` | Unit tests |
96
+ | `bun run *:doppler` | Any of the above with secrets from Doppler |
364
97
 
365
- Since `doppler run` injects secrets into `process.env` before the command starts, and both Next.js and Bun treat already-set `process.env` values as higher priority than `.env` file values, you can even keep `.env.development` around as a local fallback without conflict — Doppler's values win when present.
98
+ **Working on the template itself:** clone this repo, copy `.env.development.example` to `.env.development`, then run `bun install`. Publishing steps are in [PUBLISHING.md](PUBLISHING.md).
@@ -0,0 +1,57 @@
1
+ "use client"
2
+
3
+ import Link from "next/link"
4
+ import { useEffect, useState } from "react"
5
+
6
+ // Relative imports: this file ships raw in the npm package, where `@/` would
7
+ // resolve against the consuming project instead.
8
+ import { Button } from "../ui/button"
9
+ import { COOKIE_BANNER_ENABLED, setCookieConsent, useCookieConsent } from "../../lib/cookie-consent"
10
+
11
+ // Mount once in the root layout. Shows until the visitor picks an option;
12
+ // "Necessary only" is as prominent as "Accept all" on purpose — GDPR requires
13
+ // rejecting to be as easy as accepting.
14
+ export function CookieBanner() {
15
+ const consent = useCookieConsent()
16
+ // useCookieConsent() is null on the server *and* before a choice, so wait
17
+ // for mount to tell them apart — otherwise returning visitors see a flash.
18
+ const [mounted, setMounted] = useState(false)
19
+ useEffect(() => setMounted(true), [])
20
+
21
+ if (!COOKIE_BANNER_ENABLED || !mounted || consent) return null
22
+
23
+ return (
24
+ <div
25
+ role="dialog"
26
+ aria-live="polite"
27
+ aria-label="Cookie consent"
28
+ className="fixed inset-x-4 bottom-4 z-50 mx-auto max-w-lg rounded-lg border border-border bg-background p-4 shadow-lg"
29
+ >
30
+ <p className="text-sm text-muted-foreground">
31
+ We use necessary cookies to keep you signed in. With your permission we also use a device
32
+ fingerprint to detect suspicious sign-ins, and Google One Tap sign-in.{" "}
33
+ <Link href="/privacy" className="underline underline-offset-4 hover:text-foreground">
34
+ Privacy policy
35
+ </Link>
36
+ </p>
37
+ <div className="mt-3 flex justify-end gap-2">
38
+ <Button variant="outline" size="sm" onClick={() => setCookieConsent("necessary")}>
39
+ Necessary only
40
+ </Button>
41
+ <Button size="sm" onClick={() => setCookieConsent("all")}>
42
+ Accept all
43
+ </Button>
44
+ </div>
45
+ </div>
46
+ )
47
+ }
48
+
49
+ // Withdrawing consent must be as easy as giving it — this reopens the banner.
50
+ export function CookieSettingsButton({ className }: { className?: string }) {
51
+ if (!COOKIE_BANNER_ENABLED) return null
52
+ return (
53
+ <button type="button" className={className} onClick={() => setCookieConsent(null)}>
54
+ Cookie settings
55
+ </button>
56
+ )
57
+ }
@@ -220,6 +220,9 @@ var envSchema = zod.z.object({
220
220
  PORT: zod.z.string().default("3000").transform(Number),
221
221
  DATABASE_URL: zod.z.string().default(""),
222
222
  RESEND_API_KEY: zod.z.string().default(""),
223
+ // Sender for every transactional email. Must be on a domain verified in
224
+ // your Resend account.
225
+ EMAIL_FROM: zod.z.string().default("Acme <onboarding@xavierkhew.xyz>"),
223
226
  // Display name for this deployment — shown as the authenticator-app label
224
227
  // (2FA issuer) and the passkey relying-party name. Consuming projects set
225
228
  // this in their own .env; defaults to the template name.