├── .prettierrc ├── .eslintrc.json ├── app ├── (splash) │ ├── page.tsx │ ├── GetStarted │ │ ├── ConvexLogo.tsx │ │ └── GetStarted.tsx │ └── layout.tsx ├── product │ ├── Chat │ │ ├── ChatIntro.tsx │ │ ├── randomName.ts │ │ ├── MessageList.tsx │ │ ├── Message.tsx │ │ └── Chat.tsx │ ├── page.tsx │ └── layout.tsx ├── layout.tsx ├── globals.css └── signin │ └── page.tsx ├── next.config.ts ├── convex ├── auth.config.ts ├── http.ts ├── auth.ts ├── schema.ts ├── users.ts ├── _generated │ ├── api.js │ ├── api.d.ts │ ├── dataModel.d.ts │ ├── server.js │ └── server.d.ts ├── tsconfig.json ├── messages.ts └── README.md ├── postcss.config.mjs ├── lib └── utils.ts ├── components ├── Code.tsx ├── SignInMethodDivider.tsx ├── ConvexClientProvider.tsx ├── ThemeToggle.tsx ├── ui │ ├── input.tsx │ ├── toggle.tsx │ ├── toggle-group.tsx │ ├── button.tsx │ ├── card.tsx │ └── dropdown-menu.tsx └── UserMenu.tsx ├── eslint.config.mjs ├── components.json ├── tsconfig.json ├── .gitignore ├── middleware.ts ├── package.json ├── public └── convex.svg ├── tailwind.config.ts ├── setup.mjs └── README.md /.prettierrc: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/(splash)/page.tsx: -------------------------------------------------------------------------------- 1 | import { GetStarted } from "@/app/(splash)/GetStarted/GetStarted"; 2 | 3 | export default function HomePage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /convex/auth.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | providers: [ 3 | { 4 | domain: process.env.CONVEX_SITE_URL, 5 | applicationID: "convex", 6 | }, 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /convex/http.ts: -------------------------------------------------------------------------------- 1 | import { httpRouter } from "convex/server"; 2 | import { auth } from "./auth"; 3 | 4 | const http = httpRouter(); 5 | 6 | auth.addHttpRoutes(http); 7 | 8 | export default http; 9 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { clsx, type ClassValue } from "clsx"; 2 | import { twMerge } from "tailwind-merge"; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /components/Code.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | export const Code = ({ children }: { children: ReactNode }) => { 4 | return ( 5 | 6 | {children} 7 | 8 | ); 9 | }; 10 | -------------------------------------------------------------------------------- /convex/auth.ts: -------------------------------------------------------------------------------- 1 | import GitHub from "@auth/core/providers/github"; 2 | import Resend from "@auth/core/providers/resend"; 3 | import { convexAuth } from "@convex-dev/auth/server"; 4 | 5 | export const { auth, signIn, signOut, store, isAuthenticated } = convexAuth({ 6 | providers: [GitHub, Resend], 7 | }); 8 | -------------------------------------------------------------------------------- /app/product/Chat/ChatIntro.tsx: -------------------------------------------------------------------------------- 1 | export function ChatIntro() { 2 | return ( 3 |
4 |

Chat

5 |

6 | Open this app in multiple browser windows to see the real-time database 7 | in action 8 |

9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { authTables } from "@convex-dev/auth/server"; 2 | import { defineSchema, defineTable } from "convex/server"; 3 | import { v } from "convex/values"; 4 | 5 | // The schema is normally optional, but Convex Auth 6 | // requires indexes defined on `authTables`. 7 | export default defineSchema({ 8 | ...authTables, 9 | messages: defineTable({ 10 | userId: v.id("users"), 11 | body: v.string(), 12 | }), 13 | }); 14 | -------------------------------------------------------------------------------- /app/product/Chat/randomName.ts: -------------------------------------------------------------------------------- 1 | const namesList = 2 | "Robert,Linda,Daniel,Anthony,Donald,Paul,Kevin,Brian,Patricia,Jennifer," + 3 | "Elizabeth,William,Richard,Jessica,Lisa,Nancy,Matthew,Ashley,Kimberly," + 4 | "Donna,Kenneth,Melissa"; 5 | const names = namesList.split(","); 6 | 7 | export function randomName(): string { 8 | const picked = names[Math.floor(Math.random() * names.length)]; 9 | return Math.random() > 0.5 ? picked.slice(0, 3) : picked; 10 | } 11 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { dirname } from "path"; 2 | import { fileURLToPath } from "url"; 3 | import { FlatCompat } from "@eslint/eslintrc"; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const compat = new FlatCompat({ 9 | baseDirectory: __dirname, 10 | }); 11 | 12 | const eslintConfig = [ 13 | ...compat.extends("next/core-web-vitals", "next/typescript"), 14 | ]; 15 | 16 | export default eslintConfig; 17 | -------------------------------------------------------------------------------- /components/SignInMethodDivider.tsx: -------------------------------------------------------------------------------- 1 | export function SignInMethodDivider() { 2 | return ( 3 |
4 |
5 | 6 |
7 |
8 | 9 | Or continue with 10 | 11 |
12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /convex/users.ts: -------------------------------------------------------------------------------- 1 | import { getAuthUserId } from "@convex-dev/auth/server"; 2 | import { query } from "./_generated/server"; 3 | 4 | export const viewer = query({ 5 | args: {}, 6 | handler: async (ctx) => { 7 | const userId = await getAuthUserId(ctx); 8 | if (userId === null) { 9 | throw new Error("Not signed in"); 10 | } 11 | const user = await ctx.db.get(userId); 12 | if (user === null) { 13 | throw new Error("User was deleted"); 14 | } 15 | return user; 16 | }, 17 | }); 18 | -------------------------------------------------------------------------------- /convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { anyApi } from "convex/server"; 12 | 13 | /** 14 | * A utility for referencing Convex functions in your app's API. 15 | * 16 | * Usage: 17 | * ```js 18 | * const myFunctionReference = api.myModule.myFunction; 19 | * ``` 20 | */ 21 | export const api = anyApi; 22 | export const internal = anyApi; 23 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "stone", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /components/ConvexClientProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ConvexAuthNextjsProvider } from "@convex-dev/auth/nextjs"; 4 | import { ConvexReactClient } from "convex/react"; 5 | import { ReactNode } from "react"; 6 | 7 | const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!, { 8 | verbose: true, 9 | }); 10 | 11 | export default function ConvexClientProvider({ 12 | children, 13 | }: { 14 | children: ReactNode; 15 | }) { 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 26 | "exclude": ["node_modules"] 27 | } 28 | -------------------------------------------------------------------------------- /.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.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 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 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | 43 | # Ignored for the template, you probably want to remove it: 44 | package-lock.json 45 | -------------------------------------------------------------------------------- /app/product/Chat/MessageList.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode, useEffect, useRef } from "react"; 2 | 3 | export function MessageList({ 4 | messages, 5 | children, 6 | }: { 7 | messages: unknown; 8 | children: ReactNode; 9 | }) { 10 | const messageListRef = useRef(null); 11 | 12 | // Scrolls the list down when new messages 13 | // are received or sent. 14 | useEffect(() => { 15 | if (messageListRef.current) { 16 | messageListRef.current.scrollTo({ 17 | top: messageListRef.current.scrollHeight, 18 | behavior: "smooth", 19 | }); 20 | } 21 | }, [messages]); 22 | return ( 23 |
    27 | {children} 28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /components/ThemeToggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"; 4 | import { DesktopIcon, MoonIcon, SunIcon } from "@radix-ui/react-icons"; 5 | import { useTheme } from "next-themes"; 6 | 7 | export function ThemeToggle() { 8 | const { theme, setTheme } = useTheme(); 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /app/product/page.tsx: -------------------------------------------------------------------------------- 1 | import { Chat } from "@/app/product/Chat/Chat"; 2 | import { ChatIntro } from "@/app/product/Chat/ChatIntro"; 3 | import { UserMenu } from "@/components/UserMenu"; 4 | import { api } from "@/convex/_generated/api"; 5 | import { convexAuthNextjsToken } from "@convex-dev/auth/nextjs/server"; 6 | import { fetchQuery } from "convex/nextjs"; 7 | 8 | export default async function ProductPage() { 9 | const viewer = await fetchQuery( 10 | api.users.viewer, 11 | {}, 12 | { token: await convexAuthNextjsToken() }, 13 | ); 14 | return ( 15 |
16 |
17 | 18 | {viewer.name} 19 |
20 | 21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | /* This TypeScript project config describes the environment that 3 | * Convex functions run in and is used to typecheck them. 4 | * You can modify it, but some settings required to use Convex. 5 | */ 6 | "compilerOptions": { 7 | /* These settings are not required by Convex and can be modified. */ 8 | "allowJs": true, 9 | "strict": true, 10 | "moduleResolution": "Bundler", 11 | "jsx": "react-jsx", 12 | "skipLibCheck": true, 13 | "allowSyntheticDefaultImports": true, 14 | 15 | /* These compiler options are required by Convex */ 16 | "target": "ESNext", 17 | "lib": ["ES2021", "dom"], 18 | "forceConsistentCasingInFileNames": true, 19 | "module": "ESNext", 20 | "isolatedModules": true, 21 | "noEmit": true 22 | }, 23 | "include": ["./**/*"], 24 | "exclude": ["./_generated"] 25 | } 26 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { 2 | convexAuthNextjsMiddleware, 3 | createRouteMatcher, 4 | nextjsMiddlewareRedirect, 5 | } from "@convex-dev/auth/nextjs/server"; 6 | 7 | const isSignInPage = createRouteMatcher(["/signin"]); 8 | const isProtectedRoute = createRouteMatcher(["/product(.*)"]); 9 | 10 | export default convexAuthNextjsMiddleware(async (request, { convexAuth }) => { 11 | if (isSignInPage(request) && (await convexAuth.isAuthenticated())) { 12 | return nextjsMiddlewareRedirect(request, "/product"); 13 | } 14 | if (isProtectedRoute(request) && !(await convexAuth.isAuthenticated())) { 15 | return nextjsMiddlewareRedirect(request, "/signin"); 16 | } 17 | }); 18 | 19 | export const config = { 20 | // The following matcher runs middleware on all routes 21 | // except static assets. 22 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"], 23 | }; 24 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Input = React.forwardRef>( 6 | ({ className, type, ...props }, ref) => { 7 | return ( 8 | 17 | ); 18 | }, 19 | ); 20 | Input.displayName = "Input"; 21 | 22 | export { Input }; 23 | -------------------------------------------------------------------------------- /app/product/Chat/Message.tsx: -------------------------------------------------------------------------------- 1 | import { Id } from "@/convex/_generated/dataModel"; 2 | import { cn } from "@/lib/utils"; 3 | import { ReactNode } from "react"; 4 | 5 | export function Message({ 6 | authorName, 7 | authorId, 8 | viewerId, 9 | children, 10 | }: { 11 | authorName: string; 12 | authorId: Id<"users">; 13 | viewerId: Id<"users">; 14 | children: ReactNode; 15 | }) { 16 | return ( 17 |
  • 23 |
    {authorName}
    24 |

    30 | {children} 31 |

    32 |
  • 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /convex/messages.ts: -------------------------------------------------------------------------------- 1 | import { getAuthUserId } from "@convex-dev/auth/server"; 2 | import { v } from "convex/values"; 3 | import { mutation, query } from "./_generated/server"; 4 | 5 | export const list = query({ 6 | args: {}, 7 | handler: async (ctx) => { 8 | // Grab the most recent messages. 9 | const messages = await ctx.db.query("messages").order("desc").take(100); 10 | // Add the author's name to each message. 11 | return Promise.all( 12 | messages.map(async (message) => { 13 | const { name, email } = (await ctx.db.get(message.userId))!; 14 | return { ...message, author: name ?? email! }; 15 | }), 16 | ); 17 | }, 18 | }); 19 | 20 | export const send = mutation({ 21 | args: { body: v.string(), author: v.string() }, 22 | handler: async (ctx, { body }) => { 23 | const userId = await getAuthUserId(ctx); 24 | if (userId === null) { 25 | throw new Error("Not signed in"); 26 | } 27 | // Send a new message. 28 | await ctx.db.insert("messages", { body, userId }); 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated `api` utility. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type { 12 | ApiFromModules, 13 | FilterApi, 14 | FunctionReference, 15 | } from "convex/server"; 16 | import type * as auth from "../auth.js"; 17 | import type * as http from "../http.js"; 18 | import type * as messages from "../messages.js"; 19 | import type * as users from "../users.js"; 20 | 21 | /** 22 | * A utility for referencing Convex functions in your app's API. 23 | * 24 | * Usage: 25 | * ```js 26 | * const myFunctionReference = api.myModule.myFunction; 27 | * ``` 28 | */ 29 | declare const fullApi: ApiFromModules<{ 30 | auth: typeof auth; 31 | http: typeof http; 32 | messages: typeof messages; 33 | users: typeof users; 34 | }>; 35 | export declare const api: FilterApi< 36 | typeof fullApi, 37 | FunctionReference 38 | >; 39 | export declare const internal: FilterApi< 40 | typeof fullApi, 41 | FunctionReference 42 | >; 43 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { ThemeProvider } from "next-themes"; 3 | import { Geist, Geist_Mono } from "next/font/google"; 4 | import "./globals.css"; 5 | import { ConvexAuthNextjsServerProvider } from "@convex-dev/auth/nextjs/server"; 6 | 7 | const geistSans = Geist({ 8 | variable: "--font-geist-sans", 9 | subsets: ["latin"], 10 | }); 11 | 12 | const geistMono = Geist_Mono({ 13 | variable: "--font-geist-mono", 14 | subsets: ["latin"], 15 | }); 16 | 17 | export const metadata: Metadata = { 18 | title: "Convex + Next.js + Convex Auth", 19 | description: "Generated by npm create convex", 20 | icons: { 21 | icon: "/convex.svg", 22 | }, 23 | }; 24 | 25 | export default function RootLayout({ 26 | children, 27 | }: Readonly<{ 28 | children: React.ReactNode; 29 | }>) { 30 | return ( 31 | 32 | {/* `suppressHydrationWarning` only affects the html tag, 33 | // and is needed by `ThemeProvider` which sets the theme 34 | // class attribute on it */} 35 | 36 | 39 | {children} 40 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "npm-run-all --parallel dev:frontend dev:backend", 7 | "dev:frontend": "next dev --turbopack", 8 | "dev:backend": "convex dev", 9 | "predev": "convex dev --until-success && node setup.mjs --once && convex dashboard", 10 | "build": "next build", 11 | "start": "next start", 12 | "lint": "next lint" 13 | }, 14 | "dependencies": { 15 | "@auth/core": "^0.37.0", 16 | "@convex-dev/auth": "^0.0.77", 17 | "@radix-ui/react-dropdown-menu": "^2.1.4", 18 | "@radix-ui/react-icons": "^1.3.2", 19 | "@radix-ui/react-slot": "^1.1.1", 20 | "@radix-ui/react-toggle": "^1.1.1", 21 | "@radix-ui/react-toggle-group": "^1.1.1", 22 | "class-variance-authority": "^0.7.1", 23 | "clsx": "^2.1.1", 24 | "convex": "^1.17.4", 25 | "lucide-react": "^0.469.0", 26 | "next": "15.5.7", 27 | "next-themes": "^0.4.4", 28 | "react": "^19.2.1", 29 | "react-dom": "^19.2.1", 30 | "sonner": "^2.0.5", 31 | "tailwind-merge": "^2.6.0", 32 | "tailwindcss-animate": "^1.0.7" 33 | }, 34 | "devDependencies": { 35 | "@eslint/eslintrc": "^3", 36 | "@types/node": "^20", 37 | "@types/react": "^19", 38 | "@types/react-dom": "^19", 39 | "dotenv": "^16.4.5", 40 | "eslint": "^9", 41 | "eslint-config-next": "15.1.3", 42 | "npm-run-all": "^4.1.5", 43 | "postcss": "^8", 44 | "prettier": "3.4.2", 45 | "tailwindcss": "^3.4.1", 46 | "typescript": "^5" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /components/UserMenu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeToggle } from "@/components/ThemeToggle"; 4 | import { Button } from "@/components/ui/button"; 5 | import { 6 | DropdownMenu, 7 | DropdownMenuContent, 8 | DropdownMenuItem, 9 | DropdownMenuLabel, 10 | DropdownMenuSeparator, 11 | DropdownMenuTrigger, 12 | } from "@/components/ui/dropdown-menu"; 13 | import { useAuthActions } from "@convex-dev/auth/react"; 14 | import { PersonIcon } from "@radix-ui/react-icons"; 15 | import { ReactNode } from "react"; 16 | 17 | export function UserMenu({ children }: { children: ReactNode }) { 18 | return ( 19 |
    20 | {children} 21 | 22 | 23 | 27 | 28 | 29 | {children} 30 | 31 | 32 | Theme 33 | 34 | 35 | 36 | 37 | 38 |
    39 | ); 40 | } 41 | 42 | function SignOutButton() { 43 | const { signOut } = useAuthActions(); 44 | return ( 45 | void signOut()}>Sign out 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /components/ui/toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as TogglePrimitive from "@radix-ui/react-toggle"; 5 | import { cva, type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const toggleVariants = cva( 10 | "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 11 | { 12 | variants: { 13 | variant: { 14 | default: "bg-transparent", 15 | outline: 16 | "border border-input bg-transparent shadow-sm hover:bg-accent hover:text-accent-foreground", 17 | }, 18 | size: { 19 | default: "h-9 px-2 min-w-9", 20 | sm: "h-8 px-1.5 min-w-8", 21 | lg: "h-10 px-2.5 min-w-10", 22 | }, 23 | }, 24 | defaultVariants: { 25 | variant: "default", 26 | size: "default", 27 | }, 28 | }, 29 | ); 30 | 31 | const Toggle = React.forwardRef< 32 | React.ElementRef, 33 | React.ComponentPropsWithoutRef & 34 | VariantProps 35 | >(({ className, variant, size, ...props }, ref) => ( 36 | 41 | )); 42 | 43 | Toggle.displayName = TogglePrimitive.Root.displayName; 44 | 45 | export { Toggle, toggleVariants }; 46 | -------------------------------------------------------------------------------- /app/product/layout.tsx: -------------------------------------------------------------------------------- 1 | import ConvexClientProvider from "@/components/ConvexClientProvider"; 2 | import { cn } from "@/lib/utils"; 3 | import { ChatBubbleIcon, HomeIcon, ReaderIcon } from "@radix-ui/react-icons"; 4 | import Link from "next/link"; 5 | import { ReactNode } from "react"; 6 | 7 | export default function ProductLayout({ children }: { children: ReactNode }) { 8 | return ( 9 | 10 |
    11 | 12 | {children} 13 |
    14 |
    15 | ); 16 | } 17 | 18 | function ProductMenu() { 19 | return ( 20 | 37 | ); 38 | } 39 | 40 | function MenuLink({ 41 | active, 42 | href, 43 | children, 44 | }: { 45 | active?: boolean; 46 | href: string; 47 | children: ReactNode; 48 | }) { 49 | return ( 50 | 57 | {children} 58 | 59 | ); 60 | } 61 | -------------------------------------------------------------------------------- /public/convex.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/product/Chat/Chat.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Message } from "@/app/product/Chat/Message"; 4 | import { MessageList } from "@/app/product/Chat/MessageList"; 5 | import { Button } from "@/components/ui/button"; 6 | import { Input } from "@/components/ui/input"; 7 | import { useMutation, useQuery } from "convex/react"; 8 | import { FormEvent, useState } from "react"; 9 | import { api } from "../../../convex/_generated/api"; 10 | import { Id } from "@/convex/_generated/dataModel"; 11 | 12 | export function Chat({ viewer }: { viewer: Id<"users"> }) { 13 | const [newMessageText, setNewMessageText] = useState(""); 14 | const messages = useQuery(api.messages.list); 15 | const sendMessage = useMutation(api.messages.send); 16 | 17 | const handleSubmit = (event: FormEvent) => { 18 | event.preventDefault(); 19 | setNewMessageText(""); 20 | sendMessage({ body: newMessageText, author: viewer }).catch((error) => { 21 | console.error("Failed to send message:", error); 22 | }); 23 | }; 24 | 25 | return ( 26 | <> 27 | 28 | {messages?.map((message) => ( 29 | 35 | {message.body} 36 | 37 | ))} 38 | 39 |
    40 |
    41 | setNewMessageText(event.target.value)} 44 | placeholder="Write a message…" 45 | /> 46 | 49 |
    50 |
    51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated data model types. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import type { 12 | DataModelFromSchemaDefinition, 13 | DocumentByName, 14 | TableNamesInDataModel, 15 | SystemTableNames, 16 | } from "convex/server"; 17 | import type { GenericId } from "convex/values"; 18 | import schema from "../schema.js"; 19 | 20 | /** 21 | * The names of all of your Convex tables. 22 | */ 23 | export type TableNames = TableNamesInDataModel; 24 | 25 | /** 26 | * The type of a document stored in Convex. 27 | * 28 | * @typeParam TableName - A string literal type of the table name (like "users"). 29 | */ 30 | export type Doc = DocumentByName< 31 | DataModel, 32 | TableName 33 | >; 34 | 35 | /** 36 | * An identifier for a document in Convex. 37 | * 38 | * Convex documents are uniquely identified by their `Id`, which is accessible 39 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 40 | * 41 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 42 | * 43 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 44 | * strings when type checking. 45 | * 46 | * @typeParam TableName - A string literal type of the table name (like "users"). 47 | */ 48 | export type Id = 49 | GenericId; 50 | 51 | /** 52 | * A type describing your Convex data model. 53 | * 54 | * This type includes information about what tables you have, the type of 55 | * documents stored in those tables, and the indexes defined on them. 56 | * 57 | * This type is used to parameterize methods like `queryGeneric` and 58 | * `mutationGeneric` to make them type-safe. 59 | */ 60 | export type DataModel = DataModelFromSchemaDefinition; 61 | -------------------------------------------------------------------------------- /components/ui/toggle-group.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group"; 5 | import { type VariantProps } from "class-variance-authority"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | import { toggleVariants } from "@/components/ui/toggle"; 9 | 10 | const ToggleGroupContext = React.createContext< 11 | VariantProps 12 | >({ 13 | size: "default", 14 | variant: "default", 15 | }); 16 | 17 | const ToggleGroup = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef & 20 | VariantProps 21 | >(({ className, variant, size, children, ...props }, ref) => ( 22 | 27 | 28 | {children} 29 | 30 | 31 | )); 32 | 33 | ToggleGroup.displayName = ToggleGroupPrimitive.Root.displayName; 34 | 35 | const ToggleGroupItem = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef & 38 | VariantProps 39 | >(({ className, children, variant, size, ...props }, ref) => { 40 | const context = React.useContext(ToggleGroupContext); 41 | 42 | return ( 43 | 54 | {children} 55 | 56 | ); 57 | }); 58 | 59 | ToggleGroupItem.displayName = ToggleGroupPrimitive.Item.displayName; 60 | 61 | export { ToggleGroup, ToggleGroupItem }; 62 | -------------------------------------------------------------------------------- /app/(splash)/GetStarted/ConvexLogo.tsx: -------------------------------------------------------------------------------- 1 | export const ConvexLogo = ({ 2 | width, 3 | height, 4 | }: { 5 | width?: number; 6 | height?: number; 7 | }) => ( 8 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | -------------------------------------------------------------------------------- /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 gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | }, 35 | ); 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean; 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button"; 46 | return ( 47 | 52 | ); 53 | }, 54 | ); 55 | Button.displayName = "Button"; 56 | 57 | export { Button, buttonVariants }; 58 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* To change the theme colors, change the values below 6 | or use the "Copy code" button at https://ui.shadcn.com/themes */ 7 | @layer base { 8 | :root { 9 | --background: 0 0% 100%; 10 | --foreground: 20 14.3% 4.1%; 11 | --card: 0 0% 100%; 12 | --card-foreground: 20 14.3% 4.1%; 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 20 14.3% 4.1%; 15 | --primary: 24 9.8% 10%; 16 | --primary-foreground: 60 9.1% 97.8%; 17 | --secondary: 60 4.8% 95.9%; 18 | --secondary-foreground: 24 9.8% 10%; 19 | --muted: 60 4.8% 95.9%; 20 | --muted-foreground: 25 5.3% 44.7%; 21 | --accent: 60 4.8% 95.9%; 22 | --accent-foreground: 24 9.8% 10%; 23 | --destructive: 0 84.2% 60.2%; 24 | --destructive-foreground: 60 9.1% 97.8%; 25 | --border: 20 5.9% 90%; 26 | --input: 20 5.9% 90%; 27 | --ring: 20 14.3% 4.1%; 28 | --chart-1: 12 76% 61%; 29 | --chart-2: 173 58% 39%; 30 | --chart-3: 197 37% 24%; 31 | --chart-4: 43 74% 66%; 32 | --chart-5: 27 87% 67%; 33 | --radius: 0.5rem; 34 | } 35 | .dark { 36 | --background: 20 14.3% 4.1%; 37 | --foreground: 60 9.1% 97.8%; 38 | --card: 20 14.3% 4.1%; 39 | --card-foreground: 60 9.1% 97.8%; 40 | --popover: 20 14.3% 4.1%; 41 | --popover-foreground: 60 9.1% 97.8%; 42 | --primary: 60 9.1% 97.8%; 43 | --primary-foreground: 24 9.8% 10%; 44 | --secondary: 12 6.5% 15.1%; 45 | --secondary-foreground: 60 9.1% 97.8%; 46 | --muted: 12 6.5% 15.1%; 47 | --muted-foreground: 24 5.4% 63.9%; 48 | --accent: 12 6.5% 15.1%; 49 | --accent-foreground: 60 9.1% 97.8%; 50 | --destructive: 0 62.8% 30.6%; 51 | --destructive-foreground: 60 9.1% 97.8%; 52 | --border: 12 6.5% 15.1%; 53 | --input: 12 6.5% 15.1%; 54 | --ring: 24 5.7% 82.9%; 55 | --chart-1: 220 70% 50%; 56 | --chart-2: 160 60% 45%; 57 | --chart-3: 30 80% 55%; 58 | --chart-4: 280 65% 60%; 59 | --chart-5: 340 75% 55%; 60 | } 61 | } 62 | 63 | @layer base { 64 | * { 65 | @apply border-border; 66 | } 67 | body { 68 | @apply bg-background text-foreground; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
    17 | )); 18 | Card.displayName = "Card"; 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
    29 | )); 30 | CardHeader.displayName = "CardHeader"; 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLDivElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |
    41 | )); 42 | CardTitle.displayName = "CardTitle"; 43 | 44 | const CardDescription = React.forwardRef< 45 | HTMLDivElement, 46 | React.HTMLAttributes 47 | >(({ className, ...props }, ref) => ( 48 |
    53 | )); 54 | CardDescription.displayName = "CardDescription"; 55 | 56 | const CardContent = React.forwardRef< 57 | HTMLDivElement, 58 | React.HTMLAttributes 59 | >(({ className, ...props }, ref) => ( 60 |
    61 | )); 62 | CardContent.displayName = "CardContent"; 63 | 64 | const CardFooter = React.forwardRef< 65 | HTMLDivElement, 66 | React.HTMLAttributes 67 | >(({ className, ...props }, ref) => ( 68 |
    73 | )); 74 | CardFooter.displayName = "CardFooter"; 75 | 76 | export { 77 | Card, 78 | CardHeader, 79 | CardFooter, 80 | CardTitle, 81 | CardDescription, 82 | CardContent, 83 | }; 84 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | export default { 4 | darkMode: ["selector"], 5 | content: [ 6 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 8 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 9 | ], 10 | safelist: ["dark"], 11 | theme: { 12 | container: { 13 | center: true, 14 | padding: "2rem", 15 | screens: { 16 | "2xl": "1400px", 17 | }, 18 | }, 19 | extend: { 20 | colors: { 21 | background: "hsl(var(--background))", 22 | foreground: "hsl(var(--foreground))", 23 | card: { 24 | DEFAULT: "hsl(var(--card))", 25 | foreground: "hsl(var(--card-foreground))", 26 | }, 27 | popover: { 28 | DEFAULT: "hsl(var(--popover))", 29 | foreground: "hsl(var(--popover-foreground))", 30 | }, 31 | primary: { 32 | DEFAULT: "hsl(var(--primary))", 33 | foreground: "hsl(var(--primary-foreground))", 34 | }, 35 | secondary: { 36 | DEFAULT: "hsl(var(--secondary))", 37 | foreground: "hsl(var(--secondary-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | destructive: { 48 | DEFAULT: "hsl(var(--destructive))", 49 | foreground: "hsl(var(--destructive-foreground))", 50 | }, 51 | border: "hsl(var(--border))", 52 | input: "hsl(var(--input))", 53 | ring: "hsl(var(--ring))", 54 | chart: { 55 | "1": "hsl(var(--chart-1))", 56 | "2": "hsl(var(--chart-2))", 57 | "3": "hsl(var(--chart-3))", 58 | "4": "hsl(var(--chart-4))", 59 | "5": "hsl(var(--chart-5))", 60 | }, 61 | }, 62 | borderRadius: { 63 | lg: "var(--radius)", 64 | md: "calc(var(--radius) - 2px)", 65 | sm: "calc(var(--radius) - 4px)", 66 | }, 67 | }, 68 | }, 69 | plugins: [require("tailwindcss-animate")], 70 | } satisfies Config; 71 | -------------------------------------------------------------------------------- /app/(splash)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import Link from "next/link"; 3 | import { ReactNode } from "react"; 4 | 5 | export default function SplashPageLayout({ 6 | children, 7 | }: { 8 | children: ReactNode; 9 | }) { 10 | return ( 11 |
    12 |
    13 | 21 |
    22 |
    {children}
    23 |
    24 |
    25 | Built with ❤️ at{" "} 26 | Convex. 27 | Powered by Convex,{" "} 28 | Next.js and{" "} 29 | shadcn/ui. 30 |
    31 |
    32 |
    33 | ); 34 | } 35 | 36 | function FooterLink({ href, children }: { href: string; children: ReactNode }) { 37 | return ( 38 | 43 | {children} 44 | 45 | ); 46 | } 47 | 48 | function SplashPageNav() { 49 | return ( 50 | <> 51 | 55 | Docs 56 | 57 | 61 | Stack 62 | 63 | 67 | Discord 68 | 69 | 70 | 71 | 72 | 73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /setup.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * This script runs `npx @convex-dev/auth` to help with setting up 3 | * environment variables for Convex Auth. 4 | * 5 | * You can safely delete it and remove it from package.json scripts. 6 | */ 7 | 8 | import fs from "fs"; 9 | import { config as loadEnvFile } from "dotenv"; 10 | import { spawnSync } from "child_process"; 11 | 12 | if (!fs.existsSync(".env.local")) { 13 | // Something is off, skip the script. 14 | process.exit(0); 15 | } 16 | 17 | const config = {}; 18 | loadEnvFile({ path: ".env.local", processEnv: config }); 19 | 20 | const runOnceWorkflow = process.argv.includes("--once"); 21 | 22 | if (runOnceWorkflow && config.SETUP_SCRIPT_RAN !== undefined) { 23 | // The script has already ran once, skip. 24 | process.exit(0); 25 | } 26 | 27 | // The fallback should never be used. 28 | const deploymentName = 29 | config.CONVEX_DEPLOYMENT.split(":").slice(-1)[0] ?? ""; 30 | 31 | const variables = JSON.stringify({ 32 | help: 33 | "This template includes prebuilt sign-in via GitHub OAuth and " + 34 | "magic links via Resend. " + 35 | "This command can help you configure the credentials for these services " + 36 | "via additional Convex environment variables.", 37 | providers: [ 38 | { 39 | name: "GitHub OAuth", 40 | help: 41 | "Create a GitHub OAuth App, follow the instruction here: " + 42 | "https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app\n\n" + 43 | `When you're asked for a callback URL use:\n\n` + 44 | ` https://${deploymentName}.convex.site/api/auth/callback/github`, 45 | variables: [ 46 | { 47 | name: "AUTH_GITHUB_ID", 48 | description: "the Client ID of your GitHub OAuth App", 49 | }, 50 | { 51 | name: "AUTH_GITHUB_SECRET", 52 | description: "the generated client secret", 53 | }, 54 | ], 55 | }, 56 | { 57 | name: "Resend", 58 | help: "Sign up for Resend at https://resend.com/signup. Then create an API Key.", 59 | variables: [ 60 | { 61 | name: "AUTH_RESEND_KEY", 62 | description: "the API Key", 63 | }, 64 | ], 65 | }, 66 | ], 67 | success: 68 | "You're all set. If you need to, you can rerun this command with `node setup.mjs`.", 69 | }); 70 | 71 | console.error( 72 | "You chose Convex Auth as the auth solution. " + 73 | "This command will walk you through setting up " + 74 | "the required Convex environment variables", 75 | ); 76 | 77 | const result = spawnSync( 78 | "npx", 79 | ["@convex-dev/auth", "--variables", variables, "--skip-git-check"], 80 | { stdio: "inherit" }, 81 | ); 82 | 83 | if (runOnceWorkflow) { 84 | fs.writeFileSync(".env.local", `\nSETUP_SCRIPT_RAN=1\n`, { flag: "a" }); 85 | } 86 | 87 | process.exit(result.status); 88 | -------------------------------------------------------------------------------- /convex/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex functions directory! 2 | 3 | Write your Convex functions here. 4 | See https://docs.convex.dev/functions for more. 5 | 6 | A query function that takes two arguments looks like: 7 | 8 | ```ts 9 | // functions.js 10 | import { query } from "./_generated/server"; 11 | import { v } from "convex/values"; 12 | 13 | export const myQueryFunction = query({ 14 | // Validators for arguments. 15 | args: { 16 | first: v.number(), 17 | second: v.string(), 18 | }, 19 | 20 | // Function implementation. 21 | handler: async (ctx, args) => { 22 | // Read the database as many times as you need here. 23 | // See https://docs.convex.dev/database/reading-data. 24 | const documents = await ctx.db.query("tablename").collect(); 25 | 26 | // Arguments passed from the client are properties of the args object. 27 | console.log(args.first, args.second); 28 | 29 | // Write arbitrary JavaScript here: filter, aggregate, build derived data, 30 | // remove non-public properties, or create new objects. 31 | return documents; 32 | }, 33 | }); 34 | ``` 35 | 36 | Using this query function in a React component looks like: 37 | 38 | ```ts 39 | const data = useQuery(api.functions.myQueryFunction, { 40 | first: 10, 41 | second: "hello", 42 | }); 43 | ``` 44 | 45 | A mutation function looks like: 46 | 47 | ```ts 48 | // functions.js 49 | import { mutation } from "./_generated/server"; 50 | import { v } from "convex/values"; 51 | 52 | export const myMutationFunction = mutation({ 53 | // Validators for arguments. 54 | args: { 55 | first: v.string(), 56 | second: v.string(), 57 | }, 58 | 59 | // Function implementation. 60 | handler: async (ctx, args) => { 61 | // Insert or modify documents in the database here. 62 | // Mutations can also read from the database like queries. 63 | // See https://docs.convex.dev/database/writing-data. 64 | const message = { body: args.first, author: args.second }; 65 | const id = await ctx.db.insert("messages", message); 66 | 67 | // Optionally, return a value from your mutation. 68 | return await ctx.db.get(id); 69 | }, 70 | }); 71 | ``` 72 | 73 | Using this mutation function in a React component looks like: 74 | 75 | ```ts 76 | const mutation = useMutation(api.functions.myMutationFunction); 77 | function handleButtonPress() { 78 | // fire and forget, the most common way to use mutations 79 | mutation({ first: "Hello!", second: "me" }); 80 | // OR 81 | // use the result once the mutation has completed 82 | mutation({ first: "Hello!", second: "me" }).then((result) => 83 | console.log(result), 84 | ); 85 | } 86 | ``` 87 | 88 | Use the Convex CLI to push your functions to a deployment. See everything 89 | the Convex CLI can do by running `npx convex -h` in your project root 90 | directory. To learn more, launch the docs with `npx convex docs`. 91 | -------------------------------------------------------------------------------- /app/signin/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignInMethodDivider } from "@/components/SignInMethodDivider"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Input } from "@/components/ui/input"; 6 | import { useAuthActions } from "@convex-dev/auth/react"; 7 | import { GitHubLogoIcon } from "@radix-ui/react-icons"; 8 | import { toast, Toaster } from "sonner"; 9 | import { useState } from "react"; 10 | 11 | export default function SignInPage() { 12 | const [step, setStep] = useState<"signIn" | "linkSent">("signIn"); 13 | 14 | return ( 15 |
    16 |
    17 | {step === "signIn" ? ( 18 | <> 19 |

    20 | Sign in or create an account 21 |

    22 | 23 | 24 | setStep("linkSent")} /> 25 | 26 | ) : ( 27 | <> 28 |

    29 | Check your email 30 |

    31 |

    A sign-in link has been sent to your email address.

    32 | 39 | 40 | )} 41 |
    42 |
    43 | ); 44 | } 45 | 46 | function SignInWithGitHub() { 47 | const { signIn } = useAuthActions(); 48 | return ( 49 | 57 | ); 58 | } 59 | 60 | function SignInWithMagicLink({ 61 | handleLinkSent, 62 | }: { 63 | handleLinkSent: () => void; 64 | }) { 65 | const { signIn } = useAuthActions(); 66 | return ( 67 |
    { 70 | event.preventDefault(); 71 | const formData = new FormData(event.currentTarget); 72 | formData.set("redirectTo", "/product"); 73 | signIn("resend", formData) 74 | .then(handleLinkSent) 75 | .catch((error) => { 76 | console.error(error); 77 | toast.error("Could not send sign-in link"); 78 | }); 79 | }} 80 | > 81 | 82 | 83 | 84 | 85 | 86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Convex + Next.js + Convex Auth app 2 | 3 | This is a [Convex](https://convex.dev/) project created with [`npm create convex`](https://www.npmjs.com/package/create-convex). 4 | 5 | After the initial setup (<2 minutes) you'll have a working full-stack app using: 6 | 7 | - Convex as your backend (database, server logic) 8 | - [Convex Auth](https://labs.convex.dev/auth) for your authentication implementation 9 | - [React](https://react.dev/) as your frontend (web page interactivity) 10 | - [Next.js](https://nextjs.org/) for optimized web hosting and page routing 11 | - [Tailwind](https://tailwindcss.com/) and [shadcn/ui](https://ui.shadcn.com/) for building great looking accessible UI fast 12 | 13 | ## Get started 14 | 15 | If you just cloned this codebase and didn't use `npm create convex`, run: 16 | 17 | ``` 18 | npm install 19 | npm run dev 20 | ``` 21 | 22 | If you're reading this README on GitHub and want to use this template, run: 23 | 24 | ``` 25 | npm create convex@latest -- -t nextjs-convexauth-shadcn 26 | ``` 27 | 28 | ## The app 29 | 30 | The app is a basic multi-user chat. Walkthrough of the source code: 31 | 32 | - [convex/auth.ts](./convex/auth.ts) configures the available authentication methods 33 | - [convex/messages.ts](./convex/messages.ts) is the chat backend implementation 34 | - [middleware.ts](./middleware.ts) determines which pages require sign-in 35 | - [app/layout.tsx](./app/layout.tsx) is the main app layout 36 | - [app/(splash)/page.tsx](<./app/(splash)/page.tsx>) is the splash page (doesn't require sign-in) 37 | - [app/product/layout.tsx](./app/product/layout.tsx) is the "product" layout for the [product page](./app/product/page.tsx) (requires sign-in) 38 | - [app/signin/page.tsx](./app/signin/page.tsx) is the sign-in page 39 | - [app/product/Chat/Chat.tsx](./app/product/Chat/Chat.tsx) is the chat frontend 40 | 41 | ## Configuring other authentication methods 42 | 43 | To configure different authentication methods, see [Configuration](https://labs.convex.dev/auth/config) in the Convex Auth docs. 44 | 45 | ## Learn more 46 | 47 | To learn more about developing your project with Convex, check out: 48 | 49 | - The [Tour of Convex](https://docs.convex.dev/get-started) for a thorough introduction to Convex principles. 50 | - The rest of [Convex docs](https://docs.convex.dev/) to learn about all Convex features. 51 | - [Stack](https://stack.convex.dev/) for in-depth articles on advanced topics. 52 | 53 | ## Join the community 54 | 55 | Join thousands of developers building full-stack apps with Convex: 56 | 57 | ## Deploy on Vercel 58 | 59 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 60 | 61 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 62 | 63 | ## Deploy on Vercel 64 | 65 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 66 | 67 | # Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 68 | 69 | - Join the [Convex Discord community](https://convex.dev/community) to get help in real-time. 70 | - Follow [Convex on GitHub](https://github.com/get-convex/), star and contribute to the open-source implementation of Convex. 71 | -------------------------------------------------------------------------------- /convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { 12 | actionGeneric, 13 | httpActionGeneric, 14 | queryGeneric, 15 | mutationGeneric, 16 | internalActionGeneric, 17 | internalMutationGeneric, 18 | internalQueryGeneric, 19 | } from "convex/server"; 20 | 21 | /** 22 | * Define a query in this Convex app's public API. 23 | * 24 | * This function will be allowed to read your Convex database and will be accessible from the client. 25 | * 26 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 27 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 28 | */ 29 | export const query = queryGeneric; 30 | 31 | /** 32 | * Define a query that is only accessible from other Convex functions (but not from the client). 33 | * 34 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 35 | * 36 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 37 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 38 | */ 39 | export const internalQuery = internalQueryGeneric; 40 | 41 | /** 42 | * Define a mutation in this Convex app's public API. 43 | * 44 | * This function will be allowed to modify your Convex database and will be accessible from the client. 45 | * 46 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 47 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 48 | */ 49 | export const mutation = mutationGeneric; 50 | 51 | /** 52 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 53 | * 54 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 55 | * 56 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 57 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 58 | */ 59 | export const internalMutation = internalMutationGeneric; 60 | 61 | /** 62 | * Define an action in this Convex app's public API. 63 | * 64 | * An action is a function which can execute any JavaScript code, including non-deterministic 65 | * code and code with side-effects, like calling third-party services. 66 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 67 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 68 | * 69 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 70 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 71 | */ 72 | export const action = actionGeneric; 73 | 74 | /** 75 | * Define an action that is only accessible from other Convex functions (but not from the client). 76 | * 77 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 78 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 79 | */ 80 | export const internalAction = internalActionGeneric; 81 | 82 | /** 83 | * Define a Convex HTTP action. 84 | * 85 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 86 | * as its second. 87 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 88 | */ 89 | export const httpAction = httpActionGeneric; 90 | -------------------------------------------------------------------------------- /app/(splash)/GetStarted/GetStarted.tsx: -------------------------------------------------------------------------------- 1 | import { ConvexLogo } from "@/app/(splash)/GetStarted/ConvexLogo"; 2 | import { Code } from "@/components/Code"; 3 | import { Button } from "@/components/ui/button"; 4 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; 5 | import { 6 | CodeIcon, 7 | MagicWandIcon, 8 | PlayIcon, 9 | StackIcon, 10 | } from "@radix-ui/react-icons"; 11 | import Link from "next/link"; 12 | import { ReactNode } from "react"; 13 | 14 | export const GetStarted = () => { 15 | return ( 16 |
    17 |
    18 |

    19 | Your app powered by 20 | 21 |

    22 |
    23 | Build a realtime full-stack app in no time. 24 |
    25 |
    26 | 29 | 32 |
    33 |
    34 |

    35 | Next steps 36 |

    37 |
    38 | This template is a starting point for building your web application. 39 |
    40 |
    41 | 42 | 43 | 44 | Play with the app 45 | 46 | 47 | 48 | Click on{" "} 49 | 53 | Get Started 54 | {" "} 55 | to see the app in action. 56 | 57 | 58 | 59 | 60 | 61 | Inspect your database 62 | 63 | 64 | 65 | The{" "} 66 | 71 | Convex dashboard 72 | {" "} 73 | is already open in another window. 74 | 75 | 76 | 77 | 78 | 79 | 80 | Change the backend 81 | 82 | 83 | 84 | Edit convex/messages.ts to change the backend 85 | functionality. 86 | 87 | 88 | 89 | 90 | 91 | 92 | Change the frontend 93 | 94 | 95 | 96 | Edit app/page.tsx to change your frontend. 97 | 98 | 99 |
    100 |
    101 |
    102 |
    103 |
    104 |

    105 | Helpful resources 106 |

    107 |
    108 | 109 | Read comprehensive documentation for all Convex features. 110 | 111 | 112 | Learn about best practices, use cases, and more from a growing 113 | collection of articles, videos, and walkthroughs. 114 | 115 | 116 | Join our developer community to ask questions, trade tips & 117 | tricks, and show off your projects. 118 | 119 | 120 | Get unblocked quickly by searching across the docs, Stack, and 121 | Discord chats. 122 | 123 |
    124 |
    125 |
    126 |
    127 | ); 128 | }; 129 | 130 | function Resource({ 131 | title, 132 | children, 133 | href, 134 | }: { 135 | title: string; 136 | children: ReactNode; 137 | href: string; 138 | }) { 139 | return ( 140 | 150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /** 3 | * Generated utilities for implementing server-side Convex query and mutation functions. 4 | * 5 | * THIS CODE IS AUTOMATICALLY GENERATED. 6 | * 7 | * To regenerate, run `npx convex dev`. 8 | * @module 9 | */ 10 | 11 | import { 12 | ActionBuilder, 13 | HttpActionBuilder, 14 | MutationBuilder, 15 | QueryBuilder, 16 | GenericActionCtx, 17 | GenericMutationCtx, 18 | GenericQueryCtx, 19 | GenericDatabaseReader, 20 | GenericDatabaseWriter, 21 | } from "convex/server"; 22 | import type { DataModel } from "./dataModel.js"; 23 | 24 | /** 25 | * Define a query in this Convex app's public API. 26 | * 27 | * This function will be allowed to read your Convex database and will be accessible from the client. 28 | * 29 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 30 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 31 | */ 32 | export declare const query: QueryBuilder; 33 | 34 | /** 35 | * Define a query that is only accessible from other Convex functions (but not from the client). 36 | * 37 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 38 | * 39 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 40 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 41 | */ 42 | export declare const internalQuery: QueryBuilder; 43 | 44 | /** 45 | * Define a mutation in this Convex app's public API. 46 | * 47 | * This function will be allowed to modify your Convex database and will be accessible from the client. 48 | * 49 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 50 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 51 | */ 52 | export declare const mutation: MutationBuilder; 53 | 54 | /** 55 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 56 | * 57 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 58 | * 59 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 60 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 61 | */ 62 | export declare const internalMutation: MutationBuilder; 63 | 64 | /** 65 | * Define an action in this Convex app's public API. 66 | * 67 | * An action is a function which can execute any JavaScript code, including non-deterministic 68 | * code and code with side-effects, like calling third-party services. 69 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 70 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 71 | * 72 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 73 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 74 | */ 75 | export declare const action: ActionBuilder; 76 | 77 | /** 78 | * Define an action that is only accessible from other Convex functions (but not from the client). 79 | * 80 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 81 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 82 | */ 83 | export declare const internalAction: ActionBuilder; 84 | 85 | /** 86 | * Define an HTTP action. 87 | * 88 | * This function will be used to respond to HTTP requests received by a Convex 89 | * deployment if the requests matches the path and method where this action 90 | * is routed. Be sure to route your action in `convex/http.js`. 91 | * 92 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 93 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 94 | */ 95 | export declare const httpAction: HttpActionBuilder; 96 | 97 | /** 98 | * A set of services for use within Convex query functions. 99 | * 100 | * The query context is passed as the first argument to any Convex query 101 | * function run on the server. 102 | * 103 | * This differs from the {@link MutationCtx} because all of the services are 104 | * read-only. 105 | */ 106 | export type QueryCtx = GenericQueryCtx; 107 | 108 | /** 109 | * A set of services for use within Convex mutation functions. 110 | * 111 | * The mutation context is passed as the first argument to any Convex mutation 112 | * function run on the server. 113 | */ 114 | export type MutationCtx = GenericMutationCtx; 115 | 116 | /** 117 | * A set of services for use within Convex action functions. 118 | * 119 | * The action context is passed as the first argument to any Convex action 120 | * function run on the server. 121 | */ 122 | export type ActionCtx = GenericActionCtx; 123 | 124 | /** 125 | * An interface to read from the database within Convex query functions. 126 | * 127 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 128 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 129 | * building a query. 130 | */ 131 | export type DatabaseReader = GenericDatabaseReader; 132 | 133 | /** 134 | * An interface to read from and write to the database within Convex mutation 135 | * functions. 136 | * 137 | * Convex guarantees that all writes within a single mutation are 138 | * executed atomically, so you never have to worry about partial writes leaving 139 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 140 | * for the guarantees Convex provides your functions. 141 | */ 142 | export type DatabaseWriter = GenericDatabaseWriter; 143 | -------------------------------------------------------------------------------- /components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"; 5 | import { Check, ChevronRight, Circle } from "lucide-react"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root; 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group; 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal; 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub; 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup; 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean; 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )); 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName; 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )); 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName; 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 74 | 75 | )); 76 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName; 77 | 78 | const DropdownMenuItem = React.forwardRef< 79 | React.ElementRef, 80 | React.ComponentPropsWithoutRef & { 81 | inset?: boolean; 82 | } 83 | >(({ className, inset, ...props }, ref) => ( 84 | svg]:size-4 [&>svg]:shrink-0", 88 | inset && "pl-8", 89 | className, 90 | )} 91 | {...props} 92 | /> 93 | )); 94 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName; 95 | 96 | const DropdownMenuCheckboxItem = React.forwardRef< 97 | React.ElementRef, 98 | React.ComponentPropsWithoutRef 99 | >(({ className, children, checked, ...props }, ref) => ( 100 | 109 | 110 | 111 | 112 | 113 | 114 | {children} 115 | 116 | )); 117 | DropdownMenuCheckboxItem.displayName = 118 | DropdownMenuPrimitive.CheckboxItem.displayName; 119 | 120 | const DropdownMenuRadioItem = React.forwardRef< 121 | React.ElementRef, 122 | React.ComponentPropsWithoutRef 123 | >(({ className, children, ...props }, ref) => ( 124 | 132 | 133 | 134 | 135 | 136 | 137 | {children} 138 | 139 | )); 140 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName; 141 | 142 | const DropdownMenuLabel = React.forwardRef< 143 | React.ElementRef, 144 | React.ComponentPropsWithoutRef & { 145 | inset?: boolean; 146 | } 147 | >(({ className, inset, ...props }, ref) => ( 148 | 157 | )); 158 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName; 159 | 160 | const DropdownMenuSeparator = React.forwardRef< 161 | React.ElementRef, 162 | React.ComponentPropsWithoutRef 163 | >(({ className, ...props }, ref) => ( 164 | 169 | )); 170 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName; 171 | 172 | const DropdownMenuShortcut = ({ 173 | className, 174 | ...props 175 | }: React.HTMLAttributes) => { 176 | return ( 177 | 181 | ); 182 | }; 183 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"; 184 | 185 | export { 186 | DropdownMenu, 187 | DropdownMenuTrigger, 188 | DropdownMenuContent, 189 | DropdownMenuItem, 190 | DropdownMenuCheckboxItem, 191 | DropdownMenuRadioItem, 192 | DropdownMenuLabel, 193 | DropdownMenuSeparator, 194 | DropdownMenuShortcut, 195 | DropdownMenuGroup, 196 | DropdownMenuPortal, 197 | DropdownMenuSub, 198 | DropdownMenuSubContent, 199 | DropdownMenuSubTrigger, 200 | DropdownMenuRadioGroup, 201 | }; 202 | --------------------------------------------------------------------------------