├── apps └── web │ ├── hooks │ └── .gitkeep │ ├── lib │ └── .gitkeep │ ├── app │ ├── (auth) │ │ ├── forgot-password │ │ │ └── page.tsx │ │ ├── login │ │ │ └── page.tsx │ │ ├── register │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── favicon.ico │ ├── api │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ ├── page.tsx │ └── layout.tsx │ ├── public │ └── cat.gif │ ├── .env.example │ ├── eslint.config.js │ ├── middleware.ts │ ├── next.config.js │ ├── tsconfig.json │ ├── components.json │ ├── .gitignore │ ├── components │ ├── providers.tsx │ └── auth │ │ ├── logout-button.tsx │ │ ├── user-card.tsx │ │ ├── register-form.tsx │ │ └── login-form.tsx │ ├── package.json │ └── README.md ├── packages ├── ui │ ├── src │ │ ├── hooks │ │ │ └── .gitkeep │ │ ├── components │ │ │ ├── .gitkeep │ │ │ ├── sonner.tsx │ │ │ ├── label.tsx │ │ │ ├── input.tsx │ │ │ ├── button.tsx │ │ │ └── card.tsx │ │ ├── lib │ │ │ └── utils.ts │ │ └── styles │ │ │ └── globals.css │ ├── eslint.config.mjs │ ├── postcss.config.mjs │ ├── tsconfig.json │ ├── components.json │ └── package.json ├── backend │ ├── .gitignore │ ├── better-auth │ │ ├── handlers.ts │ │ ├── client.ts │ │ ├── middleware.ts │ │ └── server.ts │ ├── .env.example │ ├── convex │ │ ├── schema.ts │ │ ├── http.ts │ │ ├── convex.config.ts │ │ ├── auth.config.ts │ │ ├── _generated │ │ │ ├── api.js │ │ │ ├── dataModel.d.ts │ │ │ ├── server.js │ │ │ ├── server.d.ts │ │ │ └── api.d.ts │ │ ├── lib │ │ │ └── email.tsx │ │ ├── polyfill.ts │ │ └── auth.ts │ ├── tsconfig.json │ └── package.json ├── eslint-config │ ├── README.md │ ├── package.json │ ├── base.js │ ├── react-internal.js │ └── next.js ├── email │ ├── src │ │ ├── index.ts │ │ └── templates │ │ │ └── verify-email.tsx │ ├── tsconfig.json │ └── package.json └── typescript-config │ ├── react-library.json │ ├── package.json │ ├── nextjs.json │ ├── base.json │ └── convex.json ├── turbo └── generators │ ├── package.json │ ├── templates │ ├── package.json.hbs │ └── tsconfig.json.hbs │ └── config.ts ├── pnpm-workspace.yaml ├── tsconfig.json ├── .prettierignore ├── .vscode └── settings.json ├── .prettierrc ├── next-env.d.ts ├── turbo.json ├── .gitignore ├── package.json ├── README.md └── .cursor └── rules └── convex_rules.md /apps/web/hooks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/lib/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ui/src/hooks/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/ui/src/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/app/(auth)/forgot-password/page.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/backend/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .env.local 3 | -------------------------------------------------------------------------------- /turbo/generators/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json" 3 | } 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pnpm-lock.yaml 3 | .next 4 | .turbo 5 | dist 6 | turbo/generators/templates -------------------------------------------------------------------------------- /apps/web/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordanliu/convex-starter/HEAD/apps/web/app/favicon.ico -------------------------------------------------------------------------------- /apps/web/public/cat.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jordanliu/convex-starter/HEAD/apps/web/public/cat.gif -------------------------------------------------------------------------------- /packages/eslint-config/README.md: -------------------------------------------------------------------------------- 1 | # `@turbo/eslint-config` 2 | 3 | Collection of internal eslint configurations. 4 | -------------------------------------------------------------------------------- /apps/web/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { nextJsHandler } from "@convex-dev/better-auth/nextjs"; 2 | 3 | export const { GET, POST } = nextJsHandler(); 4 | -------------------------------------------------------------------------------- /packages/backend/better-auth/handlers.ts: -------------------------------------------------------------------------------- 1 | import { nextJsHandler } from "@convex-dev/better-auth/nextjs"; 2 | 3 | export const { GET, POST } = nextJsHandler(); 4 | -------------------------------------------------------------------------------- /apps/web/.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210 2 | NEXT_PUBLIC_CONVEX_SITE_URL=http://127.0.0.1:3211 3 | NEXT_PUBLIC_APP_URL=http://localhost:3000 4 | -------------------------------------------------------------------------------- /apps/web/app/(auth)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { LoginForm } from "@/components/auth/login-form"; 2 | 3 | export default function LoginPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /apps/web/eslint.config.js: -------------------------------------------------------------------------------- 1 | import { nextJsConfig } from "@repo/eslint-config/next-js"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default nextJsConfig; 5 | -------------------------------------------------------------------------------- /packages/ui/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { config } from "@repo/eslint-config/react-internal"; 2 | 3 | /** @type {import("eslint").Linter.Config} */ 4 | export default config; 5 | -------------------------------------------------------------------------------- /apps/web/app/(auth)/register/page.tsx: -------------------------------------------------------------------------------- 1 | import { RegisterForm } from "@/components/auth/register-form"; 2 | 3 | export default function LoginPage() { 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /packages/ui/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { "@tailwindcss/postcss": {} }, 4 | }; 5 | 6 | export default config; 7 | -------------------------------------------------------------------------------- /packages/email/src/index.ts: -------------------------------------------------------------------------------- 1 | import { render as renderEmail } from "@react-email/components"; 2 | 3 | export const render = (react: React.ReactNode) => { 4 | return renderEmail(react); 5 | }; 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tailwindCSS.experimental.configFile": "packages/ui/src/styles/globals.css", 3 | "eslint.workingDirectories": [ 4 | { 5 | "mode": "auto" 6 | } 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /packages/typescript-config/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "jsx": "react-jsx" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/src/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 | -------------------------------------------------------------------------------- /turbo/generators/templates/package.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/{{ name }}", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "clean": "git clean -xdf .cache .turbo dist node_modules" 7 | } 8 | } -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "useTabs": false, 4 | "tabWidth": 2, 5 | "semi": true, 6 | "singleQuote": false, 7 | "plugins": ["prettier-plugin-tailwindcss", "prettier-plugin-organize-imports"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/typescript-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/typescript-config", 3 | "version": "0.0.0", 4 | "private": true, 5 | "license": "MIT", 6 | "publishConfig": { 7 | "access": "public" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. 6 | -------------------------------------------------------------------------------- /packages/backend/.env.example: -------------------------------------------------------------------------------- 1 | CONVEX_DEPLOYMENT= 2 | NEXT_PUBLIC_CONVEX_URL= 3 | 4 | APP_URL= 5 | 6 | RESEND_API_KEY= 7 | BETTER_AUTH_SECRET= 8 | 9 | GITHUB_CLIENT_ID= 10 | GITHUB_CLIENT_SECRET= 11 | 12 | GOOGLE_CLIENT_ID= 13 | GOOGLE_CLIENT_SECRET= -------------------------------------------------------------------------------- /packages/backend/better-auth/client.ts: -------------------------------------------------------------------------------- 1 | import { convexClient } from "@convex-dev/better-auth/client/plugins"; 2 | import { createAuthClient } from "better-auth/react"; 3 | 4 | export const authClient = createAuthClient({ 5 | plugins: [convexClient()], 6 | }); 7 | -------------------------------------------------------------------------------- /packages/backend/convex/schema.ts: -------------------------------------------------------------------------------- 1 | import { defineSchema, defineTable } from "convex/server"; 2 | import { v } from "convex/values"; 3 | 4 | export default defineSchema({ 5 | users: defineTable({ 6 | email: v.string(), 7 | }).index("email", ["email"]), 8 | }); 9 | -------------------------------------------------------------------------------- /packages/backend/convex/http.ts: -------------------------------------------------------------------------------- 1 | import { httpRouter } from "convex/server"; 2 | import { auth } from "../better-auth/server"; 3 | import { betterAuthComponent } from "./auth"; 4 | 5 | const http = httpRouter(); 6 | 7 | betterAuthComponent.registerRoutes(http, auth); 8 | 9 | export default http; 10 | -------------------------------------------------------------------------------- /packages/backend/convex/convex.config.ts: -------------------------------------------------------------------------------- 1 | import betterAuth from "@convex-dev/better-auth/convex.config"; 2 | import resend from "@convex-dev/resend/convex.config"; 3 | import { defineApp } from "convex/server"; 4 | 5 | const app = defineApp(); 6 | app.use(resend); 7 | app.use(betterAuth); 8 | 9 | export default app; 10 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@repo/ui/*": ["./src/*"] 7 | }, 8 | "outDir": "dist" 9 | }, 10 | "include": ["src"], 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/convex.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./*"], 7 | "@repo/*": ["../../packages/*"] 8 | } 9 | }, 10 | "include": ["**/*.ts", "**/*.tsx"], 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/email/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/react-library.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./*"], 7 | "@repo/*": ["../../packages/*"] 8 | } 9 | }, 10 | "include": ["**/*.ts", "**/*.tsx"], 11 | "exclude": ["node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /turbo/generators/templates/tsconfig.json.hbs: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/base.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./*"], 7 | "@repo/*": ["../../packages/*"] 8 | } 9 | }, 10 | "include": ["**/*.ts", "**/*.tsx"], 11 | "exclude": ["node_modules"] 12 | } -------------------------------------------------------------------------------- /packages/backend/convex/auth.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | providers: [ 3 | { 4 | // Your Convex site URL is provided in a system 5 | // environment variable 6 | domain: process.env.CONVEX_SITE_URL, 7 | 8 | // Application ID has to be "convex" 9 | applicationID: "convex", 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /packages/typescript-config/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "extends": "./base.json", 4 | "compilerOptions": { 5 | "plugins": [{ "name": "next" }], 6 | "module": "ESNext", 7 | "moduleResolution": "Bundler", 8 | "allowJs": true, 9 | "jsx": "preserve", 10 | "noEmit": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@repo/backend/better-auth/middleware"; 2 | 3 | export const middleware = authMiddleware; 4 | 5 | export const config = { 6 | matcher: [ 7 | // Protect all routes except auth routes, api routes, static files, and public assets 8 | "/((?!login|register|forgot-password|api|_next/static|_next/image|favicon.ico).*)", 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /packages/backend/better-auth/middleware.ts: -------------------------------------------------------------------------------- 1 | import { getSessionCookie } from "better-auth/cookies"; 2 | import { NextRequest, NextResponse } from "next/server"; 3 | 4 | export async function authMiddleware(request: NextRequest) { 5 | const sessionCookie = getSessionCookie(request); 6 | 7 | if (!sessionCookie) { 8 | return NextResponse.redirect(new URL("/login", request.url)); 9 | } 10 | 11 | return NextResponse.next(); 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | transpilePackages: ["@repo/ui"], 4 | images: { 5 | remotePatterns: [ 6 | { 7 | protocol: "https", 8 | hostname: "avatars.githubusercontent.com", 9 | }, 10 | { 11 | protocol: "https", 12 | hostname: "lh3.googleusercontent.com", 13 | }, 14 | ], 15 | }, 16 | }; 17 | 18 | export default nextConfig; 19 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.com/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 8 | "outputs": [".next/**", "!.next/cache/**"] 9 | }, 10 | "lint": { 11 | "dependsOn": ["^lint"] 12 | }, 13 | "check-types": { 14 | "dependsOn": ["^check-types"] 15 | }, 16 | "dev": { 17 | "cache": false, 18 | "persistent": true 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@repo/typescript-config/nextjs.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "paths": { 6 | "@/*": ["./*"], 7 | "@repo/*": ["../../packages/*"] 8 | }, 9 | "plugins": [ 10 | { 11 | "name": "next" 12 | } 13 | ] 14 | }, 15 | "include": [ 16 | "next-env.d.ts", 17 | "next.config.ts", 18 | "**/*.ts", 19 | "**/*.tsx", 20 | ".next/types/**/*.ts" 21 | ], 22 | "exclude": ["node_modules"] 23 | } 24 | -------------------------------------------------------------------------------- /apps/web/app/page.tsx: -------------------------------------------------------------------------------- 1 | import UserCard from "@/components/auth/user-card"; 2 | import { getToken } from "@convex-dev/better-auth/nextjs"; 3 | import { auth } from "@repo/backend/better-auth/server"; 4 | import { api } from "@repo/backend/convex/_generated/api"; 5 | import { fetchQuery } from "convex/nextjs"; 6 | 7 | export default async function Page() { 8 | const token = await getToken(auth); 9 | 10 | const user = await fetchQuery(api.auth.getCurrentUser, {}, { token }); 11 | return ; 12 | } 13 | -------------------------------------------------------------------------------- /apps/web/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": "", 8 | "css": "../../packages/ui/src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true 11 | }, 12 | "iconLibrary": "lucide", 13 | "aliases": { 14 | "components": "@/components", 15 | "hooks": "@/hooks", 16 | "lib": "@/lib", 17 | "utils": "@repo/ui/lib/utils", 18 | "ui": "@repo/ui/components" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/ui/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": "", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true 11 | }, 12 | "iconLibrary": "lucide", 13 | "aliases": { 14 | "components": "@repo/ui/components", 15 | "utils": "@repo/ui/lib/utils", 16 | "hooks": "@repo/ui/hooks", 17 | "lib": "@repo/ui/lib", 18 | "ui": "@repo/ui/components" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | -------------------------------------------------------------------------------- /packages/typescript-config/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "declaration": true, 5 | "declarationMap": true, 6 | "esModuleInterop": true, 7 | "incremental": false, 8 | "isolatedModules": true, 9 | "lib": ["es2022", "DOM", "DOM.Iterable"], 10 | "module": "NodeNext", 11 | "moduleDetection": "force", 12 | "moduleResolution": "NodeNext", 13 | "noUncheckedIndexedAccess": true, 14 | "resolveJsonModule": true, 15 | "skipLibCheck": true, 16 | "strict": true, 17 | "target": "ES2022" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/backend/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, componentsGeneric } 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 | export const components = componentsGeneric(); 24 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # env files (can opt-in for commiting if needed) 29 | .env.local 30 | .env.production 31 | .env.development 32 | .env.test 33 | .env 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | -------------------------------------------------------------------------------- /packages/ui/src/components/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/backend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "dev": "convex dev", 7 | "setup": "convex dev --until-success", 8 | "clean": "git clean -xdf .cache .turbo dist node_modules" 9 | }, 10 | "dependencies": { 11 | "@convex-dev/better-auth": "0.8.0-alpha.6", 12 | "@convex-dev/resend": "^0.1.10", 13 | "@repo/email": "workspace:*", 14 | "better-auth": "^1.3.7", 15 | "convex": "^1.25.4", 16 | "next": "^15.5.0" 17 | }, 18 | "devDependencies": { 19 | "@repo/eslint-config": "workspace:*", 20 | "@repo/typescript-config": "workspace:*", 21 | "@types/node": "^24.3.0" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/ui/src/components/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@repo/ui/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /apps/web/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { CatIcon } from "lucide-react"; 2 | import { ReactNode } from "react"; 3 | 4 | export default function Layout({ children }: { children: ReactNode }) { 5 | return ( 6 |
7 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convex-starter", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo run build", 6 | "dev": "turbo run dev", 7 | "lint": "turbo run lint", 8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 9 | "check-types": "turbo run check-types" 10 | }, 11 | "devDependencies": { 12 | "@repo/eslint-config": "workspace:*", 13 | "@repo/typescript-config": "workspace:*", 14 | "@turbo/gen": "^2.5.6", 15 | "prettier": "^3.6.2", 16 | "prettier-plugin-organize-imports": "^4.2.0", 17 | "prettier-plugin-tailwindcss": "^0.6.14", 18 | "turbo": "^2.5.6", 19 | "typescript": "5.9.2" 20 | }, 21 | "packageManager": "pnpm@9.0.0", 22 | "engines": { 23 | "node": ">=18" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/eslint-config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/eslint-config", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "private": true, 6 | "exports": { 7 | "./base": "./base.js", 8 | "./next-js": "./next.js", 9 | "./react-internal": "./react-internal.js" 10 | }, 11 | "devDependencies": { 12 | "@eslint/js": "^9.33.0", 13 | "@next/eslint-plugin-next": "^15.5.0", 14 | "eslint": "^9.33.0", 15 | "eslint-config-prettier": "^10.1.8", 16 | "eslint-plugin-only-warn": "^1.1.0", 17 | "eslint-plugin-react": "^7.37.5", 18 | "eslint-plugin-react-hooks": "^5.2.0", 19 | "eslint-plugin-turbo": "^2.5.6", 20 | "globals": "^16.3.0", 21 | "typescript": "^5.9.2", 22 | "typescript-eslint": "^8.40.0" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/eslint-config/base.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import turboPlugin from "eslint-plugin-turbo"; 4 | import tseslint from "typescript-eslint"; 5 | import onlyWarn from "eslint-plugin-only-warn"; 6 | 7 | /** 8 | * A shared ESLint configuration for the repository. 9 | * 10 | * @type {import("eslint").Linter.Config[]} 11 | * */ 12 | export const config = [ 13 | js.configs.recommended, 14 | eslintConfigPrettier, 15 | ...tseslint.configs.recommended, 16 | { 17 | plugins: { 18 | turbo: turboPlugin, 19 | }, 20 | rules: { 21 | "turbo/no-undeclared-env-vars": "warn", 22 | }, 23 | }, 24 | { 25 | plugins: { 26 | onlyWarn, 27 | }, 28 | }, 29 | { 30 | ignores: ["dist/**"], 31 | }, 32 | ]; 33 | -------------------------------------------------------------------------------- /packages/typescript-config/convex.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "compilerOptions": { 4 | "target": "ES2017", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules", "components/ui/**/*.tsx"] 28 | } 29 | -------------------------------------------------------------------------------- /apps/web/components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ConvexBetterAuthProvider } from "@convex-dev/better-auth/react"; 4 | import { authClient } from "@repo/backend/better-auth/client"; 5 | import { ConvexReactClient } from "convex/react"; 6 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 7 | import { ReactNode } from "react"; 8 | 9 | const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); 10 | 11 | export function Providers({ children }: { children: ReactNode }) { 12 | return ( 13 | 14 | 21 | {children} 22 | 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /packages/email/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/email", 3 | "private": true, 4 | "version": "0.0.0", 5 | "scripts": { 6 | "clean": "git clean -xdf .cache .turbo dist node_modules" 7 | }, 8 | "dependencies": { 9 | "@react-email/components": "^0.5.1", 10 | "react": "^19.1.1", 11 | "react-dom": "^19.1.1" 12 | }, 13 | "devDependencies": { 14 | "@repo/eslint-config": "workspace:*", 15 | "@repo/typescript-config": "workspace:*", 16 | "@types/node": "^24.3.0", 17 | "@types/react": "^19.1.11", 18 | "@types/react-dom": "^19.1.7" 19 | }, 20 | "exports": { 21 | ".": { 22 | "import": "./src/index.ts", 23 | "require": "./src/index.ts" 24 | }, 25 | "./templates/*": { 26 | "import": "./src/templates/*.tsx", 27 | "require": "./src/templates/*.tsx" 28 | }, 29 | "./utils": { 30 | "import": "./src/utils.ts", 31 | "require": "./src/utils.ts" 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Geist, Geist_Mono } from "next/font/google"; 2 | 3 | import { Providers } from "@/components/providers"; 4 | import "@repo/ui/globals.css"; 5 | import { Toaster } from "@repo/ui/src/components/sonner"; 6 | 7 | const fontSans = Geist({ 8 | subsets: ["latin"], 9 | variable: "--font-sans", 10 | }); 11 | 12 | const fontMono = Geist_Mono({ 13 | subsets: ["latin"], 14 | variable: "--font-mono", 15 | }); 16 | 17 | export const metadata = { 18 | title: "convex-starter", 19 | description: "convex-starter", 20 | }; 21 | 22 | export default function RootLayout({ 23 | children, 24 | }: Readonly<{ 25 | children: React.ReactNode; 26 | }>) { 27 | return ( 28 | 29 | 32 | {children} 33 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /apps/web/components/auth/logout-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { authClient } from "@repo/backend/better-auth/client"; 4 | import { Button } from "@repo/ui/src/components/button"; 5 | import { useRouter } from "next/navigation"; 6 | import { useState } from "react"; 7 | 8 | export default function LogoutButton() { 9 | const router = useRouter(); 10 | const [isLoggingOut, setIsLoggingOut] = useState(false); 11 | 12 | const handleLogout = async () => { 13 | setIsLoggingOut(true); 14 | try { 15 | await authClient.signOut({ 16 | fetchOptions: { 17 | onSuccess: () => { 18 | router.push("/login"); 19 | }, 20 | }, 21 | }); 22 | } catch (error) { 23 | console.error("Logout failed:", error); 24 | } finally { 25 | setIsLoggingOut(false); 26 | } 27 | }; 28 | return ( 29 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/backend/convex/lib/email.tsx: -------------------------------------------------------------------------------- 1 | import { RunMutationCtx } from "@convex-dev/better-auth"; 2 | import { Resend } from "@convex-dev/resend"; 3 | import { render } from "@repo/email"; 4 | import { components } from "../_generated/api"; 5 | import "../polyfill"; 6 | 7 | export const resend: Resend = new Resend(components.resend, { 8 | testMode: true, 9 | }); 10 | 11 | export const sendEmail = async ( 12 | ctx: RunMutationCtx, 13 | { 14 | from, 15 | to, 16 | subject, 17 | react, 18 | cc, 19 | bcc, 20 | replyTo, 21 | }: { 22 | from?: string; 23 | to: string; 24 | subject: string; 25 | react: any; 26 | cc?: string[]; 27 | bcc?: string[]; 28 | replyTo?: string[]; 29 | } 30 | ) => { 31 | const defaultFrom = "delivered@resend.dev"; 32 | 33 | await resend.sendEmail(ctx, { 34 | from: from || defaultFrom, 35 | to: to, 36 | subject, 37 | html: await render(react), 38 | ...(cc && { cc }), 39 | ...(bcc && { bcc }), 40 | ...(replyTo && { replyTo }), 41 | }); 42 | }; 43 | -------------------------------------------------------------------------------- /packages/backend/convex/polyfill.ts: -------------------------------------------------------------------------------- 1 | // polyfill MessageChannel without using node:events 2 | if (typeof MessageChannel === "undefined") { 3 | class MockMessagePort { 4 | onmessage: ((ev: MessageEvent) => void) | undefined; 5 | onmessageerror: ((ev: MessageEvent) => void) | undefined; 6 | 7 | close() {} 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | postMessage(_message: unknown, _transfer: Transferable[] = []) {} 10 | start() {} 11 | addEventListener() {} 12 | removeEventListener() {} 13 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 14 | dispatchEvent(_event: Event): boolean { 15 | return false; 16 | } 17 | } 18 | 19 | class MockMessageChannel { 20 | port1: MockMessagePort; 21 | port2: MockMessagePort; 22 | 23 | constructor() { 24 | this.port1 = new MockMessagePort(); 25 | this.port2 = new MockMessagePort(); 26 | } 27 | } 28 | 29 | globalThis.MessageChannel = 30 | MockMessageChannel as unknown as typeof MessageChannel; 31 | } 32 | -------------------------------------------------------------------------------- /packages/ui/src/components/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@repo/ui/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /turbo/generators/config.ts: -------------------------------------------------------------------------------- 1 | import type { PlopTypes } from "@turbo/gen"; 2 | 3 | export default function generator(plop: PlopTypes.NodePlopAPI): void { 4 | plop.setGenerator("package", { 5 | description: "Generate a new package", 6 | prompts: [ 7 | { 8 | type: "input", 9 | name: "name", 10 | message: 11 | "What is the name of the package? (You can skip the `@repo/` prefix)", 12 | }, 13 | ], 14 | actions: [ 15 | (answers) => { 16 | if ( 17 | "name" in answers && 18 | typeof answers.name === "string" && 19 | answers.name.startsWith("@repo/") 20 | ) { 21 | answers.name = answers.name.replace("@repo/", ""); 22 | } 23 | return "Config sanitized"; 24 | }, 25 | { 26 | type: "add", 27 | path: "packages/{{ name }}/package.json", 28 | templateFile: "templates/package.json.hbs", 29 | }, 30 | { 31 | type: "add", 32 | path: "packages/{{ name }}/tsconfig.json", 33 | templateFile: "templates/tsconfig.json.hbs", 34 | }, 35 | ], 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "type": "module", 5 | "private": true, 6 | "scripts": { 7 | "dev": "next dev --turbopack --port 3000", 8 | "build": "next build", 9 | "start": "next start", 10 | "lint": "next lint --max-warnings 0", 11 | "check-types": "tsc --noEmit" 12 | }, 13 | "dependencies": { 14 | "@convex-dev/better-auth": "0.8.0-alpha.6", 15 | "@hookform/resolvers": "^5.2.1", 16 | "@repo/backend": "workspace:*", 17 | "@repo/ui": "workspace:*", 18 | "better-auth": "^1.3.7", 19 | "convex": "^1.25.4", 20 | "lucide-react": "^0.541.0", 21 | "next": "^15.5.0", 22 | "next-themes": "^0.4.6", 23 | "pg": "^8.16.3", 24 | "react": "^19.1.1", 25 | "react-dom": "^19.1.1", 26 | "react-hook-form": "^7.62.0", 27 | "sonner": "^2.0.7", 28 | "zod": "^4.0.17" 29 | }, 30 | "devDependencies": { 31 | "@repo/eslint-config": "workspace:*", 32 | "@repo/typescript-config": "workspace:*", 33 | "@types/node": "^24.3.0", 34 | "@types/react": "19.1.11", 35 | "@types/react-dom": "19.1.7", 36 | "eslint": "^9.33.0", 37 | "typescript": "5.9.2" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/eslint-config/react-internal.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import tseslint from "typescript-eslint"; 4 | import pluginReactHooks from "eslint-plugin-react-hooks"; 5 | import pluginReact from "eslint-plugin-react"; 6 | import globals from "globals"; 7 | import { config as baseConfig } from "./base.js"; 8 | 9 | /** 10 | * A custom ESLint configuration for libraries that use React. 11 | * 12 | * @type {import("eslint").Linter.Config[]} */ 13 | export const config = [ 14 | ...baseConfig, 15 | js.configs.recommended, 16 | eslintConfigPrettier, 17 | ...tseslint.configs.recommended, 18 | pluginReact.configs.flat.recommended, 19 | { 20 | languageOptions: { 21 | ...pluginReact.configs.flat.recommended.languageOptions, 22 | globals: { 23 | ...globals.serviceworker, 24 | ...globals.browser, 25 | }, 26 | }, 27 | }, 28 | { 29 | plugins: { 30 | "react-hooks": pluginReactHooks, 31 | }, 32 | settings: { react: { version: "detect" } }, 33 | rules: { 34 | ...pluginReactHooks.configs.recommended.rules, 35 | // React scope no longer necessary with new JSX transform. 36 | "react/react-in-jsx-scope": "off", 37 | }, 38 | }, 39 | ]; 40 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@repo/ui", 3 | "version": "0.0.0", 4 | "private": true, 5 | "exports": { 6 | "./globals.css": "./src/styles/globals.css", 7 | "./postcss.config": "./postcss.config.mjs", 8 | "./lib/*": "./src/lib/*.ts", 9 | "./components/*": "./src/components/*.tsx", 10 | "./hooks/*": "./src/hooks/*.ts" 11 | }, 12 | "scripts": { 13 | "lint": "eslint . --max-warnings 0", 14 | "check-types": "tsc --noEmit" 15 | }, 16 | "devDependencies": { 17 | "@repo/eslint-config": "workspace:*", 18 | "@repo/typescript-config": "workspace:*", 19 | "@tailwindcss/postcss": "^4.1.12", 20 | "@turbo/gen": "^2.5.6", 21 | "@types/node": "^24.3.0", 22 | "@types/react": "^19.1.11", 23 | "@types/react-dom": "^19.1.7", 24 | "eslint": "^9.33.0", 25 | "tailwindcss": "^4.1.12", 26 | "typescript": "5.9.2" 27 | }, 28 | "dependencies": { 29 | "@radix-ui/react-label": "^2.1.7", 30 | "@radix-ui/react-slot": "^1.2.3", 31 | "class-variance-authority": "^0.7.1", 32 | "clsx": "^2.1.1", 33 | "lucide-react": "^0.541.0", 34 | "next-themes": "^0.4.6", 35 | "react": "^19.1.1", 36 | "react-dom": "^19.1.1", 37 | "sonner": "^2.0.7", 38 | "tailwind-merge": "^3.3.1", 39 | "tw-animate-css": "^1.3.7", 40 | "zod": "^4.0.17" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/eslint-config/next.js: -------------------------------------------------------------------------------- 1 | import js from "@eslint/js"; 2 | import eslintConfigPrettier from "eslint-config-prettier"; 3 | import tseslint from "typescript-eslint"; 4 | import pluginReactHooks from "eslint-plugin-react-hooks"; 5 | import pluginReact from "eslint-plugin-react"; 6 | import globals from "globals"; 7 | import pluginNext from "@next/eslint-plugin-next"; 8 | import { config as baseConfig } from "./base.js"; 9 | 10 | /** 11 | * A custom ESLint configuration for libraries that use Next.js. 12 | * 13 | * @type {import("eslint").Linter.Config[]} 14 | * */ 15 | export const nextJsConfig = [ 16 | ...baseConfig, 17 | js.configs.recommended, 18 | eslintConfigPrettier, 19 | ...tseslint.configs.recommended, 20 | { 21 | ...pluginReact.configs.flat.recommended, 22 | languageOptions: { 23 | ...pluginReact.configs.flat.recommended.languageOptions, 24 | globals: { 25 | ...globals.serviceworker, 26 | }, 27 | }, 28 | }, 29 | { 30 | plugins: { 31 | "@next/next": pluginNext, 32 | }, 33 | rules: { 34 | ...pluginNext.configs.recommended.rules, 35 | ...pluginNext.configs["core-web-vitals"].rules, 36 | }, 37 | }, 38 | { 39 | plugins: { 40 | "react-hooks": pluginReactHooks, 41 | }, 42 | settings: { react: { version: "detect" } }, 43 | rules: { 44 | ...pluginReactHooks.configs.recommended.rules, 45 | // React scope no longer necessary with new JSX transform. 46 | "react/react-in-jsx-scope": "off", 47 | }, 48 | }, 49 | ]; 50 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | 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. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /packages/backend/convex/auth.ts: -------------------------------------------------------------------------------- 1 | import { 2 | BetterAuth, 3 | type AuthFunctions, 4 | type PublicAuthFunctions, 5 | } from "@convex-dev/better-auth"; 6 | import { api, components, internal } from "./_generated/api"; 7 | import type { DataModel, Id } from "./_generated/dataModel"; 8 | import { query } from "./_generated/server"; 9 | 10 | // Typesafe way to pass Convex functions defined in this file 11 | const authFunctions: AuthFunctions = internal.auth; 12 | const publicAuthFunctions: PublicAuthFunctions = api.auth; 13 | 14 | // Initialize the component 15 | export const betterAuthComponent = new BetterAuth(components.betterAuth, { 16 | authFunctions, 17 | publicAuthFunctions, 18 | }); 19 | 20 | // These are required named exports 21 | export const { 22 | createUser, 23 | updateUser, 24 | deleteUser, 25 | createSession, 26 | isAuthenticated, 27 | } = betterAuthComponent.createAuthFunctions({ 28 | // Must create a user and return the user id 29 | onCreateUser: async (ctx, user) => { 30 | return ctx.db.insert("users", { email: user.email }); 31 | }, 32 | 33 | // Delete the user when they are deleted from Better Auth 34 | onDeleteUser: async (ctx, userId) => { 35 | await ctx.db.delete(userId as Id<"users">); 36 | }, 37 | }); 38 | 39 | // Example function for getting the current user 40 | // Feel free to edit, omit, etc. 41 | export const getCurrentUser = query({ 42 | args: {}, 43 | handler: async (ctx) => { 44 | // Get user data from Better Auth - email, name, image, etc. 45 | const userMetadata = await betterAuthComponent.getAuthUser(ctx); 46 | if (!userMetadata) { 47 | return null; 48 | } 49 | // Get user data from your application's database 50 | // (skip this if you have no fields in your users table schema) 51 | const user = await ctx.db.get(userMetadata.userId as Id<"users">); 52 | return { 53 | ...user, 54 | ...userMetadata, 55 | }; 56 | }, 57 | }); 58 | -------------------------------------------------------------------------------- /packages/backend/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 | -------------------------------------------------------------------------------- /apps/web/components/auth/user-card.tsx: -------------------------------------------------------------------------------- 1 | import LogoutButton from "@/components/auth/logout-button"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardDescription, 6 | CardFooter, 7 | CardHeader, 8 | } from "@repo/ui/src/components/card"; 9 | import Image from "next/image"; 10 | 11 | type Props = { 12 | user: { 13 | name?: string | null; 14 | email?: string | null; 15 | image?: string | null; 16 | } | null; 17 | }; 18 | 19 | function UserCard({ user }: Props) { 20 | return ( 21 |
22 | 23 | 24 | 25 | next-starter 32 | 33 | 34 | 35 |
36 |
Name
37 |
{user?.name || "Not provided"}
38 |
39 |
40 |
Email
41 |
{user?.email}
42 |
43 | {user?.image && ( 44 |
45 |
Avatar
46 |
47 | User avatar 54 |
55 |
56 | )} 57 |
58 | 59 | 60 | 61 |
62 |
63 | ); 64 | } 65 | 66 | export default UserCard; 67 | -------------------------------------------------------------------------------- /packages/backend/better-auth/server.ts: -------------------------------------------------------------------------------- 1 | import { convexAdapter } from "@convex-dev/better-auth"; 2 | import { convex } from "@convex-dev/better-auth/plugins"; 3 | import { requireMutationCtx } from "@convex-dev/better-auth/utils"; 4 | import VerifyEmail from "@repo/email/templates/verify-email"; 5 | import { betterAuth, BetterAuthOptions } from "better-auth"; 6 | import { organization } from "better-auth/plugins"; 7 | import { GenericCtx } from "../convex/_generated/server"; 8 | import { betterAuthComponent } from "../convex/auth"; 9 | import { sendEmail } from "../convex/lib/email"; 10 | 11 | const createOptions = (ctx: GenericCtx) => 12 | ({ 13 | baseURL: process.env.APP_URL as string, 14 | database: convexAdapter(ctx, betterAuthComponent), 15 | account: { 16 | accountLinking: { 17 | enabled: true, 18 | allowDifferentEmails: true, 19 | }, 20 | }, 21 | 22 | emailAndPassword: { 23 | enabled: true, 24 | }, 25 | emailVerification: { 26 | sendVerificationEmail: async ({ user, url }) => { 27 | await sendEmail(requireMutationCtx(ctx), { 28 | to: user.email, 29 | subject: "Verify your email address", 30 | react: VerifyEmail({ name: user.name || "", verificationUrl: url }), 31 | }); 32 | }, 33 | }, 34 | socialProviders: { 35 | github: { 36 | clientId: process.env.GITHUB_CLIENT_ID as string, 37 | clientSecret: process.env.GITHUB_CLIENT_SECRET as string, 38 | }, 39 | google: { 40 | clientId: process.env.GOOGLE_CLIENT_ID as string, 41 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string, 42 | accessType: "offline", 43 | prompt: "select_account+consent", 44 | }, 45 | }, 46 | plugins: [organization()], 47 | }) satisfies BetterAuthOptions; 48 | 49 | export const auth = (ctx: GenericCtx): ReturnType => { 50 | const options = createOptions(ctx); 51 | return betterAuth({ 52 | ...options, 53 | plugins: [ 54 | ...options.plugins, 55 | // Pass in options so plugin schema inference flows through. Only required 56 | // for plugins that customize the user or session schema. 57 | // See "Some caveats": 58 | // https://www.better-auth.com/docs/concepts/session-management#customizing-session-response 59 | convex({ options }), 60 | ], 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /packages/ui/src/components/button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "@radix-ui/react-slot"; 2 | import { cva, type VariantProps } from "class-variance-authority"; 3 | import * as React from "react"; 4 | 5 | import { cn } from "@repo/ui/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-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 16 | outline: 17 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", 20 | ghost: 21 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 22 | link: "text-primary underline-offset-4 hover:underline", 23 | }, 24 | size: { 25 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 26 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 27 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 28 | icon: "size-9", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ); 37 | 38 | function Button({ 39 | className, 40 | variant, 41 | size, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"button"> & 45 | VariantProps & { 46 | asChild?: boolean; 47 | }) { 48 | const Comp = asChild ? Slot : "button"; 49 | 50 | return ( 51 | 56 | ); 57 | } 58 | 59 | export { Button, buttonVariants }; 60 | -------------------------------------------------------------------------------- /packages/ui/src/components/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@repo/ui/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /packages/email/src/templates/verify-email.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Button, 4 | Container, 5 | Head, 6 | Hr, 7 | Html, 8 | Link, 9 | Text, 10 | } from "@react-email/components"; 11 | 12 | interface VerifyEmailProps { 13 | name: string; 14 | verificationUrl: string; 15 | } 16 | 17 | export default function VerifyEmail({ 18 | name, 19 | verificationUrl, 20 | }: VerifyEmailProps) { 21 | return ( 22 | 23 | 24 | 25 | 26 | {name ? `Hi ${name},` : "Hi there,"} 27 | 28 | 29 | Welcome! Please verify your email address to complete your account 30 | setup. 31 | 32 | 33 | 34 | Click the button below to verify your email address: 35 | 36 | 37 | 40 | 41 | 42 | Or copy and paste this link into your browser: 43 | 44 | 45 | 46 | {verificationUrl} 47 | 48 | 49 |
50 | 51 | 52 | If you didn't create an account, you can safely ignore this email. 53 | 54 | 55 | 56 | This verification link will expire in 24 hours for security reasons. 57 | 58 |
59 | 60 | 61 | ); 62 | } 63 | 64 | const main = { 65 | backgroundColor: "#ffffff", 66 | fontFamily: 67 | '-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif', 68 | }; 69 | 70 | const container = { 71 | margin: "0 auto", 72 | padding: "20px 0 48px", 73 | maxWidth: "560px", 74 | }; 75 | 76 | const heading = { 77 | fontSize: "24px", 78 | letterSpacing: "-0.5px", 79 | lineHeight: "1.3", 80 | fontWeight: "400", 81 | color: "#484848", 82 | padding: "17px 0 0", 83 | }; 84 | 85 | const paragraph = { 86 | margin: "0 0 15px", 87 | fontSize: "15px", 88 | lineHeight: "1.4", 89 | color: "#3c4149", 90 | }; 91 | 92 | const button = { 93 | backgroundColor: "#007ee6", 94 | borderRadius: "4px", 95 | color: "#fff", 96 | fontSize: "15px", 97 | textDecoration: "none", 98 | textAlign: "center" as const, 99 | display: "block", 100 | width: "210px", 101 | padding: "14px 7px", 102 | margin: "16px auto", 103 | }; 104 | 105 | const link = { 106 | color: "#007ee6", 107 | fontSize: "14px", 108 | textDecoration: "underline", 109 | wordBreak: "break-all" as const, 110 | }; 111 | 112 | const hr = { 113 | borderColor: "#e6ebf1", 114 | margin: "20px 0", 115 | }; 116 | 117 | const footer = { 118 | color: "#8898aa", 119 | fontSize: "12px", 120 | lineHeight: "16px", 121 | marginTop: "12px", 122 | }; 123 | -------------------------------------------------------------------------------- /packages/backend/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 | componentsGeneric, 20 | } from "convex/server"; 21 | 22 | /** 23 | * Define a query in this Convex app's public API. 24 | * 25 | * This function will be allowed to read your Convex database and will be accessible from the client. 26 | * 27 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 28 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 29 | */ 30 | export const query = queryGeneric; 31 | 32 | /** 33 | * Define a query that is only accessible from other Convex functions (but not from the client). 34 | * 35 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 36 | * 37 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 38 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 39 | */ 40 | export const internalQuery = internalQueryGeneric; 41 | 42 | /** 43 | * Define a mutation in this Convex app's public API. 44 | * 45 | * This function will be allowed to modify your Convex database and will be accessible from the client. 46 | * 47 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 48 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 49 | */ 50 | export const mutation = mutationGeneric; 51 | 52 | /** 53 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 54 | * 55 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 56 | * 57 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 58 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 59 | */ 60 | export const internalMutation = internalMutationGeneric; 61 | 62 | /** 63 | * Define an action in this Convex app's public API. 64 | * 65 | * An action is a function which can execute any JavaScript code, including non-deterministic 66 | * code and code with side-effects, like calling third-party services. 67 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 68 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 69 | * 70 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 71 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 72 | */ 73 | export const action = actionGeneric; 74 | 75 | /** 76 | * Define an action that is only accessible from other Convex functions (but not from the client). 77 | * 78 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 79 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 80 | */ 81 | export const internalAction = internalActionGeneric; 82 | 83 | /** 84 | * Define a Convex HTTP action. 85 | * 86 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 87 | * as its second. 88 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 89 | */ 90 | export const httpAction = httpActionGeneric; 91 | -------------------------------------------------------------------------------- /packages/ui/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @source "../../../apps/**/*.{ts,tsx}"; 3 | @source "../../../components/**/*.{ts,tsx}"; 4 | @source "../**/*.{ts,tsx}"; 5 | 6 | @import "tw-animate-css"; 7 | 8 | @custom-variant dark (&:is(.dark *)); 9 | 10 | :root { 11 | --background: oklch(1 0 0); 12 | --foreground: oklch(0.145 0 0); 13 | --card: oklch(1 0 0); 14 | --card-foreground: oklch(0.145 0 0); 15 | --popover: oklch(1 0 0); 16 | --popover-foreground: oklch(0.145 0 0); 17 | --primary: oklch(0.205 0 0); 18 | --primary-foreground: oklch(0.985 0 0); 19 | --secondary: oklch(0.97 0 0); 20 | --secondary-foreground: oklch(0.205 0 0); 21 | --muted: oklch(0.97 0 0); 22 | --muted-foreground: oklch(0.556 0 0); 23 | --accent: oklch(0.97 0 0); 24 | --accent-foreground: oklch(0.205 0 0); 25 | --destructive: oklch(0.577 0.245 27.325); 26 | --destructive-foreground: oklch(0.577 0.245 27.325); 27 | --border: oklch(0.922 0 0); 28 | --input: oklch(0.922 0 0); 29 | --ring: oklch(0.708 0 0); 30 | --chart-1: oklch(0.646 0.222 41.116); 31 | --chart-2: oklch(0.6 0.118 184.704); 32 | --chart-3: oklch(0.398 0.07 227.392); 33 | --chart-4: oklch(0.828 0.189 84.429); 34 | --chart-5: oklch(0.769 0.188 70.08); 35 | --radius: 0.625rem; 36 | --sidebar: oklch(0.985 0 0); 37 | --sidebar-foreground: oklch(0.145 0 0); 38 | --sidebar-primary: oklch(0.205 0 0); 39 | --sidebar-primary-foreground: oklch(0.985 0 0); 40 | --sidebar-accent: oklch(0.97 0 0); 41 | --sidebar-accent-foreground: oklch(0.205 0 0); 42 | --sidebar-border: oklch(0.922 0 0); 43 | --sidebar-ring: oklch(0.708 0 0); 44 | } 45 | 46 | .dark { 47 | --background: oklch(0.145 0 0); 48 | --foreground: oklch(0.985 0 0); 49 | --card: oklch(0.145 0 0); 50 | --card-foreground: oklch(0.985 0 0); 51 | --popover: oklch(0.145 0 0); 52 | --popover-foreground: oklch(0.985 0 0); 53 | --primary: oklch(0.985 0 0); 54 | --primary-foreground: oklch(0.205 0 0); 55 | --secondary: oklch(0.269 0 0); 56 | --secondary-foreground: oklch(0.985 0 0); 57 | --muted: oklch(0.269 0 0); 58 | --muted-foreground: oklch(0.708 0 0); 59 | --accent: oklch(0.269 0 0); 60 | --accent-foreground: oklch(0.985 0 0); 61 | --destructive: oklch(0.396 0.141 25.723); 62 | --destructive-foreground: oklch(0.637 0.237 25.331); 63 | --border: oklch(0.269 0 0); 64 | --input: oklch(0.269 0 0); 65 | --ring: oklch(0.556 0 0); 66 | --chart-1: oklch(0.488 0.243 264.376); 67 | --chart-2: oklch(0.696 0.17 162.48); 68 | --chart-3: oklch(0.769 0.188 70.08); 69 | --chart-4: oklch(0.627 0.265 303.9); 70 | --chart-5: oklch(0.645 0.246 16.439); 71 | --sidebar: oklch(0.205 0 0); 72 | --sidebar-foreground: oklch(0.985 0 0); 73 | --sidebar-primary: oklch(0.488 0.243 264.376); 74 | --sidebar-primary-foreground: oklch(0.985 0 0); 75 | --sidebar-accent: oklch(0.269 0 0); 76 | --sidebar-accent-foreground: oklch(0.985 0 0); 77 | --sidebar-border: oklch(0.269 0 0); 78 | --sidebar-ring: oklch(0.439 0 0); 79 | } 80 | 81 | @theme inline { 82 | --color-background: var(--background); 83 | --color-foreground: var(--foreground); 84 | --color-card: var(--card); 85 | --color-card-foreground: var(--card-foreground); 86 | --color-popover: var(--popover); 87 | --color-popover-foreground: var(--popover-foreground); 88 | --color-primary: var(--primary); 89 | --color-primary-foreground: var(--primary-foreground); 90 | --color-secondary: var(--secondary); 91 | --color-secondary-foreground: var(--secondary-foreground); 92 | --color-muted: var(--muted); 93 | --color-muted-foreground: var(--muted-foreground); 94 | --color-accent: var(--accent); 95 | --color-accent-foreground: var(--accent-foreground); 96 | --color-destructive: var(--destructive); 97 | --color-destructive-foreground: var(--destructive-foreground); 98 | --color-border: var(--border); 99 | --color-input: var(--input); 100 | --color-ring: var(--ring); 101 | --color-chart-1: var(--chart-1); 102 | --color-chart-2: var(--chart-2); 103 | --color-chart-3: var(--chart-3); 104 | --color-chart-4: var(--chart-4); 105 | --color-chart-5: var(--chart-5); 106 | --radius-sm: calc(var(--radius) - 4px); 107 | --radius-md: calc(var(--radius) - 2px); 108 | --radius-lg: var(--radius); 109 | --radius-xl: calc(var(--radius) + 4px); 110 | --color-sidebar: var(--sidebar); 111 | --color-sidebar-foreground: var(--sidebar-foreground); 112 | --color-sidebar-primary: var(--sidebar-primary); 113 | --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 114 | --color-sidebar-accent: var(--sidebar-accent); 115 | --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 116 | --color-sidebar-border: var(--sidebar-border); 117 | --color-sidebar-ring: var(--sidebar-ring); 118 | } 119 | 120 | @layer base { 121 | * { 122 | @apply border-border outline-ring/50; 123 | } 124 | body { 125 | @apply bg-background text-foreground; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # convex-starter 2 | 3 | A highly opinionated Next.js starter with better-auth, convex, shadcn/ui, react-email, and turborepo. Pre-configured for rapid, scalable development. 4 | 5 | ## Project Structure 6 | 7 | ``` 8 | convex-starter/ 9 | ├── apps/ 10 | │ └── web/ # Main Next.js application 11 | ├── packages/ 12 | │ ├── backend/ # Convex backend 13 | │ ├── eslint-config/ # Shared ESLint configurations 14 | │ ├── typescript-config/ # Shared TypeScript configurations 15 | │ └── ui/ # Shared UI components (shadcn/ui) 16 | └── turbo/ # Turborepo configuration 17 | ``` 18 | 19 | ## Features 20 | 21 | - Authentication with [Better Auth](https://better-auth.com) 22 | - Backend platform (db, functions, storage, jobs) using [Convex](https://www.convex.dev/) 23 | - UI components built with [shadcn/ui](https://ui.shadcn.com) and [Tailwind CSS](https://tailwindcss.com) 24 | - Email support with [react-email](https://react.email) and [Resend](https://resend.com) 25 | - Form handling via [react-hook-form](https://react-hook-form.com) 26 | - Monorepo setup using [Turborepo](https://turbo.build/repo) 27 | 28 | ## Getting Started 29 | 30 | ### 1. Create a New Project 31 | 32 | ```bash 33 | npx create-next-app@latest [project-name] --use-pnpm --example https://github.com/jordanliu/convex-starter 34 | ``` 35 | 36 | ### 2. Install Dependencies 37 | 38 | ```bash 39 | cd [project-name] 40 | pnpm install 41 | ``` 42 | 43 | ### 3. Configure Client 44 | 45 | Copy the example environment file into .env.local in apps/web, then update it with your real values. 46 | 47 | ```bash 48 | cp apps/web/.env.example apps/web/.env.local 49 | ``` 50 | 51 | ### 4. Configure Convex 52 | 53 | ```bash 54 | pnpm --filter @repo/backend run setup 55 | ``` 56 | 57 | This initializes your Convex project. Next, ensure your backend environment variables are uploaded to the Convex dashboard. From root run: 58 | 59 | ```bash 60 | cp packages/backend/.env.example packages/backend/.env.local 61 | ``` 62 | 63 | You will then need to upload the environment variables into your Convex dashboard manually or via `convex env`. You can find more details [here](https://docs.convex.dev/production/environment-variables). 64 | 65 | ### 5. Start the Development Server 66 | 67 | ```bash 68 | pnpm dev 69 | ``` 70 | 71 | This will start both the Next.js application at [http://localhost:3000](http://localhost:3000) and the Convex development server at [http://127.0.0.1:6790](http://127.0.0.1:6790). 72 | 73 | ## Available Commands 74 | 75 | ### Development 76 | 77 | ```bash 78 | pnpm dev # Start development servers for all packages 79 | pnpm build # Build all packages for production 80 | pnpm start # Start production server (requires build) 81 | ``` 82 | 83 | ### Code Quality 84 | 85 | ```bash 86 | pnpm lint # Run ESLint across all packages 87 | pnpm format # Format code with Prettier 88 | pnpm check-types # Run TypeScript type checking 89 | ``` 90 | 91 | ### Convex-Specific 92 | 93 | ```bash 94 | pnpm --filter @repo/backend setup # Initialize Convex project (run once) 95 | pnpm --filter @repo/backend dev # Start Convex development server only 96 | pnpm --filter @repo/backend deploy # Deploy Convex backend to production 97 | ``` 98 | 99 | ### Package-Specific 100 | 101 | ```bash 102 | pnpm --filter web dev # Run only the Next.js application 103 | ``` 104 | 105 | ## Project Management 106 | 107 | ### Adding New Packages 108 | 109 | ```bash 110 | turbo gen 111 | ``` 112 | 113 | Follow the prompts to scaffold a new package with proper TypeScript and build configurations. 114 | 115 | ### Adding shadcn/ui Components 116 | 117 | ```bash 118 | cd apps/web 119 | pnpm dlx shadcn@canary add [component-name] 120 | ``` 121 | 122 | Components are automatically added to the UI package and can be imported across the monorepo. 123 | 124 | ### Managing Dependencies 125 | 126 | ```bash 127 | # Add to specific package 128 | pnpm --filter web add [package-name] 129 | pnpm --filter @repo/ui add [package-name] 130 | pnpm --filter @repo/backend add [package-name] 131 | 132 | # Add to workspace root (affects all packages) 133 | pnpm add -w [package-name] 134 | 135 | # Add dev dependencies 136 | pnpm --filter web add -D [package-name] 137 | ``` 138 | 139 | ## Deployment 140 | 141 | ### 1. Deploy Convex Backend 142 | 143 | ```bash 144 | pnpm --filter @repo/backend run deploy 145 | ``` 146 | 147 | This creates your production Convex deployment and provides you with a production URL. 148 | 149 | ### 2. Configure Production Environment 150 | 151 | Update your hosting platform (Vercel, Netlify, etc.) with the production Convex URL: 152 | 153 | ```env 154 | CONVEX_URL=https://your-production-deployment.convex.cloud 155 | NEXT_PUBLIC_CONVEX_URL=https://your-production-deployment.convex.cloud 156 | ``` 157 | 158 | ### 3. Build and Deploy Frontend 159 | 160 | ```bash 161 | pnpm build 162 | ``` 163 | 164 | Then deploy the built application using your preferred hosting platform's deployment method. 165 | -------------------------------------------------------------------------------- /apps/web/components/auth/register-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { authClient } from "@repo/backend/better-auth/client"; 5 | import { Button } from "@repo/ui/components/button"; 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardHeader, 11 | CardTitle, 12 | } from "@repo/ui/components/card"; 13 | import { Input } from "@repo/ui/components/input"; 14 | import { Label } from "@repo/ui/components/label"; 15 | import { cn } from "@repo/ui/lib/utils"; 16 | import Link from "next/link"; 17 | import { useRouter } from "next/navigation"; 18 | import { useState } from "react"; 19 | import { useForm } from "react-hook-form"; 20 | import { toast } from "sonner"; 21 | import { z } from "zod"; 22 | 23 | const registerSchema = z.object({ 24 | name: z.string().min(2, "Name must be at least 2 characters"), 25 | email: z.string().email("Please enter a valid email address"), 26 | password: z.string().min(8, "Password must be at least 8 characters"), 27 | }); 28 | 29 | type RegisterFormData = z.infer; 30 | 31 | export function RegisterForm({ 32 | className, 33 | ...props 34 | }: React.ComponentProps<"div">) { 35 | const [isLoading, setIsLoading] = useState(false); 36 | const router = useRouter(); 37 | 38 | const { 39 | register, 40 | handleSubmit, 41 | formState: { errors }, 42 | } = useForm({ 43 | resolver: zodResolver(registerSchema), 44 | }); 45 | 46 | const onSubmit = async (data: RegisterFormData) => { 47 | setIsLoading(true); 48 | 49 | try { 50 | const { data: authData, error } = await authClient.signUp.email({ 51 | name: data.name, 52 | email: data.email, 53 | password: data.password, 54 | }); 55 | 56 | if (error) { 57 | toast.error("Sign up failed", { 58 | description: 59 | error.message || "Please check your information and try again.", 60 | }); 61 | return; 62 | } 63 | 64 | if (authData) { 65 | console.log(authData); 66 | toast.success("Account created successfully!", { 67 | description: "Welcome! You can now start using the app.", 68 | }); 69 | 70 | // Redirect to home page after successful registration 71 | router.push("/"); 72 | } 73 | } catch { 74 | toast.error("Something went wrong", { 75 | description: "An unexpected error occurred. Please try again.", 76 | }); 77 | } finally { 78 | setIsLoading(false); 79 | } 80 | }; 81 | 82 | return ( 83 |
84 | 85 | 86 | Create Account 87 | Sign up to get started 88 | 89 | 90 |
91 |
92 |
93 | 94 | 101 | {errors.name && ( 102 |

103 | {errors.name.message} 104 |

105 | )} 106 |
107 |
108 | 109 | 116 | {errors.email && ( 117 |

118 | {errors.email.message} 119 |

120 | )} 121 |
122 |
123 | 124 | 130 | {errors.password && ( 131 |

132 | {errors.password.message} 133 |

134 | )} 135 |
136 | 139 |
140 | Already have an account?{" "} 141 | 142 | Sign in 143 | 144 |
145 |
146 |
147 |
148 |
149 |
150 | ); 151 | } 152 | -------------------------------------------------------------------------------- /packages/backend/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 | AnyComponents, 14 | HttpActionBuilder, 15 | MutationBuilder, 16 | QueryBuilder, 17 | GenericActionCtx, 18 | GenericMutationCtx, 19 | GenericQueryCtx, 20 | GenericDatabaseReader, 21 | GenericDatabaseWriter, 22 | FunctionReference, 23 | } from "convex/server"; 24 | import type { DataModel } from "./dataModel.js"; 25 | 26 | type GenericCtx = 27 | | GenericActionCtx 28 | | GenericMutationCtx 29 | | GenericQueryCtx; 30 | 31 | /** 32 | * Define a query in this Convex app's public API. 33 | * 34 | * This function will be allowed to read your Convex database and will 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 declare const query: QueryBuilder; 40 | 41 | /** 42 | * Define a query that is only accessible from other Convex functions (but not from the client). 43 | * 44 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 45 | * 46 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 47 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 48 | */ 49 | export declare const internalQuery: QueryBuilder; 50 | 51 | /** 52 | * Define a mutation in this Convex app's public API. 53 | * 54 | * This function will be allowed to modify your Convex database and will 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 declare const mutation: MutationBuilder; 60 | 61 | /** 62 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 63 | * 64 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 65 | * 66 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 67 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 68 | */ 69 | export declare const internalMutation: MutationBuilder; 70 | 71 | /** 72 | * Define an action in this Convex app's public API. 73 | * 74 | * An action is a function which can execute any JavaScript code, including non-deterministic 75 | * code and code with side-effects, like calling third-party services. 76 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 77 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 78 | * 79 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 80 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 81 | */ 82 | export declare const action: ActionBuilder; 83 | 84 | /** 85 | * Define an action that is only accessible from other Convex functions (but not from the client). 86 | * 87 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 88 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 89 | */ 90 | export declare const internalAction: ActionBuilder; 91 | 92 | /** 93 | * Define an HTTP action. 94 | * 95 | * This function will be used to respond to HTTP requests received by a Convex 96 | * deployment if the requests matches the path and method where this action 97 | * is routed. Be sure to route your action in `convex/http.js`. 98 | * 99 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 100 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 101 | */ 102 | export declare const httpAction: HttpActionBuilder; 103 | 104 | /** 105 | * A set of services for use within Convex query functions. 106 | * 107 | * The query context is passed as the first argument to any Convex query 108 | * function run on the server. 109 | * 110 | * This differs from the {@link MutationCtx} because all of the services are 111 | * read-only. 112 | */ 113 | export type QueryCtx = GenericQueryCtx; 114 | 115 | /** 116 | * A set of services for use within Convex mutation functions. 117 | * 118 | * The mutation context is passed as the first argument to any Convex mutation 119 | * function run on the server. 120 | */ 121 | export type MutationCtx = GenericMutationCtx; 122 | 123 | /** 124 | * A set of services for use within Convex action functions. 125 | * 126 | * The action context is passed as the first argument to any Convex action 127 | * function run on the server. 128 | */ 129 | export type ActionCtx = GenericActionCtx; 130 | 131 | /** 132 | * An interface to read from the database within Convex query functions. 133 | * 134 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 135 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 136 | * building a query. 137 | */ 138 | export type DatabaseReader = GenericDatabaseReader; 139 | 140 | /** 141 | * An interface to read from and write to the database within Convex mutation 142 | * functions. 143 | * 144 | * Convex guarantees that all writes within a single mutation are 145 | * executed atomically, so you never have to worry about partial writes leaving 146 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 147 | * for the guarantees Convex provides your functions. 148 | */ 149 | export type DatabaseWriter = GenericDatabaseWriter; 150 | -------------------------------------------------------------------------------- /apps/web/components/auth/login-form.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { zodResolver } from "@hookform/resolvers/zod"; 4 | import { authClient } from "@repo/backend/better-auth/client"; 5 | import { Button } from "@repo/ui/components/button"; 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardHeader, 11 | CardTitle, 12 | } from "@repo/ui/components/card"; 13 | import { Input } from "@repo/ui/components/input"; 14 | import { Label } from "@repo/ui/components/label"; 15 | import { cn } from "@repo/ui/lib/utils"; 16 | import Link from "next/link"; 17 | import { useRouter } from "next/navigation"; 18 | import { useState } from "react"; 19 | import { useForm } from "react-hook-form"; 20 | import { toast } from "sonner"; 21 | import { z } from "zod"; 22 | 23 | const loginSchema = z.object({ 24 | email: z.string().email("Please enter a valid email address"), 25 | password: z.string().min(8, "Password must be at least 8 characters"), 26 | }); 27 | 28 | type LoginFormData = z.infer; 29 | 30 | export function LoginForm({ 31 | className, 32 | ...props 33 | }: React.ComponentProps<"div">) { 34 | const [isLoading, setIsLoading] = useState(false); 35 | const router = useRouter(); 36 | 37 | const { 38 | register, 39 | handleSubmit, 40 | formState: { errors }, 41 | } = useForm({ 42 | resolver: zodResolver(loginSchema), 43 | }); 44 | 45 | const onSubmit = async (data: LoginFormData) => { 46 | setIsLoading(true); 47 | 48 | try { 49 | const { data: authData, error } = await authClient.signIn.email({ 50 | email: data.email, 51 | password: data.password, 52 | }); 53 | 54 | if (error) { 55 | toast.error("Sign in failed", { 56 | description: 57 | error.message || "Please check your credentials and try again.", 58 | }); 59 | return; 60 | } 61 | 62 | if (authData) { 63 | toast.success("Welcome back!", { 64 | description: "You have been successfully signed in.", 65 | }); 66 | 67 | router.push("/"); 68 | } 69 | } catch { 70 | toast.error("Something went wrong", { 71 | description: "An unexpected error occurred. Please try again.", 72 | }); 73 | } finally { 74 | setIsLoading(false); 75 | } 76 | }; 77 | 78 | const handleSocialLogin = async (provider: "github" | "google") => { 79 | setIsLoading(true); 80 | 81 | try { 82 | await authClient.signIn.social({ 83 | provider: provider, 84 | }); 85 | } catch { 86 | toast.error("Social login failed", { 87 | description: "Please try again or use email/password.", 88 | }); 89 | } finally { 90 | setIsLoading(false); 91 | } 92 | }; 93 | 94 | return ( 95 |
96 | 97 | 98 | Welcome back 99 | 100 | Login with your GitHub or Google account 101 | 102 | 103 | 104 |
105 |
106 |
107 | 126 | 145 |
146 |
147 | 148 | Or continue with 149 | 150 |
151 |
152 |
153 | 154 | 161 | {errors.email && ( 162 |

163 | {errors.email.message} 164 |

165 | )} 166 |
167 |
168 |
169 | 170 | 174 | Forgot your password? 175 | 176 |
177 | 183 | {errors.password && ( 184 |

185 | {errors.password.message} 186 |

187 | )} 188 |
189 | 192 |
193 |
194 | Don't have an account?{" "} 195 | 196 | Sign up 197 | 198 |
199 |
200 |
201 |
202 |
203 |
204 | ); 205 | } 206 | -------------------------------------------------------------------------------- /.cursor/rules/convex_rules.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: Guidelines and best practices for building Convex projects, including database schema design, queries, mutations, and real-world examples 3 | globs: **/*.ts,**/*.tsx,**/*.js,**/*.jsx 4 | --- 5 | 6 | # Convex guidelines 7 | 8 | ## Function guidelines 9 | 10 | ### New function syntax 11 | 12 | - ALWAYS use the new function syntax for Convex functions. For example: 13 | 14 | ```typescript 15 | import { query } from "./_generated/server"; 16 | import { v } from "convex/values"; 17 | export const f = query({ 18 | args: {}, 19 | returns: v.null(), 20 | handler: async (ctx, args) => { 21 | // Function body 22 | }, 23 | }); 24 | ``` 25 | 26 | ### Http endpoint syntax 27 | 28 | - HTTP endpoints are defined in `convex/http.ts` and require an `httpAction` decorator. For example: 29 | 30 | ```typescript 31 | import { httpRouter } from "convex/server"; 32 | import { httpAction } from "./_generated/server"; 33 | const http = httpRouter(); 34 | http.route({ 35 | path: "/echo", 36 | method: "POST", 37 | handler: httpAction(async (ctx, req) => { 38 | const body = await req.bytes(); 39 | return new Response(body, { status: 200 }); 40 | }), 41 | }); 42 | ``` 43 | 44 | - HTTP endpoints are always registered at the exact path you specify in the `path` field. For example, if you specify `/api/someRoute`, the endpoint will be registered at `/api/someRoute`. 45 | 46 | ### Validators 47 | 48 | - Below is an example of an array validator: 49 | 50 | ```typescript 51 | import { mutation } from "./_generated/server"; 52 | import { v } from "convex/values"; 53 | 54 | export default mutation({ 55 | args: { 56 | simpleArray: v.array(v.union(v.string(), v.number())), 57 | }, 58 | handler: async (ctx, args) => { 59 | //... 60 | }, 61 | }); 62 | ``` 63 | 64 | - Below is an example of a schema with validators that codify a discriminated union type: 65 | 66 | ```typescript 67 | import { defineSchema, defineTable } from "convex/server"; 68 | import { v } from "convex/values"; 69 | 70 | export default defineSchema({ 71 | results: defineTable( 72 | v.union( 73 | v.object({ 74 | kind: v.literal("error"), 75 | errorMessage: v.string(), 76 | }), 77 | v.object({ 78 | kind: v.literal("success"), 79 | value: v.number(), 80 | }) 81 | ) 82 | ), 83 | }); 84 | ``` 85 | 86 | - Always use the `v.null()` validator when returning a null value. Below is an example query that returns a null value: 87 | 88 | ```typescript 89 | import { query } from "./_generated/server"; 90 | import { v } from "convex/values"; 91 | 92 | export const exampleQuery = query({ 93 | args: {}, 94 | returns: v.null(), 95 | handler: async (ctx, args) => { 96 | console.log("This query returns a null value"); 97 | return null; 98 | }, 99 | }); 100 | ``` 101 | 102 | - Here are the valid Convex types along with their respective validators: 103 | Convex Type | TS/JS type | Example Usage | Validator for argument validation and schemas | Notes | 104 | | ----------- | ------------| -----------------------| -----------------------------------------------| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 105 | | Id | string | `doc._id` | `v.id(tableName)` | | 106 | | Null | null | `null` | `v.null()` | JavaScript's `undefined` is not a valid Convex value. Functions the return `undefined` or do not return will return `null` when called from a client. Use `null` instead. | 107 | | Int64 | bigint | `3n` | `v.int64()` | Int64s only support BigInts between -2^63 and 2^63-1. Convex supports `bigint`s in most modern browsers. | 108 | | Float64 | number | `3.1` | `v.number()` | Convex supports all IEEE-754 double-precision floating point numbers (such as NaNs). Inf and NaN are JSON serialized as strings. | 109 | | Boolean | boolean | `true` | `v.boolean()` | 110 | | String | string | `"abc"` | `v.string()` | Strings are stored as UTF-8 and must be valid Unicode sequences. Strings must be smaller than the 1MB total size limit when encoded as UTF-8. | 111 | | Bytes | ArrayBuffer | `new ArrayBuffer(8)` | `v.bytes()` | Convex supports first class bytestrings, passed in as `ArrayBuffer`s. Bytestrings must be smaller than the 1MB total size limit for Convex types. | 112 | | Array | Array] | `[1, 3.2, "abc"]` | `v.array(values)` | Arrays can have at most 8192 values. | 113 | | Object | Object | `{a: "abc"}` | `v.object({property: value})` | Convex only supports "plain old JavaScript objects" (objects that do not have a custom prototype). Objects can have at most 1024 entries. Field names must be nonempty and not start with "$" or "_". | 114 | | Record | Record | `{"a": "1", "b": "2"}` | `v.record(keys, values)` | Records are objects at runtime, but can have dynamic keys. Keys must be only ASCII characters, nonempty, and not start with "$" or "\_". | 115 | 116 | ### Function registration 117 | 118 | - Use `internalQuery`, `internalMutation`, and `internalAction` to register internal functions. These functions are private and aren't part of an app's API. They can only be called by other Convex functions. These functions are always imported from `./_generated/server`. 119 | - Use `query`, `mutation`, and `action` to register public functions. These functions are part of the public API and are exposed to the public Internet. Do NOT use `query`, `mutation`, or `action` to register sensitive internal functions that should be kept private. 120 | - You CANNOT register a function through the `api` or `internal` objects. 121 | - ALWAYS include argument and return validators for all Convex functions. This includes all of `query`, `internalQuery`, `mutation`, `internalMutation`, `action`, and `internalAction`. If a function doesn't return anything, include `returns: v.null()` as its output validator. 122 | - If the JavaScript implementation of a Convex function doesn't have a return value, it implicitly returns `null`. 123 | 124 | ### Function calling 125 | 126 | - Use `ctx.runQuery` to call a query from a query, mutation, or action. 127 | - Use `ctx.runMutation` to call a mutation from a mutation or action. 128 | - Use `ctx.runAction` to call an action from an action. 129 | - ONLY call an action from another action if you need to cross runtimes (e.g. from V8 to Node). Otherwise, pull out the shared code into a helper async function and call that directly instead. 130 | - Try to use as few calls from actions to queries and mutations as possible. Queries and mutations are transactions, so splitting logic up into multiple calls introduces the risk of race conditions. 131 | - All of these calls take in a `FunctionReference`. Do NOT try to pass the callee function directly into one of these calls. 132 | - When using `ctx.runQuery`, `ctx.runMutation`, or `ctx.runAction` to call a function in the same file, specify a type annotation on the return value to work around TypeScript circularity limitations. For example, 133 | 134 | ``` 135 | export const f = query({ 136 | args: { name: v.string() }, 137 | returns: v.string(), 138 | handler: async (ctx, args) => { 139 | return "Hello " + args.name; 140 | }, 141 | }); 142 | 143 | export const g = query({ 144 | args: {}, 145 | returns: v.null(), 146 | handler: async (ctx, args) => { 147 | const result: string = await ctx.runQuery(api.example.f, { name: "Bob" }); 148 | return null; 149 | }, 150 | }); 151 | ``` 152 | 153 | ### Function references 154 | 155 | - Function references are pointers to registered Convex functions. 156 | - Use the `api` object defined by the framework in `convex/_generated/api.ts` to call public functions registered with `query`, `mutation`, or `action`. 157 | - Use the `internal` object defined by the framework in `convex/_generated/api.ts` to call internal (or private) functions registered with `internalQuery`, `internalMutation`, or `internalAction`. 158 | - Convex uses file-based routing, so a public function defined in `convex/example.ts` named `f` has a function reference of `api.example.f`. 159 | - A private function defined in `convex/example.ts` named `g` has a function reference of `internal.example.g`. 160 | - Functions can also registered within directories nested within the `convex/` folder. For example, a public function `h` defined in `convex/messages/access.ts` has a function reference of `api.messages.access.h`. 161 | 162 | ### Api design 163 | 164 | - Convex uses file-based routing, so thoughtfully organize files with public query, mutation, or action functions within the `convex/` directory. 165 | - Use `query`, `mutation`, and `action` to define public functions. 166 | - Use `internalQuery`, `internalMutation`, and `internalAction` to define private, internal functions. 167 | 168 | ### Pagination 169 | 170 | - Paginated queries are queries that return a list of results in incremental pages. 171 | - You can define pagination using the following syntax: 172 | 173 | ```ts 174 | import { v } from "convex/values"; 175 | import { query, mutation } from "./_generated/server"; 176 | import { paginationOptsValidator } from "convex/server"; 177 | export const listWithExtraArg = query({ 178 | args: { paginationOpts: paginationOptsValidator, author: v.string() }, 179 | handler: async (ctx, args) => { 180 | return await ctx.db 181 | .query("messages") 182 | .filter((q) => q.eq(q.field("author"), args.author)) 183 | .order("desc") 184 | .paginate(args.paginationOpts); 185 | }, 186 | }); 187 | ``` 188 | 189 | Note: `paginationOpts` is an object with the following properties: 190 | 191 | - `numItems`: the maximum number of documents to return (the validator is `v.number()`) 192 | - `cursor`: the cursor to use to fetch the next page of documents (the validator is `v.union(v.string(), v.null())`) 193 | - A query that ends in `.paginate()` returns an object that has the following properties: - page (contains an array of documents that you fetches) - isDone (a boolean that represents whether or not this is the last page of documents) - continueCursor (a string that represents the cursor to use to fetch the next page of documents) 194 | 195 | ## Validator guidelines 196 | 197 | - `v.bigint()` is deprecated for representing signed 64-bit integers. Use `v.int64()` instead. 198 | - Use `v.record()` for defining a record type. `v.map()` and `v.set()` are not supported. 199 | 200 | ## Schema guidelines 201 | 202 | - Always define your schema in `convex/schema.ts`. 203 | - Always import the schema definition functions from `convex/server`: 204 | - System fields are automatically added to all documents and are prefixed with an underscore. The two system fields that are automatically added to all documents are `_creationTime` which has the validator `v.number()` and `_id` which has the validator `v.id(tableName)`. 205 | - Always include all index fields in the index name. For example, if an index is defined as `["field1", "field2"]`, the index name should be "by_field1_and_field2". 206 | - Index fields must be queried in the same order they are defined. If you want to be able to query by "field1" then "field2" and by "field2" then "field1", you must create separate indexes. 207 | 208 | ## Typescript guidelines 209 | 210 | - You can use the helper typescript type `Id` imported from './\_generated/dataModel' to get the type of the id for a given table. For example if there is a table called 'users' you can use `Id<'users'>` to get the type of the id for that table. 211 | - If you need to define a `Record` make sure that you correctly provide the type of the key and value in the type. For example a validator `v.record(v.id('users'), v.string())` would have the type `Record, string>`. Below is an example of using `Record` with an `Id` type in a query: 212 | 213 | ```ts 214 | import { query } from "./_generated/server"; 215 | import { Doc, Id } from "./_generated/dataModel"; 216 | 217 | export const exampleQuery = query({ 218 | args: { userIds: v.array(v.id("users")) }, 219 | returns: v.record(v.id("users"), v.string()), 220 | handler: async (ctx, args) => { 221 | const idToUsername: Record, string> = {}; 222 | for (const userId of args.userIds) { 223 | const user = await ctx.db.get(userId); 224 | if (user) { 225 | users[user._id] = user.username; 226 | } 227 | } 228 | 229 | return idToUsername; 230 | }, 231 | }); 232 | ``` 233 | 234 | - Be strict with types, particularly around id's of documents. For example, if a function takes in an id for a document in the 'users' table, take in `Id<'users'>` rather than `string`. 235 | - Always use `as const` for string literals in discriminated union types. 236 | - When using the `Array` type, make sure to always define your arrays as `const array: Array = [...];` 237 | - When using the `Record` type, make sure to always define your records as `const record: Record = {...};` 238 | - Always add `@types/node` to your `package.json` when using any Node.js built-in modules. 239 | 240 | ## Full text search guidelines 241 | 242 | - A query for "10 messages in channel '#general' that best match the query 'hello hi' in their body" would look like: 243 | 244 | const messages = await ctx.db 245 | .query("messages") 246 | .withSearchIndex("search_body", (q) => 247 | q.search("body", "hello hi").eq("channel", "#general"), 248 | ) 249 | .take(10); 250 | 251 | ## Query guidelines 252 | 253 | - Do NOT use `filter` in queries. Instead, define an index in the schema and use `withIndex` instead. 254 | - Convex queries do NOT support `.delete()`. Instead, `.collect()` the results, iterate over them, and call `ctx.db.delete(row._id)` on each result. 255 | - Use `.unique()` to get a single document from a query. This method will throw an error if there are multiple documents that match the query. 256 | - When using async iteration, don't use `.collect()` or `.take(n)` on the result of a query. Instead, use the `for await (const row of query)` syntax. 257 | 258 | ### Ordering 259 | 260 | - By default Convex always returns documents in ascending `_creationTime` order. 261 | - You can use `.order('asc')` or `.order('desc')` to pick whether a query is in ascending or descending order. If the order isn't specified, it defaults to ascending. 262 | - Document queries that use indexes will be ordered based on the columns in the index and can avoid slow table scans. 263 | 264 | ## Mutation guidelines 265 | 266 | - Use `ctx.db.replace` to fully replace an existing document. This method will throw an error if the document does not exist. 267 | - Use `ctx.db.patch` to shallow merge updates into an existing document. This method will throw an error if the document does not exist. 268 | 269 | ## Action guidelines 270 | 271 | - Always add `"use node";` to the top of files containing actions that use Node.js built-in modules. 272 | - Never use `ctx.db` inside of an action. Actions don't have access to the database. 273 | - Below is an example of the syntax for an action: 274 | 275 | ```ts 276 | import { action } from "./_generated/server"; 277 | 278 | export const exampleAction = action({ 279 | args: {}, 280 | returns: v.null(), 281 | handler: async (ctx, args) => { 282 | console.log("This action does not return anything"); 283 | return null; 284 | }, 285 | }); 286 | ``` 287 | 288 | ## Scheduling guidelines 289 | 290 | ### Cron guidelines 291 | 292 | - Only use the `crons.interval` or `crons.cron` methods to schedule cron jobs. Do NOT use the `crons.hourly`, `crons.daily`, or `crons.weekly` helpers. 293 | - Both cron methods take in a FunctionReference. Do NOT try to pass the function directly into one of these methods. 294 | - Define crons by declaring the top-level `crons` object, calling some methods on it, and then exporting it as default. For example, 295 | 296 | ```ts 297 | import { cronJobs } from "convex/server"; 298 | import { internal } from "./_generated/api"; 299 | import { internalAction } from "./_generated/server"; 300 | 301 | const empty = internalAction({ 302 | args: {}, 303 | returns: v.null(), 304 | handler: async (ctx, args) => { 305 | console.log("empty"); 306 | }, 307 | }); 308 | 309 | const crons = cronJobs(); 310 | 311 | // Run `internal.crons.empty` every two hours. 312 | crons.interval("delete inactive users", { hours: 2 }, internal.crons.empty, {}); 313 | 314 | export default crons; 315 | ``` 316 | 317 | - You can register Convex functions within `crons.ts` just like any other file. 318 | - If a cron calls an internal function, always import the `internal` object from '\_generated/api', even if the internal function is registered in the same file. 319 | 320 | ## File storage guidelines 321 | 322 | - Convex includes file storage for large files like images, videos, and PDFs. 323 | - The `ctx.storage.getUrl()` method returns a signed URL for a given file. It returns `null` if the file doesn't exist. 324 | - Do NOT use the deprecated `ctx.storage.getMetadata` call for loading a file's metadata. 325 | 326 | Instead, query the `_storage` system table. For example, you can use `ctx.db.system.get` to get an `Id<"_storage">`. 327 | 328 | ``` 329 | import { query } from "./_generated/server"; 330 | import { Id } from "./_generated/dataModel"; 331 | 332 | type FileMetadata = { 333 | _id: Id<"_storage">; 334 | _creationTime: number; 335 | contentType?: string; 336 | sha256: string; 337 | size: number; 338 | } 339 | 340 | export const exampleQuery = query({ 341 | args: { fileId: v.id("_storage") }, 342 | returns: v.null(); 343 | handler: async (ctx, args) => { 344 | const metadata: FileMetadata | null = await ctx.db.system.get(args.fileId); 345 | console.log(metadata); 346 | return null; 347 | }, 348 | }); 349 | ``` 350 | 351 | - Convex storage stores items as `Blob` objects. You must convert all items to/from a `Blob` when using Convex storage. 352 | 353 | # Examples: 354 | 355 | ## Example: chat-app 356 | 357 | ### Task 358 | 359 | ``` 360 | Create a real-time chat application backend with AI responses. The app should: 361 | - Allow creating users with names 362 | - Support multiple chat channels 363 | - Enable users to send messages to channels 364 | - Automatically generate AI responses to user messages 365 | - Show recent message history 366 | 367 | The backend should provide APIs for: 368 | 1. User management (creation) 369 | 2. Channel management (creation) 370 | 3. Message operations (sending, listing) 371 | 4. AI response generation using OpenAI's GPT-4 372 | 373 | Messages should be stored with their channel, author, and content. The system should maintain message order 374 | and limit history display to the 10 most recent messages per channel. 375 | 376 | ``` 377 | 378 | ### Analysis 379 | 380 | 1. Task Requirements Summary: 381 | 382 | - Build a real-time chat backend with AI integration 383 | - Support user creation 384 | - Enable channel-based conversations 385 | - Store and retrieve messages with proper ordering 386 | - Generate AI responses automatically 387 | 388 | 2. Main Components Needed: 389 | 390 | - Database tables: users, channels, messages 391 | - Public APIs for user/channel management 392 | - Message handling functions 393 | - Internal AI response generation system 394 | - Context loading for AI responses 395 | 396 | 3. Public API and Internal Functions Design: 397 | Public Mutations: 398 | 399 | - createUser: 400 | - file path: convex/index.ts 401 | - arguments: {name: v.string()} 402 | - returns: v.object({userId: v.id("users")}) 403 | - purpose: Create a new user with a given name 404 | - createChannel: 405 | - file path: convex/index.ts 406 | - arguments: {name: v.string()} 407 | - returns: v.object({channelId: v.id("channels")}) 408 | - purpose: Create a new channel with a given name 409 | - sendMessage: 410 | - file path: convex/index.ts 411 | - arguments: {channelId: v.id("channels"), authorId: v.id("users"), content: v.string()} 412 | - returns: v.null() 413 | - purpose: Send a message to a channel and schedule a response from the AI 414 | 415 | Public Queries: 416 | 417 | - listMessages: 418 | - file path: convex/index.ts 419 | - arguments: {channelId: v.id("channels")} 420 | - returns: v.array(v.object({ 421 | \_id: v.id("messages"), 422 | \_creationTime: v.number(), 423 | channelId: v.id("channels"), 424 | authorId: v.optional(v.id("users")), 425 | content: v.string(), 426 | })) 427 | - purpose: List the 10 most recent messages from a channel in descending creation order 428 | 429 | Internal Functions: 430 | 431 | - generateResponse: 432 | - file path: convex/index.ts 433 | - arguments: {channelId: v.id("channels")} 434 | - returns: v.null() 435 | - purpose: Generate a response from the AI for a given channel 436 | - loadContext: 437 | - file path: convex/index.ts 438 | - arguments: {channelId: v.id("channels")} 439 | - returns: v.array(v.object({ 440 | \_id: v.id("messages"), 441 | \_creationTime: v.number(), 442 | channelId: v.id("channels"), 443 | authorId: v.optional(v.id("users")), 444 | content: v.string(), 445 | })) 446 | - writeAgentResponse: 447 | - file path: convex/index.ts 448 | - arguments: {channelId: v.id("channels"), content: v.string()} 449 | - returns: v.null() 450 | - purpose: Write an AI response to a given channel 451 | 452 | 4. Schema Design: 453 | 454 | - users 455 | - validator: { name: v.string() } 456 | - indexes: 457 | - channels 458 | - validator: { name: v.string() } 459 | - indexes: 460 | - messages 461 | - validator: { channelId: v.id("channels"), authorId: v.optional(v.id("users")), content: v.string() } 462 | - indexes 463 | - by_channel: ["channelId"] 464 | 465 | 5. Background Processing: 466 | 467 | - AI response generation runs asynchronously after each user message 468 | - Uses OpenAI's GPT-4 to generate contextual responses 469 | - Maintains conversation context using recent message history 470 | 471 | ### Implementation 472 | 473 | #### package.json 474 | 475 | ```typescript 476 | { 477 | "name": "chat-app", 478 | "description": "This example shows how to build a chat app without authentication.", 479 | "version": "1.0.0", 480 | "dependencies": { 481 | "convex": "^1.17.4", 482 | "openai": "^4.79.0" 483 | }, 484 | "devDependencies": { 485 | "typescript": "^5.7.3" 486 | } 487 | } 488 | ``` 489 | 490 | #### tsconfig.json 491 | 492 | ```typescript 493 | { 494 | "compilerOptions": { 495 | "target": "ESNext", 496 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 497 | "skipLibCheck": true, 498 | "allowSyntheticDefaultImports": true, 499 | "strict": true, 500 | "forceConsistentCasingInFileNames": true, 501 | "module": "ESNext", 502 | "moduleResolution": "Bundler", 503 | "resolveJsonModule": true, 504 | "isolatedModules": true, 505 | "allowImportingTsExtensions": true, 506 | "noEmit": true, 507 | "jsx": "react-jsx" 508 | }, 509 | "exclude": ["convex"], 510 | "include": ["**/src/**/*.tsx", "**/src/**/*.ts", "vite.config.ts"] 511 | } 512 | ``` 513 | 514 | #### convex/index.ts 515 | 516 | ```typescript 517 | import { 518 | query, 519 | mutation, 520 | internalQuery, 521 | internalMutation, 522 | internalAction, 523 | } from "./_generated/server"; 524 | import { v } from "convex/values"; 525 | import OpenAI from "openai"; 526 | import { internal } from "./_generated/api"; 527 | 528 | /** 529 | * Create a user with a given name. 530 | */ 531 | export const createUser = mutation({ 532 | args: { 533 | name: v.string(), 534 | }, 535 | returns: v.id("users"), 536 | handler: async (ctx, args) => { 537 | return await ctx.db.insert("users", { name: args.name }); 538 | }, 539 | }); 540 | 541 | /** 542 | * Create a channel with a given name. 543 | */ 544 | export const createChannel = mutation({ 545 | args: { 546 | name: v.string(), 547 | }, 548 | returns: v.id("channels"), 549 | handler: async (ctx, args) => { 550 | return await ctx.db.insert("channels", { name: args.name }); 551 | }, 552 | }); 553 | 554 | /** 555 | * List the 10 most recent messages from a channel in descending creation order. 556 | */ 557 | export const listMessages = query({ 558 | args: { 559 | channelId: v.id("channels"), 560 | }, 561 | returns: v.array( 562 | v.object({ 563 | _id: v.id("messages"), 564 | _creationTime: v.number(), 565 | channelId: v.id("channels"), 566 | authorId: v.optional(v.id("users")), 567 | content: v.string(), 568 | }) 569 | ), 570 | handler: async (ctx, args) => { 571 | const messages = await ctx.db 572 | .query("messages") 573 | .withIndex("by_channel", (q) => q.eq("channelId", args.channelId)) 574 | .order("desc") 575 | .take(10); 576 | return messages; 577 | }, 578 | }); 579 | 580 | /** 581 | * Send a message to a channel and schedule a response from the AI. 582 | */ 583 | export const sendMessage = mutation({ 584 | args: { 585 | channelId: v.id("channels"), 586 | authorId: v.id("users"), 587 | content: v.string(), 588 | }, 589 | returns: v.null(), 590 | handler: async (ctx, args) => { 591 | const channel = await ctx.db.get(args.channelId); 592 | if (!channel) { 593 | throw new Error("Channel not found"); 594 | } 595 | const user = await ctx.db.get(args.authorId); 596 | if (!user) { 597 | throw new Error("User not found"); 598 | } 599 | await ctx.db.insert("messages", { 600 | channelId: args.channelId, 601 | authorId: args.authorId, 602 | content: args.content, 603 | }); 604 | await ctx.scheduler.runAfter(0, internal.index.generateResponse, { 605 | channelId: args.channelId, 606 | }); 607 | return null; 608 | }, 609 | }); 610 | 611 | const openai = new OpenAI(); 612 | 613 | export const generateResponse = internalAction({ 614 | args: { 615 | channelId: v.id("channels"), 616 | }, 617 | returns: v.null(), 618 | handler: async (ctx, args) => { 619 | const context = await ctx.runQuery(internal.index.loadContext, { 620 | channelId: args.channelId, 621 | }); 622 | const response = await openai.chat.completions.create({ 623 | model: "gpt-4o", 624 | messages: context, 625 | }); 626 | const content = response.choices[0].message.content; 627 | if (!content) { 628 | throw new Error("No content in response"); 629 | } 630 | await ctx.runMutation(internal.index.writeAgentResponse, { 631 | channelId: args.channelId, 632 | content, 633 | }); 634 | return null; 635 | }, 636 | }); 637 | 638 | export const loadContext = internalQuery({ 639 | args: { 640 | channelId: v.id("channels"), 641 | }, 642 | returns: v.array( 643 | v.object({ 644 | role: v.union(v.literal("user"), v.literal("assistant")), 645 | content: v.string(), 646 | }) 647 | ), 648 | handler: async (ctx, args) => { 649 | const channel = await ctx.db.get(args.channelId); 650 | if (!channel) { 651 | throw new Error("Channel not found"); 652 | } 653 | const messages = await ctx.db 654 | .query("messages") 655 | .withIndex("by_channel", (q) => q.eq("channelId", args.channelId)) 656 | .order("desc") 657 | .take(10); 658 | 659 | const result = []; 660 | for (const message of messages) { 661 | if (message.authorId) { 662 | const user = await ctx.db.get(message.authorId); 663 | if (!user) { 664 | throw new Error("User not found"); 665 | } 666 | result.push({ 667 | role: "user" as const, 668 | content: `${user.name}: ${message.content}`, 669 | }); 670 | } else { 671 | result.push({ role: "assistant" as const, content: message.content }); 672 | } 673 | } 674 | return result; 675 | }, 676 | }); 677 | 678 | export const writeAgentResponse = internalMutation({ 679 | args: { 680 | channelId: v.id("channels"), 681 | content: v.string(), 682 | }, 683 | returns: v.null(), 684 | handler: async (ctx, args) => { 685 | await ctx.db.insert("messages", { 686 | channelId: args.channelId, 687 | content: args.content, 688 | }); 689 | return null; 690 | }, 691 | }); 692 | ``` 693 | 694 | #### convex/schema.ts 695 | 696 | ```typescript 697 | import { defineSchema, defineTable } from "convex/server"; 698 | import { v } from "convex/values"; 699 | 700 | export default defineSchema({ 701 | channels: defineTable({ 702 | name: v.string(), 703 | }), 704 | 705 | users: defineTable({ 706 | name: v.string(), 707 | }), 708 | 709 | messages: defineTable({ 710 | channelId: v.id("channels"), 711 | authorId: v.optional(v.id("users")), 712 | content: v.string(), 713 | }).index("by_channel", ["channelId"]), 714 | }); 715 | ``` 716 | 717 | #### src/App.tsx 718 | 719 | ```typescript 720 | export default function App() { 721 | return
Hello World
; 722 | } 723 | ``` 724 | -------------------------------------------------------------------------------- /packages/backend/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 * as auth from "../auth.js"; 12 | import type * as http from "../http.js"; 13 | import type * as lib_email from "../lib/email.js"; 14 | 15 | import type { 16 | ApiFromModules, 17 | FilterApi, 18 | FunctionReference, 19 | } from "convex/server"; 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 | "lib/email": typeof lib_email; 33 | }>; 34 | declare const fullApiWithMounts: typeof fullApi; 35 | 36 | export declare const api: FilterApi< 37 | typeof fullApiWithMounts, 38 | FunctionReference 39 | >; 40 | export declare const internal: FilterApi< 41 | typeof fullApiWithMounts, 42 | FunctionReference 43 | >; 44 | 45 | export declare const components: { 46 | resend: { 47 | lib: { 48 | cancelEmail: FunctionReference< 49 | "mutation", 50 | "internal", 51 | { emailId: string }, 52 | null 53 | >; 54 | cleanupAbandonedEmails: FunctionReference< 55 | "mutation", 56 | "internal", 57 | { olderThan?: number }, 58 | null 59 | >; 60 | cleanupOldEmails: FunctionReference< 61 | "mutation", 62 | "internal", 63 | { olderThan?: number }, 64 | null 65 | >; 66 | get: FunctionReference< 67 | "query", 68 | "internal", 69 | { emailId: string }, 70 | { 71 | complained: boolean; 72 | createdAt: number; 73 | errorMessage?: string; 74 | finalizedAt: number; 75 | from: string; 76 | headers?: Array<{ name: string; value: string }>; 77 | html?: string; 78 | opened: boolean; 79 | replyTo: Array; 80 | resendId?: string; 81 | segment: number; 82 | status: 83 | | "waiting" 84 | | "queued" 85 | | "cancelled" 86 | | "sent" 87 | | "delivered" 88 | | "delivery_delayed" 89 | | "bounced" 90 | | "failed"; 91 | subject: string; 92 | text?: string; 93 | to: string; 94 | } | null 95 | >; 96 | getStatus: FunctionReference< 97 | "query", 98 | "internal", 99 | { emailId: string }, 100 | { 101 | complained: boolean; 102 | errorMessage: string | null; 103 | opened: boolean; 104 | status: 105 | | "waiting" 106 | | "queued" 107 | | "cancelled" 108 | | "sent" 109 | | "delivered" 110 | | "delivery_delayed" 111 | | "bounced" 112 | | "failed"; 113 | } | null 114 | >; 115 | handleEmailEvent: FunctionReference< 116 | "mutation", 117 | "internal", 118 | { event: any }, 119 | null 120 | >; 121 | sendEmail: FunctionReference< 122 | "mutation", 123 | "internal", 124 | { 125 | from: string; 126 | headers?: Array<{ name: string; value: string }>; 127 | html?: string; 128 | options: { 129 | apiKey: string; 130 | initialBackoffMs: number; 131 | onEmailEvent?: { fnHandle: string }; 132 | retryAttempts: number; 133 | testMode: boolean; 134 | }; 135 | replyTo?: Array; 136 | subject: string; 137 | text?: string; 138 | to: string; 139 | }, 140 | string 141 | >; 142 | }; 143 | }; 144 | betterAuth: { 145 | adapterTest: { 146 | count: FunctionReference<"query", "internal", any, any>; 147 | create: FunctionReference<"mutation", "internal", any, any>; 148 | delete: FunctionReference<"mutation", "internal", any, any>; 149 | deleteMany: FunctionReference<"mutation", "internal", any, any>; 150 | findMany: FunctionReference<"query", "internal", any, any>; 151 | findOne: FunctionReference<"query", "internal", any, any>; 152 | isAuthenticated: FunctionReference<"query", "internal", {}, any>; 153 | update: FunctionReference<"mutation", "internal", any, any>; 154 | updateMany: FunctionReference<"mutation", "internal", any, any>; 155 | }; 156 | lib: { 157 | create: FunctionReference< 158 | "mutation", 159 | "internal", 160 | { 161 | input: 162 | | { 163 | data: { 164 | banExpires?: null | number; 165 | banReason?: null | string; 166 | banned?: null | boolean; 167 | createdAt: number; 168 | displayUsername?: null | string; 169 | email: string; 170 | emailVerified: boolean; 171 | image?: null | string; 172 | isAnonymous?: null | boolean; 173 | name: string; 174 | phoneNumber?: null | string; 175 | phoneNumberVerified?: null | boolean; 176 | role?: null | string; 177 | stripeCustomerId?: null | string; 178 | teamId?: null | string; 179 | twoFactorEnabled?: null | boolean; 180 | updatedAt: number; 181 | userId?: null | string; 182 | username?: null | string; 183 | }; 184 | model: "user"; 185 | where?: Array<{ 186 | connector?: "AND" | "OR"; 187 | field: string; 188 | operator?: 189 | | "lt" 190 | | "lte" 191 | | "gt" 192 | | "gte" 193 | | "eq" 194 | | "in" 195 | | "ne" 196 | | "contains" 197 | | "starts_with" 198 | | "ends_with"; 199 | value: 200 | | string 201 | | number 202 | | boolean 203 | | Array 204 | | Array 205 | | null; 206 | }>; 207 | } 208 | | { 209 | data: { 210 | activeOrganizationId?: null | string; 211 | activeTeamId?: null | string; 212 | createdAt: number; 213 | expiresAt: number; 214 | impersonatedBy?: null | string; 215 | ipAddress?: null | string; 216 | token: string; 217 | updatedAt: number; 218 | userAgent?: null | string; 219 | userId: string; 220 | }; 221 | model: "session"; 222 | where?: Array<{ 223 | connector?: "AND" | "OR"; 224 | field: string; 225 | operator?: 226 | | "lt" 227 | | "lte" 228 | | "gt" 229 | | "gte" 230 | | "eq" 231 | | "in" 232 | | "ne" 233 | | "contains" 234 | | "starts_with" 235 | | "ends_with"; 236 | value: 237 | | string 238 | | number 239 | | boolean 240 | | Array 241 | | Array 242 | | null; 243 | }>; 244 | } 245 | | { 246 | data: { 247 | accessToken?: null | string; 248 | accessTokenExpiresAt?: null | number; 249 | accountId: string; 250 | createdAt: number; 251 | idToken?: null | string; 252 | password?: null | string; 253 | providerId: string; 254 | refreshToken?: null | string; 255 | refreshTokenExpiresAt?: null | number; 256 | scope?: null | string; 257 | updatedAt: number; 258 | userId: string; 259 | }; 260 | model: "account"; 261 | where?: Array<{ 262 | connector?: "AND" | "OR"; 263 | field: string; 264 | operator?: 265 | | "lt" 266 | | "lte" 267 | | "gt" 268 | | "gte" 269 | | "eq" 270 | | "in" 271 | | "ne" 272 | | "contains" 273 | | "starts_with" 274 | | "ends_with"; 275 | value: 276 | | string 277 | | number 278 | | boolean 279 | | Array 280 | | Array 281 | | null; 282 | }>; 283 | } 284 | | { 285 | data: { 286 | createdAt?: null | number; 287 | expiresAt: number; 288 | identifier: string; 289 | updatedAt?: null | number; 290 | value: string; 291 | }; 292 | model: "verification"; 293 | where?: Array<{ 294 | connector?: "AND" | "OR"; 295 | field: string; 296 | operator?: 297 | | "lt" 298 | | "lte" 299 | | "gt" 300 | | "gte" 301 | | "eq" 302 | | "in" 303 | | "ne" 304 | | "contains" 305 | | "starts_with" 306 | | "ends_with"; 307 | value: 308 | | string 309 | | number 310 | | boolean 311 | | Array 312 | | Array 313 | | null; 314 | }>; 315 | } 316 | | { 317 | data: { backupCodes: string; secret: string; userId: string }; 318 | model: "twoFactor"; 319 | where?: Array<{ 320 | connector?: "AND" | "OR"; 321 | field: string; 322 | operator?: 323 | | "lt" 324 | | "lte" 325 | | "gt" 326 | | "gte" 327 | | "eq" 328 | | "in" 329 | | "ne" 330 | | "contains" 331 | | "starts_with" 332 | | "ends_with"; 333 | value: 334 | | string 335 | | number 336 | | boolean 337 | | Array 338 | | Array 339 | | null; 340 | }>; 341 | } 342 | | { 343 | data: { 344 | aaguid?: null | string; 345 | backedUp: boolean; 346 | counter: number; 347 | createdAt?: null | number; 348 | credentialID: string; 349 | deviceType: string; 350 | name?: null | string; 351 | publicKey: string; 352 | transports?: null | string; 353 | userId: string; 354 | }; 355 | model: "passkey"; 356 | where?: Array<{ 357 | connector?: "AND" | "OR"; 358 | field: string; 359 | operator?: 360 | | "lt" 361 | | "lte" 362 | | "gt" 363 | | "gte" 364 | | "eq" 365 | | "in" 366 | | "ne" 367 | | "contains" 368 | | "starts_with" 369 | | "ends_with"; 370 | value: 371 | | string 372 | | number 373 | | boolean 374 | | Array 375 | | Array 376 | | null; 377 | }>; 378 | } 379 | | { 380 | data: { 381 | createdAt: number; 382 | enabled?: null | boolean; 383 | expiresAt?: null | number; 384 | key: string; 385 | lastRefillAt?: null | number; 386 | lastRequest?: null | number; 387 | metadata?: null | string; 388 | name?: null | string; 389 | permissions?: null | string; 390 | prefix?: null | string; 391 | rateLimitEnabled?: null | boolean; 392 | rateLimitMax?: null | number; 393 | rateLimitTimeWindow?: null | number; 394 | refillAmount?: null | number; 395 | refillInterval?: null | number; 396 | remaining?: null | number; 397 | requestCount?: null | number; 398 | start?: null | string; 399 | updatedAt: number; 400 | userId: string; 401 | }; 402 | model: "apikey"; 403 | where?: Array<{ 404 | connector?: "AND" | "OR"; 405 | field: string; 406 | operator?: 407 | | "lt" 408 | | "lte" 409 | | "gt" 410 | | "gte" 411 | | "eq" 412 | | "in" 413 | | "ne" 414 | | "contains" 415 | | "starts_with" 416 | | "ends_with"; 417 | value: 418 | | string 419 | | number 420 | | boolean 421 | | Array 422 | | Array 423 | | null; 424 | }>; 425 | } 426 | | { 427 | data: { 428 | clientId?: null | string; 429 | clientSecret?: null | string; 430 | createdAt?: null | number; 431 | disabled?: null | boolean; 432 | icon?: null | string; 433 | metadata?: null | string; 434 | name?: null | string; 435 | redirectURLs?: null | string; 436 | type?: null | string; 437 | updatedAt?: null | number; 438 | userId?: null | string; 439 | }; 440 | model: "oauthApplication"; 441 | where?: Array<{ 442 | connector?: "AND" | "OR"; 443 | field: string; 444 | operator?: 445 | | "lt" 446 | | "lte" 447 | | "gt" 448 | | "gte" 449 | | "eq" 450 | | "in" 451 | | "ne" 452 | | "contains" 453 | | "starts_with" 454 | | "ends_with"; 455 | value: 456 | | string 457 | | number 458 | | boolean 459 | | Array 460 | | Array 461 | | null; 462 | }>; 463 | } 464 | | { 465 | data: { 466 | accessToken?: null | string; 467 | accessTokenExpiresAt?: null | number; 468 | clientId?: null | string; 469 | createdAt?: null | number; 470 | refreshToken?: null | string; 471 | refreshTokenExpiresAt?: null | number; 472 | scopes?: null | string; 473 | updatedAt?: null | number; 474 | userId?: null | string; 475 | }; 476 | model: "oauthAccessToken"; 477 | where?: Array<{ 478 | connector?: "AND" | "OR"; 479 | field: string; 480 | operator?: 481 | | "lt" 482 | | "lte" 483 | | "gt" 484 | | "gte" 485 | | "eq" 486 | | "in" 487 | | "ne" 488 | | "contains" 489 | | "starts_with" 490 | | "ends_with"; 491 | value: 492 | | string 493 | | number 494 | | boolean 495 | | Array 496 | | Array 497 | | null; 498 | }>; 499 | } 500 | | { 501 | data: { 502 | clientId?: null | string; 503 | consentGiven?: null | boolean; 504 | createdAt?: null | number; 505 | scopes?: null | string; 506 | updatedAt?: null | number; 507 | userId?: null | string; 508 | }; 509 | model: "oauthConsent"; 510 | where?: Array<{ 511 | connector?: "AND" | "OR"; 512 | field: string; 513 | operator?: 514 | | "lt" 515 | | "lte" 516 | | "gt" 517 | | "gte" 518 | | "eq" 519 | | "in" 520 | | "ne" 521 | | "contains" 522 | | "starts_with" 523 | | "ends_with"; 524 | value: 525 | | string 526 | | number 527 | | boolean 528 | | Array 529 | | Array 530 | | null; 531 | }>; 532 | } 533 | | { 534 | data: { 535 | createdAt: number; 536 | logo?: null | string; 537 | metadata?: null | string; 538 | name: string; 539 | slug?: null | string; 540 | }; 541 | model: "organization"; 542 | where?: Array<{ 543 | connector?: "AND" | "OR"; 544 | field: string; 545 | operator?: 546 | | "lt" 547 | | "lte" 548 | | "gt" 549 | | "gte" 550 | | "eq" 551 | | "in" 552 | | "ne" 553 | | "contains" 554 | | "starts_with" 555 | | "ends_with"; 556 | value: 557 | | string 558 | | number 559 | | boolean 560 | | Array 561 | | Array 562 | | null; 563 | }>; 564 | } 565 | | { 566 | data: { 567 | createdAt: number; 568 | organizationId: string; 569 | role: string; 570 | userId: string; 571 | }; 572 | model: "member"; 573 | where?: Array<{ 574 | connector?: "AND" | "OR"; 575 | field: string; 576 | operator?: 577 | | "lt" 578 | | "lte" 579 | | "gt" 580 | | "gte" 581 | | "eq" 582 | | "in" 583 | | "ne" 584 | | "contains" 585 | | "starts_with" 586 | | "ends_with"; 587 | value: 588 | | string 589 | | number 590 | | boolean 591 | | Array 592 | | Array 593 | | null; 594 | }>; 595 | } 596 | | { 597 | data: { 598 | email: string; 599 | expiresAt: number; 600 | inviterId: string; 601 | organizationId: string; 602 | role?: null | string; 603 | status: string; 604 | teamId?: null | string; 605 | }; 606 | model: "invitation"; 607 | where?: Array<{ 608 | connector?: "AND" | "OR"; 609 | field: string; 610 | operator?: 611 | | "lt" 612 | | "lte" 613 | | "gt" 614 | | "gte" 615 | | "eq" 616 | | "in" 617 | | "ne" 618 | | "contains" 619 | | "starts_with" 620 | | "ends_with"; 621 | value: 622 | | string 623 | | number 624 | | boolean 625 | | Array 626 | | Array 627 | | null; 628 | }>; 629 | } 630 | | { 631 | data: { 632 | createdAt: number; 633 | name: string; 634 | organizationId: string; 635 | updatedAt?: null | number; 636 | }; 637 | model: "team"; 638 | where?: Array<{ 639 | connector?: "AND" | "OR"; 640 | field: string; 641 | operator?: 642 | | "lt" 643 | | "lte" 644 | | "gt" 645 | | "gte" 646 | | "eq" 647 | | "in" 648 | | "ne" 649 | | "contains" 650 | | "starts_with" 651 | | "ends_with"; 652 | value: 653 | | string 654 | | number 655 | | boolean 656 | | Array 657 | | Array 658 | | null; 659 | }>; 660 | } 661 | | { 662 | data: { 663 | createdAt?: null | number; 664 | teamId: string; 665 | userId: string; 666 | }; 667 | model: "teamMember"; 668 | where?: Array<{ 669 | connector?: "AND" | "OR"; 670 | field: string; 671 | operator?: 672 | | "lt" 673 | | "lte" 674 | | "gt" 675 | | "gte" 676 | | "eq" 677 | | "in" 678 | | "ne" 679 | | "contains" 680 | | "starts_with" 681 | | "ends_with"; 682 | value: 683 | | string 684 | | number 685 | | boolean 686 | | Array 687 | | Array 688 | | null; 689 | }>; 690 | } 691 | | { 692 | data: { 693 | domain: string; 694 | issuer: string; 695 | oidcConfig?: null | string; 696 | organizationId?: null | string; 697 | providerId: string; 698 | samlConfig?: null | string; 699 | userId?: null | string; 700 | }; 701 | model: "ssoProvider"; 702 | where?: Array<{ 703 | connector?: "AND" | "OR"; 704 | field: string; 705 | operator?: 706 | | "lt" 707 | | "lte" 708 | | "gt" 709 | | "gte" 710 | | "eq" 711 | | "in" 712 | | "ne" 713 | | "contains" 714 | | "starts_with" 715 | | "ends_with"; 716 | value: 717 | | string 718 | | number 719 | | boolean 720 | | Array 721 | | Array 722 | | null; 723 | }>; 724 | } 725 | | { 726 | data: { 727 | createdAt: number; 728 | privateKey: string; 729 | publicKey: string; 730 | }; 731 | model: "jwks"; 732 | where?: Array<{ 733 | connector?: "AND" | "OR"; 734 | field: string; 735 | operator?: 736 | | "lt" 737 | | "lte" 738 | | "gt" 739 | | "gte" 740 | | "eq" 741 | | "in" 742 | | "ne" 743 | | "contains" 744 | | "starts_with" 745 | | "ends_with"; 746 | value: 747 | | string 748 | | number 749 | | boolean 750 | | Array 751 | | Array 752 | | null; 753 | }>; 754 | } 755 | | { 756 | data: { 757 | cancelAtPeriodEnd?: null | boolean; 758 | periodEnd?: null | number; 759 | periodStart?: null | number; 760 | plan: string; 761 | referenceId: string; 762 | seats?: null | number; 763 | status?: null | string; 764 | stripeCustomerId?: null | string; 765 | stripeSubscriptionId?: null | string; 766 | }; 767 | model: "subscription"; 768 | where?: Array<{ 769 | connector?: "AND" | "OR"; 770 | field: string; 771 | operator?: 772 | | "lt" 773 | | "lte" 774 | | "gt" 775 | | "gte" 776 | | "eq" 777 | | "in" 778 | | "ne" 779 | | "contains" 780 | | "starts_with" 781 | | "ends_with"; 782 | value: 783 | | string 784 | | number 785 | | boolean 786 | | Array 787 | | Array 788 | | null; 789 | }>; 790 | } 791 | | { 792 | data: { 793 | address: string; 794 | chainId: number; 795 | createdAt: number; 796 | isPrimary?: null | boolean; 797 | userId: string; 798 | }; 799 | model: "walletAddress"; 800 | where?: Array<{ 801 | connector?: "AND" | "OR"; 802 | field: string; 803 | operator?: 804 | | "lt" 805 | | "lte" 806 | | "gt" 807 | | "gte" 808 | | "eq" 809 | | "in" 810 | | "ne" 811 | | "contains" 812 | | "starts_with" 813 | | "ends_with"; 814 | value: 815 | | string 816 | | number 817 | | boolean 818 | | Array 819 | | Array 820 | | null; 821 | }>; 822 | } 823 | | { 824 | data: { 825 | count?: null | number; 826 | key?: null | string; 827 | lastRequest?: null | number; 828 | }; 829 | model: "rateLimit"; 830 | where?: Array<{ 831 | connector?: "AND" | "OR"; 832 | field: string; 833 | operator?: 834 | | "lt" 835 | | "lte" 836 | | "gt" 837 | | "gte" 838 | | "eq" 839 | | "in" 840 | | "ne" 841 | | "contains" 842 | | "starts_with" 843 | | "ends_with"; 844 | value: 845 | | string 846 | | number 847 | | boolean 848 | | Array 849 | | Array 850 | | null; 851 | }>; 852 | }; 853 | }, 854 | any 855 | >; 856 | deleteMany: FunctionReference< 857 | "mutation", 858 | "internal", 859 | { 860 | limit?: number; 861 | model: string; 862 | offset?: number; 863 | paginationOpts: { 864 | cursor: string | null; 865 | endCursor?: string | null; 866 | id?: number; 867 | maximumBytesRead?: number; 868 | maximumRowsRead?: number; 869 | numItems: number; 870 | }; 871 | select?: Array; 872 | sortBy?: { direction: "asc" | "desc"; field: string }; 873 | unique?: boolean; 874 | where?: Array<{ 875 | connector?: "AND" | "OR"; 876 | field: string; 877 | operator?: 878 | | "lt" 879 | | "lte" 880 | | "gt" 881 | | "gte" 882 | | "eq" 883 | | "in" 884 | | "ne" 885 | | "contains" 886 | | "starts_with" 887 | | "ends_with"; 888 | value: 889 | | string 890 | | number 891 | | boolean 892 | | Array 893 | | Array 894 | | null; 895 | }>; 896 | }, 897 | any 898 | >; 899 | deleteOne: FunctionReference< 900 | "mutation", 901 | "internal", 902 | { 903 | limit?: number; 904 | model: string; 905 | offset?: number; 906 | select?: Array; 907 | sortBy?: { direction: "asc" | "desc"; field: string }; 908 | unique?: boolean; 909 | where?: Array<{ 910 | connector?: "AND" | "OR"; 911 | field: string; 912 | operator?: 913 | | "lt" 914 | | "lte" 915 | | "gt" 916 | | "gte" 917 | | "eq" 918 | | "in" 919 | | "ne" 920 | | "contains" 921 | | "starts_with" 922 | | "ends_with"; 923 | value: 924 | | string 925 | | number 926 | | boolean 927 | | Array 928 | | Array 929 | | null; 930 | }>; 931 | }, 932 | any 933 | >; 934 | findMany: FunctionReference< 935 | "query", 936 | "internal", 937 | { 938 | limit?: number; 939 | model: string; 940 | offset?: number; 941 | paginationOpts: { 942 | cursor: string | null; 943 | endCursor?: string | null; 944 | id?: number; 945 | maximumBytesRead?: number; 946 | maximumRowsRead?: number; 947 | numItems: number; 948 | }; 949 | select?: Array; 950 | sortBy?: { direction: "asc" | "desc"; field: string }; 951 | unique?: boolean; 952 | where?: Array<{ 953 | connector?: "AND" | "OR"; 954 | field: string; 955 | operator?: 956 | | "lt" 957 | | "lte" 958 | | "gt" 959 | | "gte" 960 | | "eq" 961 | | "in" 962 | | "ne" 963 | | "contains" 964 | | "starts_with" 965 | | "ends_with"; 966 | value: 967 | | string 968 | | number 969 | | boolean 970 | | Array 971 | | Array 972 | | null; 973 | }>; 974 | }, 975 | any 976 | >; 977 | findOne: FunctionReference< 978 | "query", 979 | "internal", 980 | { 981 | limit?: number; 982 | model: string; 983 | offset?: number; 984 | select?: Array; 985 | sortBy?: { direction: "asc" | "desc"; field: string }; 986 | unique?: boolean; 987 | where?: Array<{ 988 | connector?: "AND" | "OR"; 989 | field: string; 990 | operator?: 991 | | "lt" 992 | | "lte" 993 | | "gt" 994 | | "gte" 995 | | "eq" 996 | | "in" 997 | | "ne" 998 | | "contains" 999 | | "starts_with" 1000 | | "ends_with"; 1001 | value: 1002 | | string 1003 | | number 1004 | | boolean 1005 | | Array 1006 | | Array 1007 | | null; 1008 | }>; 1009 | }, 1010 | any 1011 | >; 1012 | getCurrentSession: FunctionReference<"query", "internal", {}, any>; 1013 | updateMany: FunctionReference< 1014 | "mutation", 1015 | "internal", 1016 | { 1017 | input: 1018 | | { 1019 | limit?: number; 1020 | model: "user"; 1021 | offset?: number; 1022 | paginationOpts: { 1023 | cursor: string | null; 1024 | endCursor?: string | null; 1025 | id?: number; 1026 | maximumBytesRead?: number; 1027 | maximumRowsRead?: number; 1028 | numItems: number; 1029 | }; 1030 | select?: Array; 1031 | sortBy?: { direction: "asc" | "desc"; field: string }; 1032 | unique?: boolean; 1033 | update: { 1034 | banExpires?: null | number; 1035 | banReason?: null | string; 1036 | banned?: null | boolean; 1037 | createdAt?: number; 1038 | displayUsername?: null | string; 1039 | email?: string; 1040 | emailVerified?: boolean; 1041 | image?: null | string; 1042 | isAnonymous?: null | boolean; 1043 | name?: string; 1044 | phoneNumber?: null | string; 1045 | phoneNumberVerified?: null | boolean; 1046 | role?: null | string; 1047 | stripeCustomerId?: null | string; 1048 | teamId?: null | string; 1049 | twoFactorEnabled?: null | boolean; 1050 | updatedAt?: number; 1051 | userId?: null | string; 1052 | username?: null | string; 1053 | }; 1054 | where?: Array<{ 1055 | connector?: "AND" | "OR"; 1056 | field: string; 1057 | operator?: 1058 | | "lt" 1059 | | "lte" 1060 | | "gt" 1061 | | "gte" 1062 | | "eq" 1063 | | "in" 1064 | | "ne" 1065 | | "contains" 1066 | | "starts_with" 1067 | | "ends_with"; 1068 | value: 1069 | | string 1070 | | number 1071 | | boolean 1072 | | Array 1073 | | Array 1074 | | null; 1075 | }>; 1076 | } 1077 | | { 1078 | limit?: number; 1079 | model: "session"; 1080 | offset?: number; 1081 | paginationOpts: { 1082 | cursor: string | null; 1083 | endCursor?: string | null; 1084 | id?: number; 1085 | maximumBytesRead?: number; 1086 | maximumRowsRead?: number; 1087 | numItems: number; 1088 | }; 1089 | select?: Array; 1090 | sortBy?: { direction: "asc" | "desc"; field: string }; 1091 | unique?: boolean; 1092 | update: { 1093 | activeOrganizationId?: null | string; 1094 | activeTeamId?: null | string; 1095 | createdAt?: number; 1096 | expiresAt?: number; 1097 | impersonatedBy?: null | string; 1098 | ipAddress?: null | string; 1099 | token?: string; 1100 | updatedAt?: number; 1101 | userAgent?: null | string; 1102 | userId?: string; 1103 | }; 1104 | where?: Array<{ 1105 | connector?: "AND" | "OR"; 1106 | field: string; 1107 | operator?: 1108 | | "lt" 1109 | | "lte" 1110 | | "gt" 1111 | | "gte" 1112 | | "eq" 1113 | | "in" 1114 | | "ne" 1115 | | "contains" 1116 | | "starts_with" 1117 | | "ends_with"; 1118 | value: 1119 | | string 1120 | | number 1121 | | boolean 1122 | | Array 1123 | | Array 1124 | | null; 1125 | }>; 1126 | } 1127 | | { 1128 | limit?: number; 1129 | model: "account"; 1130 | offset?: number; 1131 | paginationOpts: { 1132 | cursor: string | null; 1133 | endCursor?: string | null; 1134 | id?: number; 1135 | maximumBytesRead?: number; 1136 | maximumRowsRead?: number; 1137 | numItems: number; 1138 | }; 1139 | select?: Array; 1140 | sortBy?: { direction: "asc" | "desc"; field: string }; 1141 | unique?: boolean; 1142 | update: { 1143 | accessToken?: null | string; 1144 | accessTokenExpiresAt?: null | number; 1145 | accountId?: string; 1146 | createdAt?: number; 1147 | idToken?: null | string; 1148 | password?: null | string; 1149 | providerId?: string; 1150 | refreshToken?: null | string; 1151 | refreshTokenExpiresAt?: null | number; 1152 | scope?: null | string; 1153 | updatedAt?: number; 1154 | userId?: string; 1155 | }; 1156 | where?: Array<{ 1157 | connector?: "AND" | "OR"; 1158 | field: string; 1159 | operator?: 1160 | | "lt" 1161 | | "lte" 1162 | | "gt" 1163 | | "gte" 1164 | | "eq" 1165 | | "in" 1166 | | "ne" 1167 | | "contains" 1168 | | "starts_with" 1169 | | "ends_with"; 1170 | value: 1171 | | string 1172 | | number 1173 | | boolean 1174 | | Array 1175 | | Array 1176 | | null; 1177 | }>; 1178 | } 1179 | | { 1180 | limit?: number; 1181 | model: "verification"; 1182 | offset?: number; 1183 | paginationOpts: { 1184 | cursor: string | null; 1185 | endCursor?: string | null; 1186 | id?: number; 1187 | maximumBytesRead?: number; 1188 | maximumRowsRead?: number; 1189 | numItems: number; 1190 | }; 1191 | select?: Array; 1192 | sortBy?: { direction: "asc" | "desc"; field: string }; 1193 | unique?: boolean; 1194 | update: { 1195 | createdAt?: null | number; 1196 | expiresAt?: number; 1197 | identifier?: string; 1198 | updatedAt?: null | number; 1199 | value?: string; 1200 | }; 1201 | where?: Array<{ 1202 | connector?: "AND" | "OR"; 1203 | field: string; 1204 | operator?: 1205 | | "lt" 1206 | | "lte" 1207 | | "gt" 1208 | | "gte" 1209 | | "eq" 1210 | | "in" 1211 | | "ne" 1212 | | "contains" 1213 | | "starts_with" 1214 | | "ends_with"; 1215 | value: 1216 | | string 1217 | | number 1218 | | boolean 1219 | | Array 1220 | | Array 1221 | | null; 1222 | }>; 1223 | } 1224 | | { 1225 | limit?: number; 1226 | model: "twoFactor"; 1227 | offset?: number; 1228 | paginationOpts: { 1229 | cursor: string | null; 1230 | endCursor?: string | null; 1231 | id?: number; 1232 | maximumBytesRead?: number; 1233 | maximumRowsRead?: number; 1234 | numItems: number; 1235 | }; 1236 | select?: Array; 1237 | sortBy?: { direction: "asc" | "desc"; field: string }; 1238 | unique?: boolean; 1239 | update: { 1240 | backupCodes?: string; 1241 | secret?: string; 1242 | userId?: string; 1243 | }; 1244 | where?: Array<{ 1245 | connector?: "AND" | "OR"; 1246 | field: string; 1247 | operator?: 1248 | | "lt" 1249 | | "lte" 1250 | | "gt" 1251 | | "gte" 1252 | | "eq" 1253 | | "in" 1254 | | "ne" 1255 | | "contains" 1256 | | "starts_with" 1257 | | "ends_with"; 1258 | value: 1259 | | string 1260 | | number 1261 | | boolean 1262 | | Array 1263 | | Array 1264 | | null; 1265 | }>; 1266 | } 1267 | | { 1268 | limit?: number; 1269 | model: "passkey"; 1270 | offset?: number; 1271 | paginationOpts: { 1272 | cursor: string | null; 1273 | endCursor?: string | null; 1274 | id?: number; 1275 | maximumBytesRead?: number; 1276 | maximumRowsRead?: number; 1277 | numItems: number; 1278 | }; 1279 | select?: Array; 1280 | sortBy?: { direction: "asc" | "desc"; field: string }; 1281 | unique?: boolean; 1282 | update: { 1283 | aaguid?: null | string; 1284 | backedUp?: boolean; 1285 | counter?: number; 1286 | createdAt?: null | number; 1287 | credentialID?: string; 1288 | deviceType?: string; 1289 | name?: null | string; 1290 | publicKey?: string; 1291 | transports?: null | string; 1292 | userId?: string; 1293 | }; 1294 | where?: Array<{ 1295 | connector?: "AND" | "OR"; 1296 | field: string; 1297 | operator?: 1298 | | "lt" 1299 | | "lte" 1300 | | "gt" 1301 | | "gte" 1302 | | "eq" 1303 | | "in" 1304 | | "ne" 1305 | | "contains" 1306 | | "starts_with" 1307 | | "ends_with"; 1308 | value: 1309 | | string 1310 | | number 1311 | | boolean 1312 | | Array 1313 | | Array 1314 | | null; 1315 | }>; 1316 | } 1317 | | { 1318 | limit?: number; 1319 | model: "apikey"; 1320 | offset?: number; 1321 | paginationOpts: { 1322 | cursor: string | null; 1323 | endCursor?: string | null; 1324 | id?: number; 1325 | maximumBytesRead?: number; 1326 | maximumRowsRead?: number; 1327 | numItems: number; 1328 | }; 1329 | select?: Array; 1330 | sortBy?: { direction: "asc" | "desc"; field: string }; 1331 | unique?: boolean; 1332 | update: { 1333 | createdAt?: number; 1334 | enabled?: null | boolean; 1335 | expiresAt?: null | number; 1336 | key?: string; 1337 | lastRefillAt?: null | number; 1338 | lastRequest?: null | number; 1339 | metadata?: null | string; 1340 | name?: null | string; 1341 | permissions?: null | string; 1342 | prefix?: null | string; 1343 | rateLimitEnabled?: null | boolean; 1344 | rateLimitMax?: null | number; 1345 | rateLimitTimeWindow?: null | number; 1346 | refillAmount?: null | number; 1347 | refillInterval?: null | number; 1348 | remaining?: null | number; 1349 | requestCount?: null | number; 1350 | start?: null | string; 1351 | updatedAt?: number; 1352 | userId?: string; 1353 | }; 1354 | where?: Array<{ 1355 | connector?: "AND" | "OR"; 1356 | field: string; 1357 | operator?: 1358 | | "lt" 1359 | | "lte" 1360 | | "gt" 1361 | | "gte" 1362 | | "eq" 1363 | | "in" 1364 | | "ne" 1365 | | "contains" 1366 | | "starts_with" 1367 | | "ends_with"; 1368 | value: 1369 | | string 1370 | | number 1371 | | boolean 1372 | | Array 1373 | | Array 1374 | | null; 1375 | }>; 1376 | } 1377 | | { 1378 | limit?: number; 1379 | model: "oauthApplication"; 1380 | offset?: number; 1381 | paginationOpts: { 1382 | cursor: string | null; 1383 | endCursor?: string | null; 1384 | id?: number; 1385 | maximumBytesRead?: number; 1386 | maximumRowsRead?: number; 1387 | numItems: number; 1388 | }; 1389 | select?: Array; 1390 | sortBy?: { direction: "asc" | "desc"; field: string }; 1391 | unique?: boolean; 1392 | update: { 1393 | clientId?: null | string; 1394 | clientSecret?: null | string; 1395 | createdAt?: null | number; 1396 | disabled?: null | boolean; 1397 | icon?: null | string; 1398 | metadata?: null | string; 1399 | name?: null | string; 1400 | redirectURLs?: null | string; 1401 | type?: null | string; 1402 | updatedAt?: null | number; 1403 | userId?: null | string; 1404 | }; 1405 | where?: Array<{ 1406 | connector?: "AND" | "OR"; 1407 | field: string; 1408 | operator?: 1409 | | "lt" 1410 | | "lte" 1411 | | "gt" 1412 | | "gte" 1413 | | "eq" 1414 | | "in" 1415 | | "ne" 1416 | | "contains" 1417 | | "starts_with" 1418 | | "ends_with"; 1419 | value: 1420 | | string 1421 | | number 1422 | | boolean 1423 | | Array 1424 | | Array 1425 | | null; 1426 | }>; 1427 | } 1428 | | { 1429 | limit?: number; 1430 | model: "oauthAccessToken"; 1431 | offset?: number; 1432 | paginationOpts: { 1433 | cursor: string | null; 1434 | endCursor?: string | null; 1435 | id?: number; 1436 | maximumBytesRead?: number; 1437 | maximumRowsRead?: number; 1438 | numItems: number; 1439 | }; 1440 | select?: Array; 1441 | sortBy?: { direction: "asc" | "desc"; field: string }; 1442 | unique?: boolean; 1443 | update: { 1444 | accessToken?: null | string; 1445 | accessTokenExpiresAt?: null | number; 1446 | clientId?: null | string; 1447 | createdAt?: null | number; 1448 | refreshToken?: null | string; 1449 | refreshTokenExpiresAt?: null | number; 1450 | scopes?: null | string; 1451 | updatedAt?: null | number; 1452 | userId?: null | string; 1453 | }; 1454 | where?: Array<{ 1455 | connector?: "AND" | "OR"; 1456 | field: string; 1457 | operator?: 1458 | | "lt" 1459 | | "lte" 1460 | | "gt" 1461 | | "gte" 1462 | | "eq" 1463 | | "in" 1464 | | "ne" 1465 | | "contains" 1466 | | "starts_with" 1467 | | "ends_with"; 1468 | value: 1469 | | string 1470 | | number 1471 | | boolean 1472 | | Array 1473 | | Array 1474 | | null; 1475 | }>; 1476 | } 1477 | | { 1478 | limit?: number; 1479 | model: "oauthConsent"; 1480 | offset?: number; 1481 | paginationOpts: { 1482 | cursor: string | null; 1483 | endCursor?: string | null; 1484 | id?: number; 1485 | maximumBytesRead?: number; 1486 | maximumRowsRead?: number; 1487 | numItems: number; 1488 | }; 1489 | select?: Array; 1490 | sortBy?: { direction: "asc" | "desc"; field: string }; 1491 | unique?: boolean; 1492 | update: { 1493 | clientId?: null | string; 1494 | consentGiven?: null | boolean; 1495 | createdAt?: null | number; 1496 | scopes?: null | string; 1497 | updatedAt?: null | number; 1498 | userId?: null | string; 1499 | }; 1500 | where?: Array<{ 1501 | connector?: "AND" | "OR"; 1502 | field: string; 1503 | operator?: 1504 | | "lt" 1505 | | "lte" 1506 | | "gt" 1507 | | "gte" 1508 | | "eq" 1509 | | "in" 1510 | | "ne" 1511 | | "contains" 1512 | | "starts_with" 1513 | | "ends_with"; 1514 | value: 1515 | | string 1516 | | number 1517 | | boolean 1518 | | Array 1519 | | Array 1520 | | null; 1521 | }>; 1522 | } 1523 | | { 1524 | limit?: number; 1525 | model: "organization"; 1526 | offset?: number; 1527 | paginationOpts: { 1528 | cursor: string | null; 1529 | endCursor?: string | null; 1530 | id?: number; 1531 | maximumBytesRead?: number; 1532 | maximumRowsRead?: number; 1533 | numItems: number; 1534 | }; 1535 | select?: Array; 1536 | sortBy?: { direction: "asc" | "desc"; field: string }; 1537 | unique?: boolean; 1538 | update: { 1539 | createdAt?: number; 1540 | logo?: null | string; 1541 | metadata?: null | string; 1542 | name?: string; 1543 | slug?: null | string; 1544 | }; 1545 | where?: Array<{ 1546 | connector?: "AND" | "OR"; 1547 | field: string; 1548 | operator?: 1549 | | "lt" 1550 | | "lte" 1551 | | "gt" 1552 | | "gte" 1553 | | "eq" 1554 | | "in" 1555 | | "ne" 1556 | | "contains" 1557 | | "starts_with" 1558 | | "ends_with"; 1559 | value: 1560 | | string 1561 | | number 1562 | | boolean 1563 | | Array 1564 | | Array 1565 | | null; 1566 | }>; 1567 | } 1568 | | { 1569 | limit?: number; 1570 | model: "member"; 1571 | offset?: number; 1572 | paginationOpts: { 1573 | cursor: string | null; 1574 | endCursor?: string | null; 1575 | id?: number; 1576 | maximumBytesRead?: number; 1577 | maximumRowsRead?: number; 1578 | numItems: number; 1579 | }; 1580 | select?: Array; 1581 | sortBy?: { direction: "asc" | "desc"; field: string }; 1582 | unique?: boolean; 1583 | update: { 1584 | createdAt?: number; 1585 | organizationId?: string; 1586 | role?: string; 1587 | userId?: string; 1588 | }; 1589 | where?: Array<{ 1590 | connector?: "AND" | "OR"; 1591 | field: string; 1592 | operator?: 1593 | | "lt" 1594 | | "lte" 1595 | | "gt" 1596 | | "gte" 1597 | | "eq" 1598 | | "in" 1599 | | "ne" 1600 | | "contains" 1601 | | "starts_with" 1602 | | "ends_with"; 1603 | value: 1604 | | string 1605 | | number 1606 | | boolean 1607 | | Array 1608 | | Array 1609 | | null; 1610 | }>; 1611 | } 1612 | | { 1613 | limit?: number; 1614 | model: "invitation"; 1615 | offset?: number; 1616 | paginationOpts: { 1617 | cursor: string | null; 1618 | endCursor?: string | null; 1619 | id?: number; 1620 | maximumBytesRead?: number; 1621 | maximumRowsRead?: number; 1622 | numItems: number; 1623 | }; 1624 | select?: Array; 1625 | sortBy?: { direction: "asc" | "desc"; field: string }; 1626 | unique?: boolean; 1627 | update: { 1628 | email?: string; 1629 | expiresAt?: number; 1630 | inviterId?: string; 1631 | organizationId?: string; 1632 | role?: null | string; 1633 | status?: string; 1634 | teamId?: null | string; 1635 | }; 1636 | where?: Array<{ 1637 | connector?: "AND" | "OR"; 1638 | field: string; 1639 | operator?: 1640 | | "lt" 1641 | | "lte" 1642 | | "gt" 1643 | | "gte" 1644 | | "eq" 1645 | | "in" 1646 | | "ne" 1647 | | "contains" 1648 | | "starts_with" 1649 | | "ends_with"; 1650 | value: 1651 | | string 1652 | | number 1653 | | boolean 1654 | | Array 1655 | | Array 1656 | | null; 1657 | }>; 1658 | } 1659 | | { 1660 | limit?: number; 1661 | model: "team"; 1662 | offset?: number; 1663 | paginationOpts: { 1664 | cursor: string | null; 1665 | endCursor?: string | null; 1666 | id?: number; 1667 | maximumBytesRead?: number; 1668 | maximumRowsRead?: number; 1669 | numItems: number; 1670 | }; 1671 | select?: Array; 1672 | sortBy?: { direction: "asc" | "desc"; field: string }; 1673 | unique?: boolean; 1674 | update: { 1675 | createdAt?: number; 1676 | name?: string; 1677 | organizationId?: string; 1678 | updatedAt?: null | number; 1679 | }; 1680 | where?: Array<{ 1681 | connector?: "AND" | "OR"; 1682 | field: string; 1683 | operator?: 1684 | | "lt" 1685 | | "lte" 1686 | | "gt" 1687 | | "gte" 1688 | | "eq" 1689 | | "in" 1690 | | "ne" 1691 | | "contains" 1692 | | "starts_with" 1693 | | "ends_with"; 1694 | value: 1695 | | string 1696 | | number 1697 | | boolean 1698 | | Array 1699 | | Array 1700 | | null; 1701 | }>; 1702 | } 1703 | | { 1704 | limit?: number; 1705 | model: "teamMember"; 1706 | offset?: number; 1707 | paginationOpts: { 1708 | cursor: string | null; 1709 | endCursor?: string | null; 1710 | id?: number; 1711 | maximumBytesRead?: number; 1712 | maximumRowsRead?: number; 1713 | numItems: number; 1714 | }; 1715 | select?: Array; 1716 | sortBy?: { direction: "asc" | "desc"; field: string }; 1717 | unique?: boolean; 1718 | update: { 1719 | createdAt?: null | number; 1720 | teamId?: string; 1721 | userId?: string; 1722 | }; 1723 | where?: Array<{ 1724 | connector?: "AND" | "OR"; 1725 | field: string; 1726 | operator?: 1727 | | "lt" 1728 | | "lte" 1729 | | "gt" 1730 | | "gte" 1731 | | "eq" 1732 | | "in" 1733 | | "ne" 1734 | | "contains" 1735 | | "starts_with" 1736 | | "ends_with"; 1737 | value: 1738 | | string 1739 | | number 1740 | | boolean 1741 | | Array 1742 | | Array 1743 | | null; 1744 | }>; 1745 | } 1746 | | { 1747 | limit?: number; 1748 | model: "ssoProvider"; 1749 | offset?: number; 1750 | paginationOpts: { 1751 | cursor: string | null; 1752 | endCursor?: string | null; 1753 | id?: number; 1754 | maximumBytesRead?: number; 1755 | maximumRowsRead?: number; 1756 | numItems: number; 1757 | }; 1758 | select?: Array; 1759 | sortBy?: { direction: "asc" | "desc"; field: string }; 1760 | unique?: boolean; 1761 | update: { 1762 | domain?: string; 1763 | issuer?: string; 1764 | oidcConfig?: null | string; 1765 | organizationId?: null | string; 1766 | providerId?: string; 1767 | samlConfig?: null | string; 1768 | userId?: null | string; 1769 | }; 1770 | where?: Array<{ 1771 | connector?: "AND" | "OR"; 1772 | field: string; 1773 | operator?: 1774 | | "lt" 1775 | | "lte" 1776 | | "gt" 1777 | | "gte" 1778 | | "eq" 1779 | | "in" 1780 | | "ne" 1781 | | "contains" 1782 | | "starts_with" 1783 | | "ends_with"; 1784 | value: 1785 | | string 1786 | | number 1787 | | boolean 1788 | | Array 1789 | | Array 1790 | | null; 1791 | }>; 1792 | } 1793 | | { 1794 | limit?: number; 1795 | model: "jwks"; 1796 | offset?: number; 1797 | paginationOpts: { 1798 | cursor: string | null; 1799 | endCursor?: string | null; 1800 | id?: number; 1801 | maximumBytesRead?: number; 1802 | maximumRowsRead?: number; 1803 | numItems: number; 1804 | }; 1805 | select?: Array; 1806 | sortBy?: { direction: "asc" | "desc"; field: string }; 1807 | unique?: boolean; 1808 | update: { 1809 | createdAt?: number; 1810 | privateKey?: string; 1811 | publicKey?: string; 1812 | }; 1813 | where?: Array<{ 1814 | connector?: "AND" | "OR"; 1815 | field: string; 1816 | operator?: 1817 | | "lt" 1818 | | "lte" 1819 | | "gt" 1820 | | "gte" 1821 | | "eq" 1822 | | "in" 1823 | | "ne" 1824 | | "contains" 1825 | | "starts_with" 1826 | | "ends_with"; 1827 | value: 1828 | | string 1829 | | number 1830 | | boolean 1831 | | Array 1832 | | Array 1833 | | null; 1834 | }>; 1835 | } 1836 | | { 1837 | limit?: number; 1838 | model: "subscription"; 1839 | offset?: number; 1840 | paginationOpts: { 1841 | cursor: string | null; 1842 | endCursor?: string | null; 1843 | id?: number; 1844 | maximumBytesRead?: number; 1845 | maximumRowsRead?: number; 1846 | numItems: number; 1847 | }; 1848 | select?: Array; 1849 | sortBy?: { direction: "asc" | "desc"; field: string }; 1850 | unique?: boolean; 1851 | update: { 1852 | cancelAtPeriodEnd?: null | boolean; 1853 | periodEnd?: null | number; 1854 | periodStart?: null | number; 1855 | plan?: string; 1856 | referenceId?: string; 1857 | seats?: null | number; 1858 | status?: null | string; 1859 | stripeCustomerId?: null | string; 1860 | stripeSubscriptionId?: null | string; 1861 | }; 1862 | where?: Array<{ 1863 | connector?: "AND" | "OR"; 1864 | field: string; 1865 | operator?: 1866 | | "lt" 1867 | | "lte" 1868 | | "gt" 1869 | | "gte" 1870 | | "eq" 1871 | | "in" 1872 | | "ne" 1873 | | "contains" 1874 | | "starts_with" 1875 | | "ends_with"; 1876 | value: 1877 | | string 1878 | | number 1879 | | boolean 1880 | | Array 1881 | | Array 1882 | | null; 1883 | }>; 1884 | } 1885 | | { 1886 | limit?: number; 1887 | model: "walletAddress"; 1888 | offset?: number; 1889 | paginationOpts: { 1890 | cursor: string | null; 1891 | endCursor?: string | null; 1892 | id?: number; 1893 | maximumBytesRead?: number; 1894 | maximumRowsRead?: number; 1895 | numItems: number; 1896 | }; 1897 | select?: Array; 1898 | sortBy?: { direction: "asc" | "desc"; field: string }; 1899 | unique?: boolean; 1900 | update: { 1901 | address?: string; 1902 | chainId?: number; 1903 | createdAt?: number; 1904 | isPrimary?: null | boolean; 1905 | userId?: string; 1906 | }; 1907 | where?: Array<{ 1908 | connector?: "AND" | "OR"; 1909 | field: string; 1910 | operator?: 1911 | | "lt" 1912 | | "lte" 1913 | | "gt" 1914 | | "gte" 1915 | | "eq" 1916 | | "in" 1917 | | "ne" 1918 | | "contains" 1919 | | "starts_with" 1920 | | "ends_with"; 1921 | value: 1922 | | string 1923 | | number 1924 | | boolean 1925 | | Array 1926 | | Array 1927 | | null; 1928 | }>; 1929 | } 1930 | | { 1931 | limit?: number; 1932 | model: "rateLimit"; 1933 | offset?: number; 1934 | paginationOpts: { 1935 | cursor: string | null; 1936 | endCursor?: string | null; 1937 | id?: number; 1938 | maximumBytesRead?: number; 1939 | maximumRowsRead?: number; 1940 | numItems: number; 1941 | }; 1942 | select?: Array; 1943 | sortBy?: { direction: "asc" | "desc"; field: string }; 1944 | unique?: boolean; 1945 | update: { 1946 | count?: null | number; 1947 | key?: null | string; 1948 | lastRequest?: null | number; 1949 | }; 1950 | where?: Array<{ 1951 | connector?: "AND" | "OR"; 1952 | field: string; 1953 | operator?: 1954 | | "lt" 1955 | | "lte" 1956 | | "gt" 1957 | | "gte" 1958 | | "eq" 1959 | | "in" 1960 | | "ne" 1961 | | "contains" 1962 | | "starts_with" 1963 | | "ends_with"; 1964 | value: 1965 | | string 1966 | | number 1967 | | boolean 1968 | | Array 1969 | | Array 1970 | | null; 1971 | }>; 1972 | }; 1973 | }, 1974 | any 1975 | >; 1976 | updateOne: FunctionReference< 1977 | "mutation", 1978 | "internal", 1979 | { 1980 | input: 1981 | | { 1982 | model: "user"; 1983 | update: { 1984 | banExpires?: null | number; 1985 | banReason?: null | string; 1986 | banned?: null | boolean; 1987 | createdAt?: number; 1988 | displayUsername?: null | string; 1989 | email?: string; 1990 | emailVerified?: boolean; 1991 | image?: null | string; 1992 | isAnonymous?: null | boolean; 1993 | name?: string; 1994 | phoneNumber?: null | string; 1995 | phoneNumberVerified?: null | boolean; 1996 | role?: null | string; 1997 | stripeCustomerId?: null | string; 1998 | teamId?: null | string; 1999 | twoFactorEnabled?: null | boolean; 2000 | updatedAt?: number; 2001 | userId?: null | string; 2002 | username?: null | string; 2003 | }; 2004 | where?: Array<{ 2005 | connector?: "AND" | "OR"; 2006 | field: string; 2007 | operator?: 2008 | | "lt" 2009 | | "lte" 2010 | | "gt" 2011 | | "gte" 2012 | | "eq" 2013 | | "in" 2014 | | "ne" 2015 | | "contains" 2016 | | "starts_with" 2017 | | "ends_with"; 2018 | value: 2019 | | string 2020 | | number 2021 | | boolean 2022 | | Array 2023 | | Array 2024 | | null; 2025 | }>; 2026 | } 2027 | | { 2028 | model: "session"; 2029 | update: { 2030 | activeOrganizationId?: null | string; 2031 | activeTeamId?: null | string; 2032 | createdAt?: number; 2033 | expiresAt?: number; 2034 | impersonatedBy?: null | string; 2035 | ipAddress?: null | string; 2036 | token?: string; 2037 | updatedAt?: number; 2038 | userAgent?: null | string; 2039 | userId?: string; 2040 | }; 2041 | where?: Array<{ 2042 | connector?: "AND" | "OR"; 2043 | field: string; 2044 | operator?: 2045 | | "lt" 2046 | | "lte" 2047 | | "gt" 2048 | | "gte" 2049 | | "eq" 2050 | | "in" 2051 | | "ne" 2052 | | "contains" 2053 | | "starts_with" 2054 | | "ends_with"; 2055 | value: 2056 | | string 2057 | | number 2058 | | boolean 2059 | | Array 2060 | | Array 2061 | | null; 2062 | }>; 2063 | } 2064 | | { 2065 | model: "account"; 2066 | update: { 2067 | accessToken?: null | string; 2068 | accessTokenExpiresAt?: null | number; 2069 | accountId?: string; 2070 | createdAt?: number; 2071 | idToken?: null | string; 2072 | password?: null | string; 2073 | providerId?: string; 2074 | refreshToken?: null | string; 2075 | refreshTokenExpiresAt?: null | number; 2076 | scope?: null | string; 2077 | updatedAt?: number; 2078 | userId?: string; 2079 | }; 2080 | where?: Array<{ 2081 | connector?: "AND" | "OR"; 2082 | field: string; 2083 | operator?: 2084 | | "lt" 2085 | | "lte" 2086 | | "gt" 2087 | | "gte" 2088 | | "eq" 2089 | | "in" 2090 | | "ne" 2091 | | "contains" 2092 | | "starts_with" 2093 | | "ends_with"; 2094 | value: 2095 | | string 2096 | | number 2097 | | boolean 2098 | | Array 2099 | | Array 2100 | | null; 2101 | }>; 2102 | } 2103 | | { 2104 | model: "verification"; 2105 | update: { 2106 | createdAt?: null | number; 2107 | expiresAt?: number; 2108 | identifier?: string; 2109 | updatedAt?: null | number; 2110 | value?: string; 2111 | }; 2112 | where?: Array<{ 2113 | connector?: "AND" | "OR"; 2114 | field: string; 2115 | operator?: 2116 | | "lt" 2117 | | "lte" 2118 | | "gt" 2119 | | "gte" 2120 | | "eq" 2121 | | "in" 2122 | | "ne" 2123 | | "contains" 2124 | | "starts_with" 2125 | | "ends_with"; 2126 | value: 2127 | | string 2128 | | number 2129 | | boolean 2130 | | Array 2131 | | Array 2132 | | null; 2133 | }>; 2134 | } 2135 | | { 2136 | model: "twoFactor"; 2137 | update: { 2138 | backupCodes?: string; 2139 | secret?: string; 2140 | userId?: string; 2141 | }; 2142 | where?: Array<{ 2143 | connector?: "AND" | "OR"; 2144 | field: string; 2145 | operator?: 2146 | | "lt" 2147 | | "lte" 2148 | | "gt" 2149 | | "gte" 2150 | | "eq" 2151 | | "in" 2152 | | "ne" 2153 | | "contains" 2154 | | "starts_with" 2155 | | "ends_with"; 2156 | value: 2157 | | string 2158 | | number 2159 | | boolean 2160 | | Array 2161 | | Array 2162 | | null; 2163 | }>; 2164 | } 2165 | | { 2166 | model: "passkey"; 2167 | update: { 2168 | aaguid?: null | string; 2169 | backedUp?: boolean; 2170 | counter?: number; 2171 | createdAt?: null | number; 2172 | credentialID?: string; 2173 | deviceType?: string; 2174 | name?: null | string; 2175 | publicKey?: string; 2176 | transports?: null | string; 2177 | userId?: string; 2178 | }; 2179 | where?: Array<{ 2180 | connector?: "AND" | "OR"; 2181 | field: string; 2182 | operator?: 2183 | | "lt" 2184 | | "lte" 2185 | | "gt" 2186 | | "gte" 2187 | | "eq" 2188 | | "in" 2189 | | "ne" 2190 | | "contains" 2191 | | "starts_with" 2192 | | "ends_with"; 2193 | value: 2194 | | string 2195 | | number 2196 | | boolean 2197 | | Array 2198 | | Array 2199 | | null; 2200 | }>; 2201 | } 2202 | | { 2203 | model: "apikey"; 2204 | update: { 2205 | createdAt?: number; 2206 | enabled?: null | boolean; 2207 | expiresAt?: null | number; 2208 | key?: string; 2209 | lastRefillAt?: null | number; 2210 | lastRequest?: null | number; 2211 | metadata?: null | string; 2212 | name?: null | string; 2213 | permissions?: null | string; 2214 | prefix?: null | string; 2215 | rateLimitEnabled?: null | boolean; 2216 | rateLimitMax?: null | number; 2217 | rateLimitTimeWindow?: null | number; 2218 | refillAmount?: null | number; 2219 | refillInterval?: null | number; 2220 | remaining?: null | number; 2221 | requestCount?: null | number; 2222 | start?: null | string; 2223 | updatedAt?: number; 2224 | userId?: string; 2225 | }; 2226 | where?: Array<{ 2227 | connector?: "AND" | "OR"; 2228 | field: string; 2229 | operator?: 2230 | | "lt" 2231 | | "lte" 2232 | | "gt" 2233 | | "gte" 2234 | | "eq" 2235 | | "in" 2236 | | "ne" 2237 | | "contains" 2238 | | "starts_with" 2239 | | "ends_with"; 2240 | value: 2241 | | string 2242 | | number 2243 | | boolean 2244 | | Array 2245 | | Array 2246 | | null; 2247 | }>; 2248 | } 2249 | | { 2250 | model: "oauthApplication"; 2251 | update: { 2252 | clientId?: null | string; 2253 | clientSecret?: null | string; 2254 | createdAt?: null | number; 2255 | disabled?: null | boolean; 2256 | icon?: null | string; 2257 | metadata?: null | string; 2258 | name?: null | string; 2259 | redirectURLs?: null | string; 2260 | type?: null | string; 2261 | updatedAt?: null | number; 2262 | userId?: null | string; 2263 | }; 2264 | where?: Array<{ 2265 | connector?: "AND" | "OR"; 2266 | field: string; 2267 | operator?: 2268 | | "lt" 2269 | | "lte" 2270 | | "gt" 2271 | | "gte" 2272 | | "eq" 2273 | | "in" 2274 | | "ne" 2275 | | "contains" 2276 | | "starts_with" 2277 | | "ends_with"; 2278 | value: 2279 | | string 2280 | | number 2281 | | boolean 2282 | | Array 2283 | | Array 2284 | | null; 2285 | }>; 2286 | } 2287 | | { 2288 | model: "oauthAccessToken"; 2289 | update: { 2290 | accessToken?: null | string; 2291 | accessTokenExpiresAt?: null | number; 2292 | clientId?: null | string; 2293 | createdAt?: null | number; 2294 | refreshToken?: null | string; 2295 | refreshTokenExpiresAt?: null | number; 2296 | scopes?: null | string; 2297 | updatedAt?: null | number; 2298 | userId?: null | string; 2299 | }; 2300 | where?: Array<{ 2301 | connector?: "AND" | "OR"; 2302 | field: string; 2303 | operator?: 2304 | | "lt" 2305 | | "lte" 2306 | | "gt" 2307 | | "gte" 2308 | | "eq" 2309 | | "in" 2310 | | "ne" 2311 | | "contains" 2312 | | "starts_with" 2313 | | "ends_with"; 2314 | value: 2315 | | string 2316 | | number 2317 | | boolean 2318 | | Array 2319 | | Array 2320 | | null; 2321 | }>; 2322 | } 2323 | | { 2324 | model: "oauthConsent"; 2325 | update: { 2326 | clientId?: null | string; 2327 | consentGiven?: null | boolean; 2328 | createdAt?: null | number; 2329 | scopes?: null | string; 2330 | updatedAt?: null | number; 2331 | userId?: null | string; 2332 | }; 2333 | where?: Array<{ 2334 | connector?: "AND" | "OR"; 2335 | field: string; 2336 | operator?: 2337 | | "lt" 2338 | | "lte" 2339 | | "gt" 2340 | | "gte" 2341 | | "eq" 2342 | | "in" 2343 | | "ne" 2344 | | "contains" 2345 | | "starts_with" 2346 | | "ends_with"; 2347 | value: 2348 | | string 2349 | | number 2350 | | boolean 2351 | | Array 2352 | | Array 2353 | | null; 2354 | }>; 2355 | } 2356 | | { 2357 | model: "organization"; 2358 | update: { 2359 | createdAt?: number; 2360 | logo?: null | string; 2361 | metadata?: null | string; 2362 | name?: string; 2363 | slug?: null | string; 2364 | }; 2365 | where?: Array<{ 2366 | connector?: "AND" | "OR"; 2367 | field: string; 2368 | operator?: 2369 | | "lt" 2370 | | "lte" 2371 | | "gt" 2372 | | "gte" 2373 | | "eq" 2374 | | "in" 2375 | | "ne" 2376 | | "contains" 2377 | | "starts_with" 2378 | | "ends_with"; 2379 | value: 2380 | | string 2381 | | number 2382 | | boolean 2383 | | Array 2384 | | Array 2385 | | null; 2386 | }>; 2387 | } 2388 | | { 2389 | model: "member"; 2390 | update: { 2391 | createdAt?: number; 2392 | organizationId?: string; 2393 | role?: string; 2394 | userId?: string; 2395 | }; 2396 | where?: Array<{ 2397 | connector?: "AND" | "OR"; 2398 | field: string; 2399 | operator?: 2400 | | "lt" 2401 | | "lte" 2402 | | "gt" 2403 | | "gte" 2404 | | "eq" 2405 | | "in" 2406 | | "ne" 2407 | | "contains" 2408 | | "starts_with" 2409 | | "ends_with"; 2410 | value: 2411 | | string 2412 | | number 2413 | | boolean 2414 | | Array 2415 | | Array 2416 | | null; 2417 | }>; 2418 | } 2419 | | { 2420 | model: "invitation"; 2421 | update: { 2422 | email?: string; 2423 | expiresAt?: number; 2424 | inviterId?: string; 2425 | organizationId?: string; 2426 | role?: null | string; 2427 | status?: string; 2428 | teamId?: null | string; 2429 | }; 2430 | where?: Array<{ 2431 | connector?: "AND" | "OR"; 2432 | field: string; 2433 | operator?: 2434 | | "lt" 2435 | | "lte" 2436 | | "gt" 2437 | | "gte" 2438 | | "eq" 2439 | | "in" 2440 | | "ne" 2441 | | "contains" 2442 | | "starts_with" 2443 | | "ends_with"; 2444 | value: 2445 | | string 2446 | | number 2447 | | boolean 2448 | | Array 2449 | | Array 2450 | | null; 2451 | }>; 2452 | } 2453 | | { 2454 | model: "team"; 2455 | update: { 2456 | createdAt?: number; 2457 | name?: string; 2458 | organizationId?: string; 2459 | updatedAt?: null | number; 2460 | }; 2461 | where?: Array<{ 2462 | connector?: "AND" | "OR"; 2463 | field: string; 2464 | operator?: 2465 | | "lt" 2466 | | "lte" 2467 | | "gt" 2468 | | "gte" 2469 | | "eq" 2470 | | "in" 2471 | | "ne" 2472 | | "contains" 2473 | | "starts_with" 2474 | | "ends_with"; 2475 | value: 2476 | | string 2477 | | number 2478 | | boolean 2479 | | Array 2480 | | Array 2481 | | null; 2482 | }>; 2483 | } 2484 | | { 2485 | model: "teamMember"; 2486 | update: { 2487 | createdAt?: null | number; 2488 | teamId?: string; 2489 | userId?: string; 2490 | }; 2491 | where?: Array<{ 2492 | connector?: "AND" | "OR"; 2493 | field: string; 2494 | operator?: 2495 | | "lt" 2496 | | "lte" 2497 | | "gt" 2498 | | "gte" 2499 | | "eq" 2500 | | "in" 2501 | | "ne" 2502 | | "contains" 2503 | | "starts_with" 2504 | | "ends_with"; 2505 | value: 2506 | | string 2507 | | number 2508 | | boolean 2509 | | Array 2510 | | Array 2511 | | null; 2512 | }>; 2513 | } 2514 | | { 2515 | model: "ssoProvider"; 2516 | update: { 2517 | domain?: string; 2518 | issuer?: string; 2519 | oidcConfig?: null | string; 2520 | organizationId?: null | string; 2521 | providerId?: string; 2522 | samlConfig?: null | string; 2523 | userId?: null | string; 2524 | }; 2525 | where?: Array<{ 2526 | connector?: "AND" | "OR"; 2527 | field: string; 2528 | operator?: 2529 | | "lt" 2530 | | "lte" 2531 | | "gt" 2532 | | "gte" 2533 | | "eq" 2534 | | "in" 2535 | | "ne" 2536 | | "contains" 2537 | | "starts_with" 2538 | | "ends_with"; 2539 | value: 2540 | | string 2541 | | number 2542 | | boolean 2543 | | Array 2544 | | Array 2545 | | null; 2546 | }>; 2547 | } 2548 | | { 2549 | model: "jwks"; 2550 | update: { 2551 | createdAt?: number; 2552 | privateKey?: string; 2553 | publicKey?: string; 2554 | }; 2555 | where?: Array<{ 2556 | connector?: "AND" | "OR"; 2557 | field: string; 2558 | operator?: 2559 | | "lt" 2560 | | "lte" 2561 | | "gt" 2562 | | "gte" 2563 | | "eq" 2564 | | "in" 2565 | | "ne" 2566 | | "contains" 2567 | | "starts_with" 2568 | | "ends_with"; 2569 | value: 2570 | | string 2571 | | number 2572 | | boolean 2573 | | Array 2574 | | Array 2575 | | null; 2576 | }>; 2577 | } 2578 | | { 2579 | model: "subscription"; 2580 | update: { 2581 | cancelAtPeriodEnd?: null | boolean; 2582 | periodEnd?: null | number; 2583 | periodStart?: null | number; 2584 | plan?: string; 2585 | referenceId?: string; 2586 | seats?: null | number; 2587 | status?: null | string; 2588 | stripeCustomerId?: null | string; 2589 | stripeSubscriptionId?: null | string; 2590 | }; 2591 | where?: Array<{ 2592 | connector?: "AND" | "OR"; 2593 | field: string; 2594 | operator?: 2595 | | "lt" 2596 | | "lte" 2597 | | "gt" 2598 | | "gte" 2599 | | "eq" 2600 | | "in" 2601 | | "ne" 2602 | | "contains" 2603 | | "starts_with" 2604 | | "ends_with"; 2605 | value: 2606 | | string 2607 | | number 2608 | | boolean 2609 | | Array 2610 | | Array 2611 | | null; 2612 | }>; 2613 | } 2614 | | { 2615 | model: "walletAddress"; 2616 | update: { 2617 | address?: string; 2618 | chainId?: number; 2619 | createdAt?: number; 2620 | isPrimary?: null | boolean; 2621 | userId?: string; 2622 | }; 2623 | where?: Array<{ 2624 | connector?: "AND" | "OR"; 2625 | field: string; 2626 | operator?: 2627 | | "lt" 2628 | | "lte" 2629 | | "gt" 2630 | | "gte" 2631 | | "eq" 2632 | | "in" 2633 | | "ne" 2634 | | "contains" 2635 | | "starts_with" 2636 | | "ends_with"; 2637 | value: 2638 | | string 2639 | | number 2640 | | boolean 2641 | | Array 2642 | | Array 2643 | | null; 2644 | }>; 2645 | } 2646 | | { 2647 | model: "rateLimit"; 2648 | update: { 2649 | count?: null | number; 2650 | key?: null | string; 2651 | lastRequest?: null | number; 2652 | }; 2653 | where?: Array<{ 2654 | connector?: "AND" | "OR"; 2655 | field: string; 2656 | operator?: 2657 | | "lt" 2658 | | "lte" 2659 | | "gt" 2660 | | "gte" 2661 | | "eq" 2662 | | "in" 2663 | | "ne" 2664 | | "contains" 2665 | | "starts_with" 2666 | | "ends_with"; 2667 | value: 2668 | | string 2669 | | number 2670 | | boolean 2671 | | Array 2672 | | Array 2673 | | null; 2674 | }>; 2675 | }; 2676 | }, 2677 | any 2678 | >; 2679 | }; 2680 | }; 2681 | }; 2682 | --------------------------------------------------------------------------------