├── .nvmrc ├── public ├── robots.txt ├── site.webmanifest └── favicon.ico ├── src ├── @trpc │ └── next-layout │ │ ├── server │ │ ├── index.ts │ │ ├── local-storage.ts │ │ └── createTrpcNextLayout.tsx │ │ └── client │ │ ├── index.ts │ │ ├── createHydrateClient.tsx │ │ └── createTrpcNextBeta.tsx ├── styles │ └── globals.css ├── types │ └── common │ │ ├── package.json │ │ └── main.d.ts ├── app │ ├── (auth) │ │ ├── layout.tsx │ │ ├── sign-in │ │ │ └── page.tsx │ │ └── sign-up │ │ │ └── page.tsx │ ├── hello-from-client.tsx │ ├── client-providers.tsx │ ├── dashboard │ │ ├── loading.tsx │ │ ├── layout.tsx │ │ ├── page.tsx │ │ ├── note-card.tsx │ │ ├── create-note.tsx │ │ └── edit-note.tsx │ ├── layout.tsx │ └── page.tsx ├── lib │ ├── api │ │ ├── types.ts │ │ ├── server.ts │ │ └── client.ts │ └── utils.ts ├── server │ ├── api │ │ ├── root.ts │ │ ├── context.ts │ │ ├── trpc.ts │ │ └── routers │ │ │ └── example.ts │ └── db │ │ ├── index.ts │ │ └── schema.ts ├── components │ ├── feature-card.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── input.tsx │ │ ├── textarea.tsx │ │ ├── button.tsx │ │ └── dialog.tsx │ └── typography │ │ └── index.tsx ├── config │ └── site.ts ├── pages │ └── api │ │ └── trpc │ │ └── [trpc].ts ├── middleware.ts └── env.mjs ├── .prettierignore ├── postcss.config.cjs ├── .editorconfig ├── .env.example ├── drizzle.config.ts ├── next.config.mjs ├── .vscode └── settings.json ├── patches └── @tanstack__react-query@4.14.5.patch ├── .gitignore ├── tailwind.config.ts ├── prettier.config.cjs ├── tsconfig.json ├── .eslintrc.cjs ├── README.md └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.18.0 -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # * 2 | User-agent: * 3 | Allow: / -------------------------------------------------------------------------------- /src/@trpc/next-layout/server/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createTrpcNextLayout"; 2 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .next 2 | dist 3 | node_modules 4 | .env 5 | .env.example 6 | pnpm-lock.yaml 7 | README.md 8 | next-env.d.ts -------------------------------------------------------------------------------- /src/@trpc/next-layout/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./createTrpcNextBeta"; 2 | export * from "./createHydrateClient"; 3 | -------------------------------------------------------------------------------- /src/types/common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "common", 3 | "version": "1.0.0", 4 | "typings": "main.d.ts" 5 | } 6 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function Layout({ children }: PropsWithChildren) { 2 | return ( 3 |
4 | {children} 5 |
6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # App 2 | NEXT_PUBLIC_APP_URL=http://localhost:3000 3 | 4 | # Authentication (Clerk) 5 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= 6 | CLERK_SECRET_KEY= 7 | 8 | #Database 9 | DB_HOST= 10 | DB_USERNAME= 11 | DB_PASSWORD= 12 | DB_URL= -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import "dotenv/config"; 2 | import type { Config } from "drizzle-kit"; 3 | 4 | const config: Config = { 5 | schema: "./src/server/db/schema.ts", 6 | connectionString: process.env.DB_URL, 7 | }; 8 | 9 | export default config; 10 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Next App Router with TRPC and Drizzle on Edge", 3 | "short_name": "Next App Router with TRPC and Drizzle on Edge", 4 | "theme_color": "#ffffff", 5 | "background_color": "#ffffff", 6 | "display": "standalone" 7 | } 8 | -------------------------------------------------------------------------------- /src/types/common/main.d.ts: -------------------------------------------------------------------------------- 1 | type PropsWithChildren

= P & { children?: ReactNode }; 2 | type PropsWithClassName

= P & { className?: string }; 3 | type PropsWithChildrenAndClassName

= PropsWithClassName

& 4 | PropsWithChildren

; 5 | -------------------------------------------------------------------------------- /src/lib/api/types.ts: -------------------------------------------------------------------------------- 1 | import type { inferRouterInputs, inferRouterOutputs } from "@trpc/server"; 2 | import { type AppRouter } from "~/server/api/root"; 3 | 4 | export type RouterOutputs = inferRouterOutputs; 5 | export type RouterInputs = inferRouterInputs; 6 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs/app-beta"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | 7 | export const runtime = "experimental-edge"; 8 | export const revalidate = 0; 9 | -------------------------------------------------------------------------------- /src/app/(auth)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs/app-beta"; 2 | 3 | export default function Page() { 4 | return ; 5 | } 6 | 7 | export const runtime = "experimental-edge"; 8 | export const revalidate = 0; 9 | -------------------------------------------------------------------------------- /src/server/api/root.ts: -------------------------------------------------------------------------------- 1 | import { exampleRouter } from "~/server/api/routers/example"; 2 | import { createTRPCRouter } from "~/server/api/trpc"; 3 | 4 | export const appRouter = createTRPCRouter({ 5 | example: exampleRouter, 6 | }); 7 | 8 | // export type definition of API 9 | export type AppRouter = typeof appRouter; 10 | -------------------------------------------------------------------------------- /src/app/hello-from-client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { api } from "~/lib/api/client"; 4 | 5 | export default function HelloFromClient() { 6 | const { data, isLoading } = api.example.hello.useQuery({ 7 | text: "Test Client TRPC Call", 8 | }); 9 | 10 | if (isLoading) return <>Loading...; 11 | if (!data) return <>Error; 12 | 13 | return <>{data.greeting}; 14 | } 15 | -------------------------------------------------------------------------------- /src/server/db/index.ts: -------------------------------------------------------------------------------- 1 | import { connect } from "@planetscale/database"; 2 | import { drizzle } from "drizzle-orm/planetscale-serverless"; 3 | import { env } from "~/env.mjs"; 4 | 5 | const config = { 6 | host: env.DB_HOST, 7 | username: env.DB_USERNAME, 8 | password: env.DB_PASSWORD, 9 | }; 10 | 11 | const connection = connect(config); 12 | 13 | export const db = drizzle(connection); 14 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. 3 | * This is especially useful for Docker builds. 4 | */ 5 | !process.env.SKIP_ENV_VALIDATION && (await import("./src/env.mjs")); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = { 9 | reactStrictMode: true, 10 | experimental: { appDir: true, typedRoutes: true }, 11 | }; 12 | export default config; 13 | -------------------------------------------------------------------------------- /src/app/client-providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ClerkProvider } from "@clerk/nextjs/app-beta/client"; 4 | import { env } from "~/env.mjs"; 5 | import { api } from "~/lib/api/client"; 6 | 7 | export function ClientProviders({ children }: PropsWithChildren) { 8 | return ( 9 | 10 | {children} 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnPaste": true, 3 | "editor.formatOnSave": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true, 7 | "source.fixAll.format": true 8 | }, 9 | "css.lint.unknownAtRules": "ignore", 10 | "typescript.tsdk": "node_modules/.pnpm/typescript@5.0.4/node_modules/typescript/lib", 11 | "typescript.enablePromptUseWorkspaceTsdk": true 12 | } 13 | -------------------------------------------------------------------------------- /src/components/feature-card.tsx: -------------------------------------------------------------------------------- 1 | type Props = { 2 | title: string; 3 | description: string; 4 | href: string; 5 | }; 6 | 7 | export default function FeatureCard({ title, description, href }: Props) { 8 | return ( 9 | 15 | {title} 16 |

{description}

17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/server/db/schema.ts: -------------------------------------------------------------------------------- 1 | import { 2 | serial, 3 | text, 4 | timestamp, 5 | varchar, 6 | } from "drizzle-orm/mysql-core/columns"; 7 | import { mysqlTable } from "drizzle-orm/mysql-core/table"; 8 | 9 | export const notes = mysqlTable("notes", { 10 | id: serial("id").primaryKey(), 11 | user_id: varchar("user_id", { length: 191 }).notNull(), 12 | slug: varchar("slug", { length: 191 }).notNull(), 13 | title: text("title").notNull(), 14 | text: text("text").default(""), 15 | created_at: timestamp("created_at").notNull().defaultNow().onUpdateNow(), 16 | }); 17 | -------------------------------------------------------------------------------- /src/lib/api/server.ts: -------------------------------------------------------------------------------- 1 | import { auth as getAuth } from "@clerk/nextjs/app-beta"; 2 | import superjson from "superjson"; 3 | import { createTRPCNextLayout } from "~/@trpc/next-layout/server"; 4 | 5 | import "server-only"; 6 | import { createContextInner } from "~/server/api/context"; 7 | import { appRouter } from "~/server/api/root"; 8 | 9 | export const api = createTRPCNextLayout({ 10 | router: appRouter, 11 | transformer: superjson, 12 | createContext() { 13 | const auth = getAuth(); 14 | return createContextInner({ 15 | auth, 16 | req: null, 17 | }); 18 | }, 19 | }); 20 | -------------------------------------------------------------------------------- /patches/@tanstack__react-query@4.14.5.patch: -------------------------------------------------------------------------------- 1 | diff --git a/build/lib/reactBatchedUpdates.mjs b/build/lib/reactBatchedUpdates.mjs 2 | index 8a5ec0f3acd8582e6d63573a9479b9cae6b40f88..48c77d58736392bc3712651c978f4f5e48697993 100644 3 | --- a/build/lib/reactBatchedUpdates.mjs 4 | +++ b/build/lib/reactBatchedUpdates.mjs 5 | @@ -1,6 +1,6 @@ 6 | -import * as ReactDOM from 'react-dom'; 7 | - 8 | -const unstable_batchedUpdates = ReactDOM.unstable_batchedUpdates; 9 | +const unstable_batchedUpdates = (callback) => { 10 | + callback() 11 | +} 12 | 13 | export { unstable_batchedUpdates }; 14 | //# sourceMappingURL=reactBatchedUpdates.mjs.map -------------------------------------------------------------------------------- /src/config/site.ts: -------------------------------------------------------------------------------- 1 | export type SiteConfig = { 2 | name: string; 3 | description: string; 4 | url: string; 5 | links: { 6 | twitter: string; 7 | github: string; 8 | }; 9 | }; 10 | 11 | export const siteConfig: SiteConfig = { 12 | name: "Next App Router with TRPC and Drizzle on Edge", 13 | description: 14 | "T3 stack using app router with TRPC on edge, Drizzle Orm with PlanetScale serverless driver", 15 | url: "https://giga-stack.vercel.app", 16 | links: { 17 | twitter: "https://twitter.com/o_ploskovytskyy", 18 | github: 19 | "https://github.com/ploskovytskyy/next-app-router-trpc-drizzle-planetscale-edge", 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /src/app/dashboard/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | return ( 3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as LabelPrimitive from "@radix-ui/react-label"; 5 | import { cn } from "~/lib/utils"; 6 | 7 | const Label = React.forwardRef< 8 | React.ElementRef, 9 | React.ComponentPropsWithoutRef 10 | >(({ className, ...props }, ref) => ( 11 | 19 | )); 20 | Label.displayName = LabelPrimitive.Root.displayName; 21 | 22 | export { Label }; 23 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import slugifyjs from "slugify"; 3 | import { twMerge } from "tailwind-merge"; 4 | import { env } from "~/env.mjs"; 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)); 8 | } 9 | 10 | export function formatDate(input: string | number): string { 11 | const date = new Date(input); 12 | return date.toLocaleDateString("en-US", { 13 | month: "long", 14 | day: "numeric", 15 | year: "numeric", 16 | }); 17 | } 18 | 19 | export function absoluteUrl(path: string) { 20 | return `${env.NEXT_PUBLIC_APP_URL}${path}`; 21 | } 22 | 23 | export function slugify(string: string) { 24 | return slugifyjs(string, { lower: true }); 25 | } 26 | -------------------------------------------------------------------------------- /.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 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # local env files 34 | # 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 35 | .env 36 | .env*.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | # typescript 42 | *.tsbuildinfo 43 | -------------------------------------------------------------------------------- /src/@trpc/next-layout/client/createHydrateClient.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useMemo } from "react"; 4 | import { Hydrate, type DehydratedState } from "@tanstack/react-query"; 5 | import { type DataTransformer } from "@trpc/server"; 6 | 7 | export function createHydrateClient(opts: { transformer?: DataTransformer }) { 8 | return function HydrateClient(props: { 9 | children: React.ReactNode; 10 | state: DehydratedState; 11 | }) { 12 | const { state, children } = props; 13 | 14 | const transformedState: DehydratedState = useMemo(() => { 15 | if (opts.transformer) { 16 | return opts.transformer.deserialize(state) as DehydratedState; 17 | } 18 | return state; 19 | }, [state]); 20 | 21 | return {children}; 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { UserButton } from "@clerk/nextjs/app-beta"; 3 | import { Home } from "lucide-react"; 4 | 5 | export default function Layout({ children }: PropsWithChildren) { 6 | return ( 7 |
8 |
9 | Dashboard 10 |
11 | 15 | 16 | 17 | 18 |
19 |
20 | {children} 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/@trpc/next-layout/server/local-storage.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unsafe-return */ 2 | /** 3 | * This file makes sure that we can get a storage that is unique to the current request context 4 | */ 5 | 6 | import { type AsyncLocalStorage } from "async_hooks"; 7 | import { requestAsyncStorage as asyncStorage } from "next/dist/client/components/request-async-storage"; 8 | 9 | function throwError(msg: string) { 10 | throw new Error(msg); 11 | } 12 | 13 | export function getRequestStorage(): T { 14 | if ("getStore" in asyncStorage) { 15 | return ( 16 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 17 | (asyncStorage as AsyncLocalStorage).getStore() ?? 18 | throwError("Couldn't get async storage") 19 | ); 20 | } 21 | 22 | return asyncStorage as T; 23 | } 24 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "~/lib/utils"; 3 | 4 | export type InputProps = React.InputHTMLAttributes; 5 | 6 | const Input = React.forwardRef( 7 | ({ className, ...props }, ref) => { 8 | return ( 9 | 17 | ); 18 | } 19 | ); 20 | Input.displayName = "Input"; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { cn } from "~/lib/utils"; 3 | 4 | export type TextareaProps = React.TextareaHTMLAttributes; 5 | 6 | const Textarea = React.forwardRef( 7 | ({ className, ...props }, ref) => { 8 | return ( 9 |