├── .gitignore ├── prisma ├── zod │ ├── index.ts │ ├── post.ts │ └── user.ts └── schema.prisma ├── bun.lockb ├── .gitattributes ├── postcss.config.cjs ├── src ├── lib │ ├── utils.ts │ ├── animated.ts │ └── getStripe.ts ├── components │ ├── theme-provider.tsx │ └── ui │ │ └── button.tsx ├── middleware.ts ├── app │ ├── u │ │ └── [userId] │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ ├── (clerk) │ │ ├── sign-in │ │ │ ├── sso-callback │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ ├── sign-up │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── api │ │ ├── trpc │ │ │ └── [trpc] │ │ │ │ └── route.ts │ │ ├── stripe │ │ │ └── webhooks │ │ │ │ └── route.ts │ │ └── clerk │ │ │ └── route.ts │ ├── _components │ │ ├── create-post.tsx │ │ └── loading.tsx │ ├── layout.tsx │ └── (marketing) │ │ └── page.tsx ├── server │ ├── db.ts │ └── api │ │ ├── root.ts │ │ ├── routers │ │ ├── post.ts │ │ └── stripe.ts │ │ └── trpc.ts ├── trpc │ ├── server.ts │ ├── shared.ts │ └── react.tsx ├── styles │ └── globals.css └── env.mjs ├── prettier.config.mjs ├── next-env.d.ts ├── next.config.mjs ├── components.json ├── tsconfig.json ├── README.md ├── .env.example ├── package.json ├── tailwind.config.ts └── public └── favicon.ico /.gitignore: -------------------------------------------------------------------------------- 1 | /.env.local 2 | node_modules 3 | .next -------------------------------------------------------------------------------- /prisma/zod/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./post" 2 | export * from "./user" 3 | -------------------------------------------------------------------------------- /bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GrandeVx/My-T3-Boilerplate/HEAD/bun.lockb -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | 8 | module.exports = config; 9 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 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 | } 7 | -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config & import('prettier-plugin-tailwindcss').options} */ 2 | const config = { 3 | plugins: ["prettier-plugin-tailwindcss"], 4 | }; 5 | 6 | export default config; 7 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially useful 3 | * for Docker builds. 4 | */ 5 | await import("./src/env.mjs"); 6 | 7 | /** @type {import("next").NextConfig} */ 8 | const config = {}; 9 | 10 | export default config; 11 | -------------------------------------------------------------------------------- /src/lib/animated.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { motion } from "framer-motion"; 3 | 4 | export const Animated_h1 = motion.h1; 5 | export const Animated_h2 = motion.h2; 6 | 7 | export const Animated_p = motion.p; 8 | export const Animated_span = motion.span; 9 | export const Animated_div = motion.div; 10 | -------------------------------------------------------------------------------- /src/lib/getStripe.ts: -------------------------------------------------------------------------------- 1 | import { Stripe, loadStripe } from "@stripe/stripe-js"; 2 | 3 | let stripePromise: Promise; 4 | const getStripe = () => { 5 | if (!stripePromise) { 6 | stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!); 7 | } 8 | return stripePromise; 9 | }; 10 | 11 | export default getStripe; 12 | -------------------------------------------------------------------------------- /src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 5 | import { type ThemeProviderProps } from "next-themes/dist/types"; 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children}; 9 | } 10 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@clerk/nextjs"; 2 | 3 | export default authMiddleware({ 4 | publicRoutes: [ 5 | "/api/trpc(.*)", 6 | "/", 7 | "/u/(.*)", 8 | "/signin(.*)", 9 | "/sso-callback(.*)", 10 | ], 11 | ignoredRoutes: ["/api/clerk(.*)", "/api/stripe/webhooks(.*)"], 12 | }); 13 | 14 | export const config = { 15 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 16 | }; 17 | -------------------------------------------------------------------------------- /src/app/u/[userId]/layout.tsx: -------------------------------------------------------------------------------- 1 | export default function UserPostLayout({ 2 | children, 3 | }: { 4 | children: React.ReactNode; 5 | }) { 6 | return ( 7 |
8 |
9 | {children} 10 |
11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/server/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | import { env } from "@/env.mjs"; 4 | 5 | const globalForPrisma = globalThis as unknown as { 6 | prisma: PrismaClient | undefined; 7 | }; 8 | 9 | export const db = 10 | globalForPrisma.prisma ?? 11 | new PrismaClient({ 12 | log: 13 | env.NODE_ENV === "development" ? ["query", "error", "warn"] : ["error"], 14 | }); 15 | 16 | if (env.NODE_ENV !== "production") globalForPrisma.prisma = db; 17 | -------------------------------------------------------------------------------- /src/server/api/root.ts: -------------------------------------------------------------------------------- 1 | import { postRouter } from "@/server/api/routers/post"; 2 | import { createTRPCRouter } from "@/server/api/trpc"; 3 | import { stripeRouter } from "./routers/stripe"; 4 | 5 | /** 6 | * This is the primary router for your server. 7 | * 8 | * All routers added in /api/routers should be manually added here. 9 | */ 10 | export const appRouter = createTRPCRouter({ 11 | post: postRouter, 12 | stripe: stripeRouter, 13 | }); 14 | 15 | // export type definition of API 16 | export type AppRouter = typeof appRouter; 17 | -------------------------------------------------------------------------------- /src/app/(clerk)/sign-in/sso-callback/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useEffect } from "react"; 4 | import { useClerk } from "@clerk/nextjs"; 5 | import type { HandleOAuthCallbackParams } from "@clerk/types"; 6 | 7 | export default function SSOCallback(props: { 8 | searchParams: HandleOAuthCallbackParams; 9 | }) { 10 | const { handleRedirectCallback } = useClerk(); 11 | 12 | useEffect(() => { 13 | void handleRedirectCallback(props.searchParams); 14 | }, [props.searchParams, handleRedirectCallback]); 15 | 16 | return ( 17 |
18 |

Loading...

19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /prisma/zod/post.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { Completeuser, relateduserSchema } from "./index" 3 | 4 | export const postSchema = z.object({ 5 | id: z.number().int(), 6 | name: z.string(), 7 | user_id: z.string(), 8 | email: z.string(), 9 | createdAt: z.date(), 10 | updatedAt: z.date(), 11 | }) 12 | 13 | export interface CompletePost extends z.infer { 14 | User: Completeuser 15 | } 16 | 17 | /** 18 | * relatedPostSchema contains all relations on your model in addition to the scalars 19 | * 20 | * NOTE: Lazy required in case of potential circular dependencies within schema 21 | */ 22 | export const relatedPostSchema: z.ZodSchema = z.lazy(() => postSchema.extend({ 23 | User: relateduserSchema, 24 | })) 25 | -------------------------------------------------------------------------------- /src/trpc/server.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createTRPCProxyClient, 3 | loggerLink, 4 | unstable_httpBatchStreamLink, 5 | } from "@trpc/client"; 6 | import { cookies } from "next/headers"; 7 | 8 | import { type AppRouter } from "@/server/api/root"; 9 | import { getUrl, transformer } from "./shared"; 10 | 11 | export const api = createTRPCProxyClient({ 12 | transformer, 13 | links: [ 14 | loggerLink({ 15 | enabled: (op) => 16 | process.env.NODE_ENV === "development" || 17 | (op.direction === "down" && op.result instanceof Error), 18 | }), 19 | unstable_httpBatchStreamLink({ 20 | url: getUrl(), 21 | headers() { 22 | return { 23 | cookie: cookies().toString(), 24 | "x-trpc-source": "rsc", 25 | }; 26 | }, 27 | }), 28 | ], 29 | }); 30 | -------------------------------------------------------------------------------- /src/app/api/trpc/[trpc]/route.ts: -------------------------------------------------------------------------------- 1 | import { fetchRequestHandler } from "@trpc/server/adapters/fetch"; 2 | import { type NextRequest } from "next/server"; 3 | 4 | import { env } from "@/env.mjs"; 5 | import { appRouter } from "@/server/api/root"; 6 | import { createTRPCContext } from "@/server/api/trpc"; 7 | 8 | const handler = (req: NextRequest) => 9 | fetchRequestHandler({ 10 | endpoint: "/api/trpc", 11 | req, 12 | router: appRouter, 13 | createContext: () => createTRPCContext({ req }), 14 | onError: 15 | env.NODE_ENV === "development" 16 | ? ({ path, error }) => { 17 | console.error( 18 | `❌ tRPC failed on ${path ?? ""}: ${error.message}` 19 | ); 20 | } 21 | : undefined, 22 | }); 23 | 24 | export { handler as GET, handler as POST }; 25 | -------------------------------------------------------------------------------- /src/app/(clerk)/sign-up/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { SignOutButton } from "@clerk/nextjs"; 5 | 6 | import { Button } from "@/components/ui/button"; 7 | 8 | export default function AuthenticationPage() { 9 | const router = useRouter(); 10 | 11 | return ( 12 |
13 |
14 |

Sign Out

15 |

16 | Are you sure you want to sign out? 17 |

18 | router.push("/?redirect=false")}> 19 | 20 | 21 |
22 |
23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /prisma/zod/user.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | import { CompletePost, relatedPostSchema } from "./index" 3 | 4 | export const userSchema = z.object({ 5 | id: z.string(), 6 | email: z.string(), 7 | stripeCustomerId: z.string().nullish(), 8 | firstName: z.string(), 9 | lastName: z.string(), 10 | profileImageUrl: z.string().nullish(), 11 | isPremium: z.boolean(), 12 | PremiumUntil: z.date().nullish(), 13 | createdAt: z.date(), 14 | updatedAt: z.date(), 15 | }) 16 | 17 | export interface Completeuser extends z.infer { 18 | Posts: CompletePost[] 19 | } 20 | 21 | /** 22 | * relateduserSchema contains all relations on your model in addition to the scalars 23 | * 24 | * NOTE: Lazy required in case of potential circular dependencies within schema 25 | */ 26 | export const relateduserSchema: z.ZodSchema = z.lazy(() => userSchema.extend({ 27 | Posts: relatedPostSchema.array(), 28 | })) 29 | -------------------------------------------------------------------------------- /src/trpc/shared.ts: -------------------------------------------------------------------------------- 1 | import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server"; 2 | import superjson from "superjson"; 3 | 4 | import { type AppRouter } from "@/server/api/root"; 5 | 6 | export const transformer = superjson; 7 | 8 | function getBaseUrl() { 9 | if (typeof window !== "undefined") return ""; 10 | if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; 11 | return `http://localhost:${process.env.PORT ?? 3000}`; 12 | } 13 | 14 | export function getUrl() { 15 | return getBaseUrl() + "/api/trpc"; 16 | } 17 | 18 | /** 19 | * Inference helper for inputs. 20 | * 21 | * @example type HelloInput = RouterInputs['example']['hello'] 22 | */ 23 | export type RouterInputs = inferRouterInputs; 24 | 25 | /** 26 | * Inference helper for outputs. 27 | * 28 | * @example type HelloOutput = RouterOutputs['example']['hello'] 29 | */ 30 | export type RouterOutputs = inferRouterOutputs; 31 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Base Options: */ 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "allowJs": true, 8 | "resolveJsonModule": true, 9 | "moduleDetection": "force", 10 | "isolatedModules": true, 11 | 12 | /* Strictness */ 13 | "strict": true, 14 | "noUncheckedIndexedAccess": true, 15 | "checkJs": true, 16 | 17 | /* Bundled projects */ 18 | "lib": ["dom", "dom.iterable", "ES2022"], 19 | "noEmit": true, 20 | "module": "ESNext", 21 | "moduleResolution": "Bundler", 22 | "jsx": "preserve", 23 | "plugins": [{ "name": "next" }], 24 | "incremental": true, 25 | 26 | /* Path Aliases */ 27 | "baseUrl": ".", 28 | "paths": { 29 | "@/*": ["./src/*"] 30 | } 31 | }, 32 | "include": [ 33 | ".eslintrc.cjs", 34 | "next-env.d.ts", 35 | "**/*.ts", 36 | "**/*.tsx", 37 | "**/*.cjs", 38 | "**/*.mjs", 39 | ".next/types/**/*.ts" 40 | ], 41 | "exclude": ["node_modules"] 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # My-T3-Boilerplate 2 | 3 | ## Description 4 | Every time i try to start a new project i have to setup the same things over and over again (and every time something decide to not work). So i decided to create a boilerplate for myself. This boilerplate is based on the [T3 Stack](https://t3.gg/) and uses [Next.js](https://nextjs.org/) as a frontend framework. 5 | 6 | ## Features 7 | - [x] [Next.js](https://nextjs.org/) 8 | - [x] [Prisma](https://www.prisma.io/) 9 | - [x] [Clerk](https://clerk.dev/) 10 | - [x] [TRPC](https://trpc.io/) 11 | - [x] [Stripe](https://stripe.com/) 12 | - [x] [Tailwind CSS](https://tailwindcss.com/) 13 | - [x] [ESLint](https://eslint.org/) 14 | - [x] [Prettier](https://prettier.io/) 15 | 16 | ## Getting Started 17 | 18 | - I used bun as a package manager, but you can use npm or yarn if you want 19 | - You need to have a Clerk account and a Stripe account 20 | - You need to have a db setup (i used mysql hosted by PlanetScale) 21 | 22 | 23 | T3 Stack app with - Next.js 14 - Clerk Auth - TRPC - Stripe - Prisma 24 | -------------------------------------------------------------------------------- /src/app/u/[userId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { api } from "@/trpc/server"; 2 | import Link from "next/link"; 3 | 4 | export default async function ClerkUserPosts({ 5 | params, 6 | }: { 7 | params: { userId: string }; 8 | }) { 9 | const userPosts = await api.post.getPostsByUser.query({ 10 | userId: params.userId, 11 | }); 12 | if (!userPosts || userPosts.length === 0) { 13 | return ( 14 |
15 |
No posts found by this user
16 | ← Back to home 17 |
18 | ); 19 | } 20 | return ( 21 |
22 | ← Back to home 23 |
    24 | {userPosts.map((post, index) => ( 25 |
  • 26 |

    {`Posted by ${ 27 | post.email 28 | } on ${post.createdAt.toLocaleDateString()}`}

    29 |

    {post.name}

    30 |
  • 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /.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 this file up-to-date 3 | # when you add new variables to `.env`. 4 | 5 | # This 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 this file named 7 | # ".env" and populate it with your secrets. 8 | 9 | # When adding additional environment variables, the schema in "/src/env.mjs" 10 | # should be updated accordingly. 11 | 12 | # Prisma 13 | # https://www.prisma.io/docs/reference/database-reference/connection-urls#env 14 | DATABASE_URL= 15 | 16 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY= # clerk publishable key 17 | CLERK_SECRET_KEY= # clerk secret key 18 | WEBHOOK_SECRET= # clerk webhook secret 19 | 20 | STRIPE_SECRET_KEY= # stripe secret key 21 | STRIPE_WEBHOOK_SECRET= # stripe webhook secret 22 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= # stripe publishable key 23 | 24 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 25 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 26 | NEXT_PUBLIC_WEBSITE_URL=http://localhost:3000 # remove this line if you are using vercel and add it to the vercel env variables 27 | -------------------------------------------------------------------------------- /src/app/_components/create-post.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useState } from "react"; 5 | 6 | import { api } from "@/trpc/react"; 7 | 8 | export function CreatePost() { 9 | const router = useRouter(); 10 | const [name, setName] = useState(""); 11 | 12 | const createPost = api.post.create.useMutation({ 13 | onSuccess: () => { 14 | router.refresh(); 15 | setName(""); 16 | }, 17 | }); 18 | 19 | return ( 20 |
{ 22 | e.preventDefault(); 23 | createPost.mutate({ name }); 24 | }} 25 | className="flex flex-row gap-2" 26 | > 27 | setName(e.target.value)} 32 | className="w-full rounded-full px-4 py-2 text-black" 33 | /> 34 | 41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css"; 2 | 3 | import { Inter } from "next/font/google"; 4 | import { cookies } from "next/headers"; 5 | 6 | import { TRPCReactProvider } from "@/trpc/react"; 7 | import { ThemeProvider } from "@/components/theme-provider"; 8 | import { ClerkProvider } from "@clerk/nextjs"; 9 | 10 | const inter = Inter({ 11 | subsets: ["latin"], 12 | variable: "--font-sans", 13 | }); 14 | 15 | export const metadata = { 16 | title: "Create T3 App", 17 | description: "Generated by create-t3-app", 18 | icons: [{ rel: "icon", url: "/favicon.ico" }], 19 | }; 20 | 21 | export default function RootLayout({ 22 | children, 23 | }: { 24 | children: React.ReactNode; 25 | }) { 26 | return ( 27 | 28 | 29 | 35 | 36 | 37 | {children} 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/app/(clerk)/sign-in/page.tsx: -------------------------------------------------------------------------------- 1 | import type { Route } from "next"; 2 | import Link from "next/link"; 3 | import { SignIn } from "@clerk/nextjs"; 4 | 5 | export default function AuthenticationPage() { 6 | return ( 7 |
8 |
9 |

10 | Create an account 11 |

12 |

13 | Enter your email below to create your account 14 |

15 |
16 | 17 | 18 |

19 | By clicking continue, you agree to our{" "} 20 | 24 | Terms of Service 25 | {" "} 26 | and{" "} 27 | 31 | Privacy Policy 32 | 33 | . 34 |

35 |
36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | generator zod { 9 | provider = "zod-prisma" 10 | output = "./zod" 11 | relationModel = true 12 | modelCase = "camelCase" 13 | modelSuffix = "Schema" 14 | useDecimalJs = true 15 | prismaJsonNullability = true 16 | } 17 | 18 | datasource db { 19 | provider = "mysql" 20 | relationMode = "prisma" 21 | url = env("DATABASE_URL") 22 | } 23 | 24 | model Post { 25 | id Int @id @default(autoincrement()) 26 | 27 | name String 28 | user_id String 29 | email String 30 | 31 | User user @relation(fields: [user_id], references: [id]) 32 | createdAt DateTime @default(now()) 33 | updatedAt DateTime @updatedAt 34 | @@index([name, user_id]) 35 | } 36 | 37 | model user { 38 | id String @id @unique 39 | email String @unique 40 | stripeCustomerId String? @unique 41 | firstName String 42 | lastName String 43 | profileImageUrl String? 44 | 45 | Posts Post[] 46 | 47 | isPremium Boolean @default(false) 48 | PremiumUntil DateTime? 49 | 50 | createdAt DateTime @default(now()) 51 | updatedAt DateTime @updatedAt 52 | 53 | @@index([id]) 54 | } 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/app/(clerk)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import Icon from "@/../public/favicon.ico"; 4 | import Image from "next/image"; 5 | 6 | export default function AuthLayout(props: { children: React.ReactNode }) { 7 | return ( 8 | <> 9 |
10 |
11 |
18 |
19 | 23 | logo 24 | T3 Stack Template 25 | 26 |
27 | 28 |
29 | {props.children} 30 |
31 |
32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /src/trpc/react.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; 4 | import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client"; 5 | import { createTRPCReact } from "@trpc/react-query"; 6 | import { useState } from "react"; 7 | 8 | import { type AppRouter } from "@/server/api/root"; 9 | import { getUrl, transformer } from "./shared"; 10 | 11 | export const api = createTRPCReact(); 12 | 13 | export function TRPCReactProvider(props: { 14 | children: React.ReactNode; 15 | cookies: string; 16 | }) { 17 | const [queryClient] = useState(() => new QueryClient()); 18 | 19 | const [trpcClient] = useState(() => 20 | api.createClient({ 21 | transformer, 22 | links: [ 23 | loggerLink({ 24 | enabled: (op) => 25 | process.env.NODE_ENV === "development" || 26 | (op.direction === "down" && op.result instanceof Error), 27 | }), 28 | unstable_httpBatchStreamLink({ 29 | url: getUrl(), 30 | headers() { 31 | return { 32 | cookie: props.cookies, 33 | "x-trpc-source": "react", 34 | }; 35 | }, 36 | }), 37 | ], 38 | }) 39 | ); 40 | 41 | return ( 42 | 43 | 44 | {props.children} 45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/server/api/routers/post.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | import { 4 | createTRPCRouter, 5 | publicProcedure, 6 | protectedProcedure, 7 | } from "@/server/api/trpc"; 8 | import { currentUser } from "@clerk/nextjs"; 9 | 10 | export const postRouter = createTRPCRouter({ 11 | hello: publicProcedure 12 | .input(z.object({ text: z.string() })) 13 | .query(({ input }) => { 14 | return { 15 | greeting: `Hello ${input.text}`, 16 | }; 17 | }), 18 | 19 | // This is a protected procedure, meaning that the user must be logged in to access it. If they try to 20 | // access it without being logged in, they will get an UNAUTHORIZED error. 21 | create: protectedProcedure 22 | .input(z.object({ name: z.string().min(1) })) 23 | .mutation(async ({ ctx, input }) => { 24 | const user = await currentUser(); 25 | 26 | return ctx.db.post.create({ 27 | data: { 28 | name: input.name, 29 | user_id: ctx.session.userId, 30 | email: user?.emailAddresses[0]?.emailAddress ?? "", 31 | }, 32 | }); 33 | }), 34 | 35 | getPostsByUser: publicProcedure 36 | .input(z.object({ userId: z.string() })) 37 | .query(async ({ ctx, input }) => { 38 | return ctx.db.post.findMany({ 39 | where: { 40 | user_id: input.userId, 41 | }, 42 | orderBy: { createdAt: "desc" }, 43 | }); 44 | }), 45 | 46 | getAllPosts: publicProcedure.query(({ ctx }) => { 47 | return ctx.db.post.findMany({ 48 | orderBy: { createdAt: "desc" }, 49 | }); 50 | }), 51 | }); 52 | -------------------------------------------------------------------------------- /src/app/_components/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function LoadingComponent() { 2 | return ( 3 |
4 |
8 | 24 | Loading General 25 |
26 |
27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /src/app/api/stripe/webhooks/route.ts: -------------------------------------------------------------------------------- 1 | import { db } from "@/server/db"; 2 | import { IncomingMessage } from "http"; 3 | import { buffer } from "micro"; 4 | import Stripe from "stripe"; 5 | 6 | const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET!; 7 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 8 | apiVersion: "2023-10-16", 9 | }); 10 | 11 | type StripeMetadata = { 12 | clerkEmailAddress: string; 13 | clerkFullName: string; 14 | clerkId: string; 15 | }; 16 | 17 | // remember to add your_url/api/stripe/webhooks to the stripe dashboard 18 | 19 | export async function POST(req: Request) { 20 | const body = await req.text(); 21 | const sig = req.headers.get("stripe-signature") as string; 22 | let event: Stripe.Event; 23 | 24 | try { 25 | if (!sig || !webhookSecret) return; 26 | event = stripe.webhooks.constructEvent(body, sig, webhookSecret); 27 | } catch (err: any) { 28 | console.log(`❌ Error message: ${err.message}`); 29 | return new Response(`Webhook Error: ${err.message}`, { status: 400 }); 30 | } 31 | 32 | if (event.type === "checkout.session.completed") { 33 | const paymentIntent = event.data.object as Stripe.Checkout.Session; 34 | console.log( 35 | `🔔 Stripe PaymentIntent status 💸: ${paymentIntent.payment_status}`, 36 | ); 37 | const { clerkId, clerkFullName, clerkEmailAddress } = 38 | paymentIntent.metadata as StripeMetadata; 39 | 40 | db.user.update({ 41 | where: { id: clerkId }, 42 | data: { 43 | isPremium: true, 44 | PremiumUntil: new Date( 45 | new Date().setMonth(new Date().getMonth() + 1), 46 | ).toISOString(), 47 | }, 48 | }); 49 | } else 50 | console.warn(`💸 Stripe Webhook : Unhandled event type: ${event.type}`); 51 | 52 | return new Response(JSON.stringify({ received: true })); 53 | } 54 | 55 | // export 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "t3-app-router-clerk", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "build": "next build", 7 | "db:push": "prisma db push", 8 | "db:studio": "prisma studio", 9 | "dev": "next dev", 10 | "postinstall": "prisma generate", 11 | "lint": "next lint", 12 | "start": "next start" 13 | }, 14 | "dependencies": { 15 | "@clerk/nextjs": "^4.27.1", 16 | "@prisma/client": "^5.1.1", 17 | "@stripe/stripe-js": "^2.2.0", 18 | "@t3-oss/env-nextjs": "^0.7.0", 19 | "@tanstack/react-query": "^4.32.6", 20 | "@trpc/client": "^10.37.1", 21 | "@trpc/next": "^10.37.1", 22 | "@trpc/react-query": "^10.37.1", 23 | "@trpc/server": "^10.37.1", 24 | "clsx": "^2.0.0", 25 | "framer-motion": "^10.16.5", 26 | "micro": "^10.0.1", 27 | "next": "^14.0.0", 28 | "next-themes": "^0.2.1", 29 | "react": "18.2.0", 30 | "react-dom": "18.2.0", 31 | "react-wrap-balancer": "^1.1.0", 32 | "stripe": "^14.5.0", 33 | "superjson": "^1.13.1", 34 | "svix": "^1.14.0", 35 | "tailwind-merge": "^2.0.0", 36 | "tailwindcss-animate": "^1.0.7", 37 | "zod": "^3.22.4", 38 | "zod-prisma": "^0.5.4" 39 | }, 40 | "devDependencies": { 41 | "@types/eslint": "^8.44.2", 42 | "@types/node": "^18.16.0", 43 | "@types/react": "^18.2.33", 44 | "@types/react-dom": "^18.2.14", 45 | "@typescript-eslint/eslint-plugin": "^6.3.0", 46 | "@typescript-eslint/parser": "^6.3.0", 47 | "autoprefixer": "^10.4.14", 48 | "eslint": "^8.47.0", 49 | "eslint-config-next": "^14.0.0", 50 | "postcss": "^8.4.27", 51 | "prettier": "^3.0.0", 52 | "prettier-plugin-tailwindcss": "^0.5.1", 53 | "prisma": "^5.1.1", 54 | "tailwindcss": "^3.3.3", 55 | "typescript": "^5.1.6" 56 | }, 57 | "ct3aMetadata": { 58 | "initVersion": "7.23.2" 59 | }, 60 | "packageManager": "npm@10.2.0" 61 | } 62 | -------------------------------------------------------------------------------- /src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | darkMode: ["class"], 4 | content: [ 5 | "./pages/**/*.{ts,tsx}", 6 | "./components/**/*.{ts,tsx}", 7 | "./app/**/*.{ts,tsx}", 8 | "./src/**/*.{ts,tsx}", 9 | ], 10 | theme: { 11 | container: { 12 | center: true, 13 | padding: "2rem", 14 | screens: { 15 | "2xl": "1400px", 16 | }, 17 | }, 18 | extend: { 19 | colors: { 20 | border: "hsl(var(--border))", 21 | input: "hsl(var(--input))", 22 | ring: "hsl(var(--ring))", 23 | background: "hsl(var(--background))", 24 | foreground: "hsl(var(--foreground))", 25 | primary: { 26 | DEFAULT: "hsl(var(--primary))", 27 | foreground: "hsl(var(--primary-foreground))", 28 | }, 29 | secondary: { 30 | DEFAULT: "hsl(var(--secondary))", 31 | foreground: "hsl(var(--secondary-foreground))", 32 | }, 33 | destructive: { 34 | DEFAULT: "hsl(var(--destructive))", 35 | foreground: "hsl(var(--destructive-foreground))", 36 | }, 37 | muted: { 38 | DEFAULT: "hsl(var(--muted))", 39 | foreground: "hsl(var(--muted-foreground))", 40 | }, 41 | accent: { 42 | DEFAULT: "hsl(var(--accent))", 43 | foreground: "hsl(var(--accent-foreground))", 44 | }, 45 | popover: { 46 | DEFAULT: "hsl(var(--popover))", 47 | foreground: "hsl(var(--popover-foreground))", 48 | }, 49 | card: { 50 | DEFAULT: "hsl(var(--card))", 51 | foreground: "hsl(var(--card-foreground))", 52 | }, 53 | }, 54 | borderRadius: { 55 | lg: "var(--radius)", 56 | md: "calc(var(--radius) - 2px)", 57 | sm: "calc(var(--radius) - 4px)", 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate")], 76 | }; 77 | -------------------------------------------------------------------------------- /src/env.mjs: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | /** 6 | * Specify your server-side environment variables schema here. This way you can ensure the app 7 | * isn't built with invalid env vars. 8 | */ 9 | server: { 10 | DATABASE_URL: z 11 | .string() 12 | .url() 13 | .refine( 14 | (str) => !str.includes("YOUR_MYSQL_URL_HERE"), 15 | "You forgot to change the default URL", 16 | ), 17 | NODE_ENV: z 18 | .enum(["development", "test", "production"]) 19 | .default("development"), 20 | CLERK_SECRET_KEY: z.string(), 21 | WEBHOOK_SECRET: z.string(), 22 | STRIPE_SECRET_KEY: z.string(), 23 | STRIPE_WEBHOOK_SECRET: z.string(), 24 | }, 25 | 26 | /** 27 | * Specify your client-side environment variables schema here. This way you can ensure the app 28 | * isn't built with invalid env vars. To expose them to the client, prefix them with 29 | * `NEXT_PUBLIC_`. 30 | */ 31 | client: { 32 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: z.string(), 33 | NEXT_PUBLIC_CLERK_SIGN_IN_URL: z.string(), 34 | NEXT_PUBLIC_CLERK_SIGN_UP_URL: z.string(), 35 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string(), 36 | NEXT_PUBLIC_WEBSITE_URL: z.string(), 37 | }, 38 | 39 | /** 40 | * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. 41 | * middlewares) or client-side so we need to destruct manually. 42 | */ 43 | runtimeEnv: { 44 | DATABASE_URL: process.env.DATABASE_URL, 45 | NODE_ENV: process.env.NODE_ENV, 46 | CLERK_SECRET_KEY: process.env.CLERK_SECRET_KEY, 47 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: 48 | process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, 49 | WEBHOOK_SECRET: process.env.WEBHOOK_SECRET, 50 | NEXT_PUBLIC_CLERK_SIGN_IN_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_IN_URL, 51 | NEXT_PUBLIC_CLERK_SIGN_UP_URL: process.env.NEXT_PUBLIC_CLERK_SIGN_UP_URL, 52 | STRIPE_SECRET_KEY: process.env.STRIPE_SECRET_KEY, 53 | STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, 54 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: 55 | process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY, 56 | NEXT_PUBLIC_WEBSITE_URL: process.env.NEXT_PUBLIC_WEBSITE_URL, 57 | 58 | // NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR, 59 | }, 60 | /** 61 | * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially 62 | * useful for Docker builds. 63 | */ 64 | skipValidation: !!process.env.SKIP_ENV_VALIDATION, 65 | /** 66 | * Makes it so that empty strings are treated as undefined. 67 | * `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error. 68 | */ 69 | emptyStringAsUndefined: true, 70 | }); 71 | -------------------------------------------------------------------------------- /src/server/api/routers/stripe.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | import Stripe from "stripe"; 3 | import { 4 | createTRPCRouter, 5 | publicProcedure, 6 | protectedProcedure, 7 | } from "@/server/api/trpc"; 8 | import { currentUser } from "@clerk/nextjs"; 9 | import { TRPCError } from "@trpc/server"; 10 | 11 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 12 | apiVersion: "2023-10-16", 13 | }); 14 | 15 | /* 16 | In This file you can add another procedure like this: 17 | Remove Subscription -> andrà a rimuovere la sottoscrizione dell'utente ma l'utente rimarrà premium fino alla fine del periodo 18 | ecc... 19 | */ 20 | 21 | export const stripeRouter = createTRPCRouter({ 22 | getCheckoutSession: protectedProcedure 23 | .input(z.object({ productId: z.string() })) 24 | .mutation(async ({ ctx, input }) => { 25 | const user = await currentUser(); 26 | 27 | if (!user) { 28 | throw new TRPCError({ 29 | code: "UNAUTHORIZED", 30 | message: "You are not signed in.", 31 | }); 32 | } 33 | 34 | const userData = await ctx.db.user.findUnique({ 35 | where: { 36 | id: ctx.session.userId, 37 | }, 38 | select: { 39 | stripeCustomerId: true, 40 | }, 41 | }); 42 | 43 | if (!ctx.session.userId || !userData?.stripeCustomerId) { 44 | throw new TRPCError({ 45 | code: "UNAUTHORIZED", 46 | message: 47 | "You are not signed in or you don't have a Stripe account, contact support.", 48 | }); 49 | } 50 | 51 | const plan = await stripe.plans.create({ 52 | amount: 2000, 53 | currency: "eur", 54 | interval: "month", 55 | product: input.productId, 56 | }); 57 | 58 | const checkoutSession = await stripe.checkout.sessions.create({ 59 | payment_method_types: ["card"], 60 | billing_address_collection: "required", 61 | line_items: [ 62 | { 63 | price: plan.id, 64 | quantity: 1, 65 | }, 66 | ], 67 | mode: "subscription", 68 | customer: userData.stripeCustomerId, 69 | success_url: 70 | process.env.NEXT_PUBLIC_WEBSITE_URL + 71 | `?session_id={CHECKOUT_SESSION_ID}`, 72 | cancel_url: process.env.NEXT_PUBLIC_WEBSITE_URL, 73 | metadata: { 74 | clerkId: user.id, 75 | clerkEmailAddress: user.emailAddresses[0]!.emailAddress, 76 | clerkFullName: user.firstName + " " + user.lastName ?? "", 77 | }, 78 | }); 79 | 80 | if (checkoutSession) { 81 | return checkoutSession; 82 | } 83 | 84 | throw new TRPCError({ 85 | code: "INTERNAL_SERVER_ERROR", 86 | message: "Something went wrong, contact support.", 87 | }); 88 | }), 89 | 90 | getUserSubscription: protectedProcedure 91 | .input(z.object({})) 92 | .query(async ({ ctx }) => { 93 | const userData = await ctx.db.user.findUnique({ 94 | where: { 95 | id: ctx.session.userId, 96 | }, 97 | select: { 98 | stripeCustomerId: true, 99 | isPremium: true, 100 | PremiumUntil: true, 101 | }, 102 | }); 103 | 104 | if (!ctx.session.userId || !userData?.stripeCustomerId) { 105 | throw new TRPCError({ 106 | code: "UNAUTHORIZED", 107 | message: 108 | "You are not signed in or you don't have a Stripe account, contact support.", 109 | }); 110 | } 111 | 112 | const subscriptions = await stripe.subscriptions.list({ 113 | customer: userData.stripeCustomerId, 114 | }); 115 | 116 | for (const subscription of subscriptions.data) { 117 | if ( 118 | subscription.status === "active" || 119 | subscription.status === "trialing" || 120 | subscription.status === "past_due" 121 | ) { 122 | return { 123 | isPremium: true, 124 | PremiumUntil: subscription.current_period_end, 125 | }; 126 | } 127 | } 128 | 129 | return { 130 | isPremium: false, 131 | PremiumUntil: null, 132 | }; 133 | }), 134 | }); 135 | -------------------------------------------------------------------------------- /src/server/api/trpc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS: 3 | * 1. You want to modify request context (see Part 1). 4 | * 2. You want to create a new middleware or type of procedure (see Part 3). 5 | * 6 | * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will 7 | * need to use are documented accordingly near the end. 8 | */ 9 | import { initTRPC } from "@trpc/server"; 10 | import { type NextRequest } from "next/server"; 11 | import superjson from "superjson"; 12 | import { ZodError } from "zod"; 13 | 14 | import { db } from "@/server/db"; 15 | import { decodeJwt, type Session } from "@clerk/nextjs/server"; 16 | import { clerkClient } from "@clerk/nextjs"; 17 | 18 | /** 19 | * 1. CONTEXT 20 | * 21 | * This section defines the "contexts" that are available in the backend API. 22 | * 23 | * These allow you to access things when processing a request, like the database, the session, etc. 24 | */ 25 | 26 | interface CreateContextOptions { 27 | headers: Headers; 28 | session: Session | null; 29 | } 30 | 31 | /** 32 | * This helper generates the "internals" for a tRPC context. If you need to use it, you can export 33 | * it from here. 34 | * 35 | * Examples of things you may need it for: 36 | * - testing, so we don't have to mock Next.js' req/res 37 | * - tRPC's `createSSGHelpers`, where we don't have req/res 38 | * 39 | * @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts 40 | */ 41 | export const createInnerTRPCContext = (opts: CreateContextOptions) => { 42 | return { 43 | headers: opts.headers, 44 | db, 45 | session: opts.session, 46 | }; 47 | }; 48 | 49 | /** 50 | * This is the actual context you will use in your router. It will be used to process every request 51 | * that goes through your tRPC endpoint. 52 | * 53 | * @see https://trpc.io/docs/context 54 | */ 55 | export const createTRPCContext = async (opts: { req: NextRequest }) => { 56 | const sessionToken = opts.req.cookies.get("__session")?.value ?? ""; 57 | 58 | try { 59 | // Decode the JWT to get the session ID 60 | const decodedJwt = decodeJwt(sessionToken); 61 | 62 | // Verify the session with Clerk to get the session object 63 | const verifiedSession = await clerkClient.sessions.verifySession( 64 | decodedJwt.payload.sid, 65 | sessionToken, 66 | ); 67 | 68 | // If the session is valid, return a context with the session 69 | return createInnerTRPCContext({ 70 | headers: opts.req.headers, 71 | session: verifiedSession, 72 | }); 73 | } catch (error) { 74 | console.log(error); 75 | } 76 | 77 | // If the session is invalid, return a context with no session 78 | return createInnerTRPCContext({ 79 | headers: opts.req.headers, 80 | session: null, 81 | }); 82 | }; 83 | 84 | /** 85 | * 2. INITIALIZATION 86 | * 87 | * This is where the tRPC API is initialized, connecting the context and transformer. We also parse 88 | * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation 89 | * errors on the backend. 90 | */ 91 | 92 | const t = initTRPC.context().create({ 93 | transformer: superjson, 94 | errorFormatter({ shape, error }) { 95 | return { 96 | ...shape, 97 | data: { 98 | ...shape.data, 99 | zodError: 100 | error.cause instanceof ZodError ? error.cause.flatten() : null, 101 | }, 102 | }; 103 | }, 104 | }); 105 | 106 | const isAuthed = t.middleware(async ({ ctx, next }) => { 107 | if (!ctx.session) { 108 | throw new Error("UNAUTHORIZED"); 109 | } 110 | 111 | return next({ 112 | ctx: { 113 | session: ctx.session, 114 | }, 115 | }); 116 | }); 117 | 118 | /** 119 | * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT) 120 | * 121 | * These are the pieces you use to build your tRPC API. You should import these a lot in the 122 | * "/src/server/api/routers" directory. 123 | */ 124 | 125 | /** 126 | * This is how you create new routers and sub-routers in your tRPC API. 127 | * 128 | * @see https://trpc.io/docs/router 129 | */ 130 | export const createTRPCRouter = t.router; 131 | 132 | /** 133 | * Public (unauthenticated) procedure 134 | * 135 | * This is the base piece you use to build new queries and mutations on your tRPC API. It does not 136 | * guarantee that a user querying is authorized, but you can still access user session data if they 137 | * are logged in. 138 | */ 139 | export const publicProcedure = t.procedure; 140 | 141 | export const protectedProcedure = t.procedure.use(isAuthed); 142 | -------------------------------------------------------------------------------- /src/app/api/clerk/route.ts: -------------------------------------------------------------------------------- 1 | import { Webhook } from "svix"; 2 | import { headers } from "next/headers"; 3 | import { WebhookEvent } from "@clerk/nextjs/server"; 4 | import { db } from "@/server/db"; 5 | import Stripe from "stripe"; 6 | 7 | // remember to add your_url/api/clerk/ to the clerk dashboard 8 | 9 | export async function POST(req: Request) { 10 | // You can find this in the Clerk Dashboard -> Webhooks -> choose the webhook 11 | const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET; 12 | 13 | if (!WEBHOOK_SECRET) { 14 | throw new Error( 15 | "Please add WEBHOOK_SECRET from Clerk Dashboard to .env or .env.local", 16 | ); 17 | } 18 | 19 | // Get the headers 20 | const headerPayload = headers(); 21 | const svix_id = headerPayload.get("svix-id"); 22 | const svix_timestamp = headerPayload.get("svix-timestamp"); 23 | const svix_signature = headerPayload.get("svix-signature"); 24 | 25 | // If there are no headers, error out 26 | if (!svix_id || !svix_timestamp || !svix_signature) { 27 | return new Response("Error occured -- no svix headers", { 28 | status: 400, 29 | }); 30 | } 31 | 32 | // Get the body 33 | const payload = await req.json(); 34 | const body = JSON.stringify(payload); 35 | 36 | // Create a new Svix instance with your secret. 37 | const wh = new Webhook(WEBHOOK_SECRET); 38 | 39 | let evt: WebhookEvent; 40 | 41 | // Verify the payload with the headers 42 | try { 43 | evt = wh.verify(body, { 44 | "svix-id": svix_id, 45 | "svix-timestamp": svix_timestamp, 46 | "svix-signature": svix_signature, 47 | }) as WebhookEvent; 48 | } catch (err) { 49 | console.error("Error verifying webhook:", err); 50 | return new Response("Error occured", { 51 | status: 400, 52 | }); 53 | } 54 | 55 | const eventType = evt.type; 56 | 57 | switch (eventType) { 58 | case "user.created": 59 | const { id } = evt.data; 60 | let { 61 | id: userId, 62 | first_name, 63 | last_name, 64 | image_url, 65 | email_addresses, 66 | } = payload.data; 67 | 68 | const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, { 69 | apiVersion: "2023-10-16", 70 | }); 71 | 72 | const stripe_customer = await stripe.customers 73 | .create({ 74 | email: email_addresses[0].email_address, 75 | name: `${first_name} ${last_name}`, 76 | metadata: { 77 | clerkId: userId, 78 | }, 79 | }) 80 | .then((customer) => { 81 | return customer; 82 | }); 83 | 84 | let user = db.user 85 | .create({ 86 | data: { 87 | id: userId, 88 | firstName: first_name ?? " ", 89 | lastName: last_name ?? " ", 90 | profileImageUrl: image_url, 91 | email: email_addresses[0].email_address, 92 | stripeCustomerId: stripe_customer.id, 93 | }, 94 | }) 95 | .then((user) => { 96 | console.log(user); 97 | }); 98 | 99 | console.log(`Webhook with and ID of ${id} and type of ${eventType}`); 100 | console.log( 101 | `User with an ID of ${userId} and name of ${first_name} ${last_name}`, 102 | ); 103 | break; 104 | 105 | case "user.updated": 106 | const { id: u_id } = evt.data; 107 | 108 | let { 109 | id: u_userId, 110 | first_name: u_first_name, 111 | last_name: u_last_name, 112 | image_url: u_profile_image_url, 113 | email_addresses: u_email_addresse, 114 | } = payload.data; 115 | 116 | let u_user = await db.user.update({ 117 | where: { 118 | id: u_userId, 119 | }, 120 | data: { 121 | firstName: u_first_name, 122 | lastName: u_last_name, 123 | profileImageUrl: u_profile_image_url, 124 | email: u_email_addresse[0].email_address, 125 | }, 126 | }); 127 | 128 | console.log(`Webhook with and ID of ${u_id} and type of ${eventType}`); 129 | break; 130 | 131 | case "user.deleted": 132 | const { id: d_id } = evt.data; 133 | 134 | let { id: d_userId } = payload.data; 135 | 136 | let d_user = await db.user.delete({ 137 | where: { 138 | id: d_userId, 139 | }, 140 | }); 141 | console.log(`Webhook with and ID of ${d_id} and type of ${eventType}`); 142 | console.log(`User with an ID of ${d_userId} was deleted`); 143 | break; 144 | 145 | default: 146 | console.log( 147 | `Webhook with and ID of [NotValued] and type of ${eventType}`, 148 | ); 149 | return new Response("Error occured", { 150 | status: 400, 151 | }); 152 | } 153 | 154 | return new Response("", { status: 200 }); 155 | } 156 | -------------------------------------------------------------------------------- /src/app/(marketing)/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { CreatePost } from "@/app/_components/create-post"; 3 | import { api } from "@/trpc/react"; 4 | import { 5 | SignInButton, 6 | SignOutButton, 7 | currentUser, 8 | useUser, 9 | } from "@clerk/nextjs"; 10 | import Link from "next/link"; 11 | import Balancer from "react-wrap-balancer"; 12 | import getStripe from "@/lib/getStripe"; 13 | import { Animated_div, Animated_h1, Animated_p } from "@/lib/animated"; 14 | import Stripe from "stripe"; 15 | import { cn } from "@/lib/utils"; 16 | import LoadingComponent from "../_components/loading"; 17 | 18 | export default function Home() { 19 | const { mutate: getCheckoutSession } = 20 | api.stripe.getCheckoutSession.useMutation({ 21 | onSuccess: async (session: Stripe.Response) => { 22 | const stripe = await getStripe(); 23 | const { error } = await stripe!.redirectToCheckout({ 24 | sessionId: session.id, 25 | }); 26 | }, 27 | }); 28 | 29 | const { user, isLoaded } = useUser(); 30 | if (!isLoaded) { 31 | return ( 32 |
33 | 34 |
35 | ); 36 | } 37 | 38 | const { data: premiumData, isLoading } = { 39 | data: { isPremium: false }, 40 | isLoading: false, 41 | }; 42 | 43 | /* 44 | 45 | i hard coded the premium because 46 | i can't do the stripe integration on a public page 47 | 48 | you need to work like that: 49 | 50 | const { data: premiumData, isLoading } = api.stripe.getUserSubscription.useQuery({}); 51 | 52 | and then you can check if the user is premium or not 53 | 54 | */ 55 | 56 | if (isLoading) { 57 | return ( 58 |
59 | 60 |
61 | ); 62 | } 63 | 64 | return ( 65 |
66 |
67 | 74 | T3 Template App 75 | 76 | 83 | 84 | This is a boilerplate for a fullstack{" "} 85 | Next.js app i have done 86 | implementing different template around the web. I have done This 87 | because every time i try to start a new project i have to do the 88 | same thing over and over again (and every time something decide to 89 | don't work...). I promise to keep this updated and to add new stuff 90 | 91 | 92 |
93 | {user && !isLoading ? ( 94 |
95 | 101 | Logged in as {user.fullName} and you are{" "} 102 | 107 | {premiumData?.isPremium ? "Premium" : "Not Premium"} 108 | 109 | 110 | 116 | 117 | 120 | 121 | 136 | 137 |
138 | ) : ( 139 |
140 |

141 | You are not logged in 142 |

143 | 144 |

145 | Sign in Now 146 |

147 |
148 |
149 | )} 150 |
151 |
152 | 153 |
154 |
155 |
156 | ); 157 | } 158 | 159 | function CrudShowcase() { 160 | "use client"; 161 | const { data: latestPost, isLoading } = api.post.getAllPosts.useQuery(); 162 | 163 | if (isLoading || !latestPost) { 164 | return
Loading Content...
; 165 | } 166 | 167 | return ( 168 |
169 | 170 |
171 |
    172 | {latestPost.map((post, index) => ( 173 |
  • 174 |

    175 | {`Posted by `} 176 | 177 | {post.email} 178 | {" "} 179 | {`on ${post.createdAt.toLocaleDateString()}`} 180 |

    181 |

    {post.name}

    182 |
  • 183 | ))} 184 |
185 |
186 |
187 | ); 188 | } 189 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- 1 |  h6  (�00 h&�(  ���L���A���$������������5����������A����������������������b�������������������� 2 | �����������������������}���0����������b�����������������l����������_�������2����������_���X���\�������������������������R������m�����������������������L�������������������G���N������������������������������Q�������������A���O���������������������������������������|������( @ ������B���h���n���R������f�����������;��� ���������������������������������%��������������J����������������������������������������������O��������������J�������������������������f���_����������������������>��������������J������������������)��� ��������������������������������J������������������"������������������_��������������J���{����������=�������������������������J���{���5�����������������������������J��������������������������J�����������������������������J���i�������������������������J���%���������������3��������������J���4���9������U�����������������������������J���S�����������5���*���������������������������������J���������������������1���8������������������������J��� ������������������,�����������������J��� 3 | ������������������(��������������J��� �������������������$������<���<�������������������������!�������������������������V���a���a���a���a���a���a���a���a���a���a���a���a���a���a���a���a���a���a���+���������������������������������������������������������������������������������������������������������-�������������������������������������������������������������������������������������������������������������(�������������������������[���h���h���h���h���h���h���h���h���h���h���h���h���h���h���h���h���h���h���h���h���M��������������� 4 | (0` ������ ������"���%���$������������ ���J���J���J���;������ ���g��������������������������������B��� ���+������������������������b�����������������������������������������������������.������������������������������������������������������������������������������������;������.��������������������������������������������������������������������������������������������O������.���������������������������������������������������O���$������!���2������������������������������#���.����������������������:�����������������������j������!����������������������������.��������������������������������������������N�������������������������G���.����������������������)�������������������x������)�������������������������.����������������������y��������������� ����������������������&���.���������������������� 5 | ���{�������������!�������������������J���.���������������������� 6 | ���y���A����������������������_���.�����������������������������������������e���.�����������������������������������������]���.����������������������%�������������������G���.��������������������������������������������!���.����������������������5�������������������������.��������������������������������������������5���.����������������������������&������ ���,��������������������������.�������������������������C�����������8������ ���������������������������������.����������������������L�������������������"������y�����������������������8������.������������������������������������������������������������������������*���.����������������������$��������������������������.������u���������.�������������������������&�����������������������������������.���������������������������������������������������.����������������������*��������������������������&���.�������������������������-��������������������������������d���d���d���O��� 7 | ���%�����������������������������1��������������������������������4����������������������������� ������!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���!���������.�����������������������������U����������������������������������������������������������������������������������������������������������������������0���8�����������������������������a���������������������������������������������������������������������������������������������������������������������������������<�������������������������� ���a����������������������������������������������������������������������������������������������������������������������������������9�������������������������� ���X�������������������������������������������������������������������������������������������������������������������������������������<������������������1��� ���!���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#���#��� ��� ��������������������� --------------------------------------------------------------------------------