├── .env.example
├── .eslintrc.cjs
├── .gitignore
├── README.md
├── drizzle.config.ts
├── env.d.ts
├── next.config.mjs
├── package.json
├── pnpm-lock.yaml
├── postcss.config.cjs
├── prettier.config.js
├── public
└── favicon.ico
├── src
├── app
│ ├── api
│ │ └── route.ts
│ ├── globals.css
│ ├── layout.tsx
│ └── page.tsx
├── env.js
└── server
│ ├── db
│ ├── index.ts
│ └── schema.ts
│ └── functions
│ └── customers.ts
├── tailwind.config.ts
├── tsconfig.json
└── wrangler.toml.example
/.env.example:
--------------------------------------------------------------------------------
1 | # Since the `.env` file is gitignored, you can use the `.env.example` file to
2 | # build a new `.env` file when you clone the repo. Keep `.env.example` file up-to-date
3 | # when you add new variables to `.env`.
4 |
5 | # `.env.example` file will be committed to version control, so make sure not to have any
6 | # secrets in it. If you are cloning this repo, create a copy of `.env.example` file named
7 | # `.env` and populate it with your secrets.
8 |
9 | # When adding additional environment variables, the schema in `/src/env.js`
10 | # should be updated accordingly.
11 |
12 | # Cloudflare variables
13 | CF_ACCOUNT_ID=""
14 | CF_USER_API_TOKEN=""
15 | DB_REMOTE_DATABASE_ID=""
16 |
17 | # Example:
18 | # SERVERVAR="foo"
19 | # NEXT_PUBLIC_CLIENTVAR="bar"
20 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import("eslint").Linter.Config} */
2 | const config = {
3 | parser: "@typescript-eslint/parser",
4 | parserOptions: {
5 | project: true,
6 | },
7 | plugins: ["@typescript-eslint"],
8 | extends: [
9 | "plugin:@next/next/recommended",
10 | "plugin:@typescript-eslint/recommended-type-checked",
11 | "plugin:@typescript-eslint/stylistic-type-checked",
12 | ],
13 | rules: {
14 | // These opinionated rules are enabled in stylistic-type-checked above.
15 | // Feel free to reconfigure them to your own preference.
16 | "@typescript-eslint/array-type": "off",
17 | "@typescript-eslint/consistent-type-definitions": "off",
18 |
19 | "@typescript-eslint/consistent-type-imports": [
20 | "warn",
21 | {
22 | prefer: "type-imports",
23 | fixStyle: "inline-type-imports",
24 | },
25 | ],
26 | "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
27 | "@typescript-eslint/require-await": "off",
28 | "@typescript-eslint/no-misused-promises": [
29 | "error",
30 | {
31 | checksVoidReturn: { attributes: false },
32 | },
33 | ],
34 | },
35 | };
36 |
37 | module.exports = config;
38 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # database
12 | /prisma/db.sqlite
13 | /prisma/db.sqlite-journal
14 |
15 | # next.js
16 | /.next/
17 | /out/
18 | next-env.d.ts
19 |
20 | # production
21 | /build
22 |
23 | # misc
24 | .DS_Store
25 | *.pem
26 | .vscode
27 |
28 | # debug
29 | npm-debug.log*
30 | yarn-debug.log*
31 | yarn-error.log*
32 | .pnpm-debug.log*
33 |
34 | # local env files
35 | # do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables
36 | .env*
37 |
38 | # vercel
39 | .vercel
40 |
41 | # typescript
42 | *.tsbuildinfo
43 |
44 | # wrangler
45 | .wrangler
46 | wrangler.toml*
47 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
Next.js + Cloudflare D1 SQL + Drizzle ORM + Drizzle Kit + Cloudflare Pages starter kit
2 |
3 | # Getting started
4 |
5 | ## Prerequisites
6 |
7 | 1. Node.js >=v20.11.0
8 | 2. pnpm >=v9.15.1
9 |
10 | ## Initialise the database(s)
11 |
12 | 1. [Create a production D1 database.](https://developers.cloudflare.com/d1/get-started/#3-create-a-database)
13 | 2. The starter kit focuses on 2 environments, **development on local machine** and **production on
14 | remote machine**. So, create the following files:
15 |
16 | 1. `.env.development`: duplicate `.env.example`, and set the variables to development values.
17 | 2. `.env.production`: duplicate `.env.example`, and set the variables to production values.
18 | 3. `wrangler.toml.development`: duplicate `wrangler.toml.example`, and set the variables to
19 | development values.
20 | 4. `wrangler.toml.production`: duplicate `wrangler.toml.example`, and set the variables to
21 | production values.
22 |
23 | 3. Install the app's dependencies:
24 |
25 | ```sh
26 | pnpm install
27 | ```
28 |
29 | 4. Generate db migration files (that documents schema changes in an SQL script).
30 |
31 | ```sh
32 | pnpm db:generate
33 | ```
34 |
35 | 5. Run db migrations (that executes the SQL script to update the database to match the schema).
36 |
37 | - dev (local) db: `pnpm db:migrate:dev`
38 | - prod (remote) db: `pnpm db:migrate:prod`
39 |
40 | 6. View the database using a graphical user interface:
41 |
42 | - dev (local) db: `pnpm db:studio:dev`
43 | - prod (remote) db: `pnpm db:studio:prod`
44 |
45 | ## Run the app
46 |
47 | - Run Next.js on dev. Ideal for development since it supports hot-reload/fast refresh.
48 |
49 | ```sh
50 | pnpm dev
51 | ```
52 |
53 | ⚠️ **Warning**: `next start` will return an error due to how the application is designed to run on
54 | Cloudflare pages.
55 |
56 | - Run Cloudflare Pages locally. Ideal to test how the app would work after being deployed.
57 |
58 | ```sh
59 | pnpm pages:dev
60 | ```
61 |
62 | ⚠️ **Warning #1**: Connecting to the prod remote db on the local code
63 | [is not supported](https://developers.cloudflare.com/d1/build-with-d1/local-development/).
64 |
65 | ⚠️ **Warning #2**: All pages deployed to Cloudflare Pages run on edge runtime, whereas
66 | [ISR only works on Nodejs runtime](https://developers.cloudflare.com/pages/framework-guides/nextjs/ssr/supported-features/)
67 | (because how Vercel designed their functions); so, some functions like `revalidatePath` will throw
68 | an error when running the app with `pnpm pages:dev`. But, the functions work as expected after
69 | deploying.
70 |
71 | ## Deploy
72 |
73 | - Deploy code to pages:
74 |
75 | ```sh
76 | pnpm pages:deploy
77 | ```
78 |
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'drizzle-kit';
2 |
3 | import { env } from '@/env';
4 |
5 | /*
6 | * NOTE: Workaround to make drizzle studio work with D1.
7 | * https://kevinkipp.com/blog/going-full-stack-on-astro-with-cloudflare-d1-and-drizzle/
8 | * Github discussion: https://github.com/drizzle-team/drizzle-orm/discussions/1545#discussioncomment-8115423
9 | */
10 | export default env.DB_LOCAL_PATH
11 | ? defineConfig({
12 | schema: './src/server/db/schema.ts',
13 | dialect: 'sqlite',
14 | dbCredentials: {
15 | url: env.DB_LOCAL_PATH,
16 | },
17 | })
18 | : defineConfig({
19 | schema: './src/server/db/schema.ts',
20 | out: './migrations',
21 | driver: 'd1-http',
22 | dialect: 'sqlite',
23 | dbCredentials: {
24 | accountId: env.CF_ACCOUNT_ID!,
25 | token: env.CF_USER_API_TOKEN!,
26 | databaseId: env.DB_REMOTE_DATABASE_ID!,
27 | },
28 | });
29 |
--------------------------------------------------------------------------------
/env.d.ts:
--------------------------------------------------------------------------------
1 | import type { D1Database } from '@cloudflare/workers-types';
2 |
3 | declare global {
4 | interface CloudflareEnv {
5 | DB: D1Database;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | // we import the utility from the next-dev submodule
2 | import { setupDevPlatform } from '@cloudflare/next-on-pages/next-dev';
3 |
4 | /** @type {import('next').NextConfig} */
5 | const nextConfig = {};
6 |
7 | export default nextConfig;
8 |
9 | // we only need to use the utility during development so we can check NODE_ENV
10 | // (note: this check is recommended but completely optional)
11 | if (process.env.NODE_ENV === 'development') {
12 | // `await`ing the call is not necessary but it helps making sure that the setup has succeeded.
13 | // If you cannot use top level awaits you could use the following to avoid an unhandled rejection:
14 | // `setupDevPlatform().catch(e => console.error(e));`
15 | await setupDevPlatform({ persist: true });
16 | }
17 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-d1-drizzle-cloudflare-pages",
3 | "version": "0.1.0",
4 | "private": true,
5 | "type": "module",
6 | "scripts": {
7 | "db:generate": "drizzle-kit generate",
8 | "db:migrate:dev": "wrangler d1 migrations apply nextjs-d1-drizzle-cloudflare-pages --local",
9 | "db:migrate:prod": "wrangler d1 migrations apply nextjs-d1-drizzle-cloudflare-pages --remote",
10 | "db:studio:dev": "pnpm setup:dev && cross-env DB_LOCAL_PATH=$(find .wrangler/state/v3/d1/miniflare-D1DatabaseObject -type f -name '*.sqlite' -print -quit) drizzle-kit studio --port 3094",
11 | "db:studio:prod": "pnpm setup:prod && drizzle-kit studio --port 3098",
12 | "build": "next build",
13 | "setup:dev": "cp .env.development .env && cp .env.development .env.production.local && cp wrangler.toml.development wrangler.toml",
14 | "setup:prod": "cp .env.production .env && cp .env.production .env.production.local && cp wrangler.toml.production wrangler.toml",
15 | "dev": "pnpm setup:dev && next dev",
16 | "pages:build": "pnpm next-on-pages",
17 | "pages:dev": "pnpm setup:dev && pnpm pages:build && pnpm wrangler pages dev --port=3000",
18 | "pages:deploy": "pnpm setup:prod && pnpm pages:build && pnpm wrangler pages deploy"
19 | },
20 | "dependencies": {
21 | "@tanstack/react-query": "^5.62.10",
22 | "next": "14.2.21",
23 | "react": "18.3.1",
24 | "react-dom": "18.3.1",
25 | "server-only": "^0.0.1",
26 | "superjson": "^2.2.2",
27 | "zod": "^3.24.1"
28 | },
29 | "devDependencies": {
30 | "@cloudflare/next-on-pages": "^1.13.7",
31 | "@cloudflare/workers-types": "^4.20241224.0",
32 | "@t3-oss/env-nextjs": "^0.11.1",
33 | "@types/better-sqlite3": "^7.6.12",
34 | "@types/eslint": "^8.56.2",
35 | "@types/node": "^22.10.2",
36 | "@types/react": "^18.3.1",
37 | "@types/react-dom": "^18.3.1",
38 | "@typescript-eslint/eslint-plugin": "^7.1.1",
39 | "@typescript-eslint/parser": "^7.1.1",
40 | "better-sqlite3": "^11.7.0",
41 | "cross-env": "^7.0.3",
42 | "drizzle-kit": "^0.23.0",
43 | "drizzle-orm": "^0.31.0",
44 | "eslint": "^8.57.0",
45 | "eslint-config-next": "^14.1.3",
46 | "eslint-plugin-drizzle": "^0.2.3",
47 | "postcss": "^8.4.49",
48 | "prettier": "^3.4.2",
49 | "prettier-plugin-tailwindcss": "^0.6.9",
50 | "tailwindcss": "^3.4.17",
51 | "typescript": "^5.7.2",
52 | "wrangler": "^3.99.0"
53 | },
54 | "packageManager": "pnpm@9.15.1"
55 | }
56 |
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: {
3 | tailwindcss: {},
4 | },
5 | };
6 |
7 | module.exports = config;
8 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | tabWidth: 2,
3 | useTabs: false,
4 | singleQuote: true,
5 | arrowParens: 'always',
6 | printWidth: 100,
7 | proseWrap: 'always',
8 | bracketSpacing: true,
9 | trailingComma: 'es5',
10 | semi: true,
11 | plugins: ['prettier-plugin-tailwindcss'],
12 | };
13 |
14 | export default config;
15 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
1 | h 6 ( � 00 h&