├── .claude ├── commands │ ├── opus.md │ └── w.md ├── rules │ ├── convex-scale-schema.mdc │ ├── 3-project-status.mdc │ ├── toast.mdc │ ├── 1-app-design-document.mdc │ ├── 2-tech-stack.mdc │ └── jotai-x.mdc ├── AGENTS.md ├── settings.json ├── skiller.toml ├── scripts │ ├── post-compact.sh │ └── user-prompt-submit.sh └── prompt.json ├── src ├── app │ ├── favicon.ico │ ├── api │ │ └── auth │ │ │ └── [...all] │ │ │ └── route.ts │ ├── page.tsx │ ├── login │ │ └── page.tsx │ ├── layout.tsx │ └── globals.css ├── lib │ ├── convex │ │ ├── hooks │ │ │ ├── index.ts │ │ │ └── useCurrentUser.ts │ │ ├── components │ │ │ ├── authenticated.tsx │ │ │ ├── convex-provider.tsx │ │ │ ├── auth-error-boundary.tsx │ │ │ └── login-form.tsx │ │ ├── rsc.tsx │ │ ├── auth-client.ts │ │ └── server.ts │ ├── utils.ts │ └── react-query │ │ ├── query-client-provider.tsx │ │ └── query-client.ts ├── components │ ├── ui │ │ ├── aspect-ratio.tsx │ │ ├── sonner.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── textarea.tsx │ │ ├── progress.tsx │ │ ├── collapsible.tsx │ │ ├── input.tsx │ │ ├── skeleton.tsx │ │ ├── switch.tsx │ │ ├── avatar.tsx │ │ ├── checkbox.tsx │ │ ├── radio-group.tsx │ │ ├── hover-card.tsx │ │ ├── toggle.tsx │ │ ├── popover.tsx │ │ ├── badge.tsx │ │ ├── scroll-area.tsx │ │ ├── alert.tsx │ │ ├── tooltip.tsx │ │ ├── toggle-group.tsx │ │ ├── tabs.tsx │ │ ├── slider.tsx │ │ ├── resizable.tsx │ │ ├── accordion.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── input-otp.tsx │ │ ├── breadcrumb.tsx │ │ ├── table.tsx │ │ ├── pagination.tsx │ │ ├── form.tsx │ │ ├── alert-dialog.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ └── drawer.tsx │ ├── providers.tsx │ └── todos │ │ ├── todo-search.tsx │ │ └── tag-picker.tsx ├── hooks │ ├── use-mounted.ts │ ├── use-mobile.ts │ └── use-random.ts └── env.ts ├── postcss.config.mjs ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── convex ├── auth.config.ts ├── .env.example ├── http.ts ├── helpers │ ├── premiumGuard.ts │ ├── roleGuard.ts │ ├── polyfills.ts │ └── getEnv.ts ├── _generated │ ├── api.js │ ├── dataModel.d.ts │ └── server.js ├── authPermissions.ts ├── convex.config.ts ├── shared │ └── types.ts ├── triggers.ts ├── init.ts ├── emails.tsx ├── organizationHelpers.ts ├── README.md ├── reset.ts ├── user.ts ├── aggregates.ts ├── polar │ └── product.ts └── authSchema.ts ├── mprocs.yaml ├── .vscode ├── extensions.json └── settings.json ├── lefthook.yml ├── .env.example ├── next.config.ts ├── components.json ├── eslint.config.mjs ├── .gitignore ├── LICENSE ├── tsconfig.json ├── .github └── workflows │ └── ci.yml ├── biome.jsonc ├── package.json └── scripts └── sync-convex-env.ts /.claude/commands/opus.md: -------------------------------------------------------------------------------- 1 | --- 2 | model: claude-opus-4-1-20250805 3 | --- 4 | 5 | $ARGUMENTS 6 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udecode/better-convex/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /.claude/commands/w.md: -------------------------------------------------------------------------------- 1 | Run `npm run logs` to watch the Next.js logs, then run `npm run typecheck:watch` to watch tsc. 2 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ['@tailwindcss/postcss'], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { nextJsHandler } from '@convex-dev/better-auth/nextjs'; 2 | 3 | export const { GET, POST } = nextJsHandler(); 4 | -------------------------------------------------------------------------------- /src/lib/convex/hooks/index.ts: -------------------------------------------------------------------------------- 1 | /** biome-ignore-all lint/performance/noBarrelFile: lib */ 2 | export * from './convex-hooks'; 3 | 4 | export * from './useCurrentUser'; 5 | -------------------------------------------------------------------------------- /convex/auth.config.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | providers: [ 3 | { 4 | applicationID: 'convex', 5 | domain: `${process.env.CONVEX_SITE_URL}`, 6 | }, 7 | ], 8 | }; 9 | -------------------------------------------------------------------------------- /mprocs.yaml: -------------------------------------------------------------------------------- 1 | procs: 2 | next: 3 | cmd: ['bun', 'dev:app'] 4 | convex: 5 | cmd: ['bun', 'dev:backend'] 6 | typecheck: 7 | cmd: ['bun', 'typecheck:watch'] 8 | autostart: false 9 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx'; 2 | import { twMerge } from 'tailwind-merge'; 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)); 6 | } 7 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "unional.vscode-sort-package-json", 4 | "biomejs.biome", 5 | "dbaeumer.vscode-eslint", 6 | "Anthropic.claude-code", 7 | "bradlc.vscode-tailwindcss", 8 | "oven.bun-vscode" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { TodoList } from '@/components/todos/todo-list'; 2 | 3 | export default async function HomePage() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /lefthook.yml: -------------------------------------------------------------------------------- 1 | pre-commit: 2 | jobs: 3 | - run: bunx ultracite fix 4 | glob: 5 | - '*.js' 6 | - '*.jsx' 7 | - '*.ts' 8 | - '*.tsx' 9 | - '*.json' 10 | - '*.jsonc' 11 | - '*.css' 12 | stage_fixed: true 13 | -------------------------------------------------------------------------------- /convex/.env.example: -------------------------------------------------------------------------------- 1 | ADMIN=better-convex@gmail.com 2 | DEPLOY_ENV=development 3 | NEXT_PUBLIC_SITE_URL=http://localhost:3005 4 | 5 | # Auth 6 | BETTER_AUTH_SECRET= 7 | GITHUB_CLIENT_ID= 8 | GITHUB_CLIENT_SECRET= 9 | GOOGLE_CLIENT_ID= 10 | GOOGLE_CLIENT_SECRET= 11 | RESEND_API_KEY= -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | CONVEX_DEPLOYMENT=local:local-better-convex 2 | NEXT_PUBLIC_SITE_URL=http://localhost:3005 3 | NEXT_PUBLIC_CONVEX_URL=http://127.0.0.1:3210 4 | NEXT_PUBLIC_CONVEX_SITE_URL=http://127.0.0.1:3211 5 | 6 | # Not used – silent warnings 7 | BETTER_AUTH_SECRET=1 8 | GITHUB_CLIENT_ID=1 9 | GOOGLE_CLIENT_ID=1 -------------------------------------------------------------------------------- /convex/http.ts: -------------------------------------------------------------------------------- 1 | import './helpers/polyfills'; 2 | import { registerRoutes } from 'better-auth-convex'; 3 | import { httpRouter } from 'convex/server'; 4 | import { createAuth } from './auth'; 5 | 6 | const http = httpRouter(); 7 | 8 | registerRoutes(http, createAuth); 9 | 10 | export default http; 11 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from 'next'; 2 | 3 | const nextConfig: NextConfig = { 4 | experimental: { 5 | turbopackFileSystemCacheForDev: true, 6 | browserDebugInfoInTerminal: true, 7 | }, 8 | reactCompiler: true, 9 | typedRoutes: true, 10 | }; 11 | 12 | export default nextConfig; 13 | -------------------------------------------------------------------------------- /src/components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { AspectRatio as AspectRatioPrimitive } from 'radix-ui'; 4 | 5 | function AspectRatio({ 6 | ...props 7 | }: React.ComponentProps) { 8 | return ; 9 | } 10 | 11 | export { AspectRatio }; 12 | -------------------------------------------------------------------------------- /.claude/rules/convex-scale-schema.mdc: -------------------------------------------------------------------------------- 1 | --- 2 | description: Use when designing Convex schema for optimal query caching and performance at scale - provides schema design patterns from Convex documentation for maximizing cache efficiency 3 | globs: convex/schema.ts 4 | alwaysApply: false 5 | --- 6 | 7 | https://stack.convex.dev/queries-that-scale#3-optimizing-queries-for-caching 8 | -------------------------------------------------------------------------------- /convex/helpers/premiumGuard.ts: -------------------------------------------------------------------------------- 1 | import type { SessionUser } from '@convex/authHelpers'; 2 | import { ConvexError } from 'convex/values'; 3 | 4 | export function premiumGuard(user: { plan?: SessionUser['plan'] }) { 5 | if (!user.plan) { 6 | throw new ConvexError({ 7 | code: 'PREMIUM_REQUIRED', 8 | message: 'Premium subscription required', 9 | }); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.claude/AGENTS.md: -------------------------------------------------------------------------------- 1 | - In all interactions and commit messages, be extremely concise and sacrifice grammar for the sake of concision. 2 | 3 | ## PR Comments 4 | 5 | - When tagging Claude in GitHub issues, use '@claude' 6 | 7 | ## GitHub 8 | 9 | - Your primary method for interacting with GitHub should be the GitHub CLI. 10 | 11 | ## Plans 12 | 13 | - At the end of each plan, give me a list of unresolved questions to answer, if any. Make the questions extremely concise. Sacrifice grammar for the sake of concision. 14 | -------------------------------------------------------------------------------- /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/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /convex/helpers/roleGuard.ts: -------------------------------------------------------------------------------- 1 | import { ConvexError } from 'convex/values'; 2 | 3 | // Helper function to check role authorization 4 | export function roleGuard( 5 | role: 'admin', 6 | user: { isAdmin?: boolean; role?: string | null } | null 7 | ) { 8 | if (!user) { 9 | throw new ConvexError({ 10 | code: 'FORBIDDEN', 11 | message: 'Access denied', 12 | }); 13 | } 14 | if (role === 'admin' && !user.isAdmin) { 15 | throw new ConvexError({ 16 | code: 'FORBIDDEN', 17 | message: 'Admin access required', 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignForm } from '@/lib/convex/components/login-form'; 2 | 3 | export default function LoginPage() { 4 | return ( 5 |
6 |
7 |
8 |

Welcome back

9 |

Sign in to continue

10 |
11 | 12 |
13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/components/providers.tsx: -------------------------------------------------------------------------------- 1 | import { NuqsAdapter } from 'nuqs/adapters/next/app'; 2 | import { ConvexProvider } from '@/lib/convex/components/convex-provider'; 3 | import { getSessionToken } from '@/lib/convex/server'; 4 | import { QueryClientProvider } from '@/lib/react-query/query-client-provider'; 5 | 6 | export async function Providers({ children }) { 7 | const token = await getSessionToken(); 8 | 9 | return ( 10 | 11 | 12 | {children} 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /src/hooks/use-mounted.ts: -------------------------------------------------------------------------------- 1 | import { useSyncExternalStore } from 'react'; 2 | 3 | /** 4 | * A custom hook that returns a boolean value indicating whether the component 5 | * is mounted (client-side) or not. Useful for preventing hydration mismatches 6 | * when rendering different content on server vs client. 7 | */ 8 | export const useMounted = () => { 9 | return useSyncExternalStore( 10 | subscribe, // subscribe: no-op, never changes 11 | () => true, // getSnapshot (client): always true 12 | () => false // getServerSnapshot (SSR): always false 13 | ); 14 | }; 15 | 16 | const subscribe = () => () => {}; 17 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import tsParser from '@typescript-eslint/parser'; 2 | import { defineConfig } from 'eslint/config'; 3 | import reactHooks from 'eslint-plugin-react-hooks'; 4 | 5 | export default defineConfig([ 6 | { 7 | ...reactHooks.configs.flat.recommended, 8 | files: ['src/**/*.ts*'], 9 | languageOptions: { parser: tsParser }, 10 | }, 11 | { 12 | ignores: [ 13 | 'node_modules/**', 14 | '.next/**', 15 | 'next-env.d.ts', 16 | 'out/**', 17 | 'build/**', 18 | 'tmp/**', 19 | 'convex', 20 | '**/vault', 21 | '**/_vault', 22 | '.contentlayer', 23 | ], 24 | }, 25 | ]); 26 | -------------------------------------------------------------------------------- /src/hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const MOBILE_BREAKPOINT = 768; 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState( 7 | undefined 8 | ); 9 | 10 | React.useEffect(() => { 11 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`); 12 | const onChange = () => { 13 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 14 | }; 15 | mql.addEventListener('change', onChange); 16 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT); 17 | return () => mql.removeEventListener('change', onChange); 18 | }, []); 19 | 20 | return !!isMobile; 21 | } 22 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useTheme } from 'next-themes'; 4 | import { Toaster as Sonner, type 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 | -------------------------------------------------------------------------------- /convex/authPermissions.ts: -------------------------------------------------------------------------------- 1 | import { createAccessControl } from 'better-auth/plugins/access'; 2 | import { 3 | defaultStatements, 4 | memberAc, 5 | ownerAc, 6 | } from 'better-auth/plugins/organization/access'; 7 | 8 | // Define access control statements for resources 9 | const statement = { 10 | ...defaultStatements, 11 | projects: ['create', 'update', 'delete'], 12 | } as const; 13 | 14 | export const ac = createAccessControl(statement); 15 | 16 | const member = ac.newRole({ 17 | ...memberAc.statements, 18 | projects: ['create', 'update'], 19 | }); 20 | 21 | const owner = ac.newRole({ 22 | ...ownerAc.statements, 23 | projects: ['create', 'update', 'delete'], 24 | }); 25 | 26 | export const roles = { member, owner }; 27 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Label as LabelPrimitive } from 'radix-ui'; 4 | import type * as React from 'react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | export { Label }; 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/* 2 | !.vscode/settings.json 3 | !.vscode/extensions.json 4 | 5 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /.next/ 15 | /out/ 16 | 17 | # production 18 | /build 19 | 20 | # misc 21 | .DS_Store 22 | *.pem 23 | 24 | # env files (can opt-in for committing if needed) 25 | .env* 26 | convex/.env 27 | !.env.example 28 | 29 | # vercel 30 | .vercel 31 | 32 | # typescript 33 | *.tsbuildinfo 34 | next-env.d.ts 35 | 36 | .codex 37 | 38 | # START Skiller Generated Files 39 | /.claude/skills 40 | /.codex/config.toml 41 | /.cursor/rules 42 | /.skillz 43 | /AGENTS.md 44 | /CLAUDE.md 45 | # END Skiller Generated Files 46 | -------------------------------------------------------------------------------- /src/lib/convex/components/authenticated.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { useAuthStatus, useIsAuth } from '@/lib/convex/hooks'; 4 | 5 | export function Authenticated({ children }: { children: React.ReactNode }) { 6 | const isAuth = useIsAuth(); 7 | 8 | if (isAuth) { 9 | return <>{children}; 10 | } 11 | 12 | return null; 13 | } 14 | 15 | export function Unauthenticated({ 16 | children, 17 | verified, 18 | }: { 19 | children: React.ReactNode; 20 | verified?: boolean; 21 | }) { 22 | const { hasSession, isAuthenticated, isLoading } = useAuthStatus(); 23 | 24 | if (!verified && hasSession) { 25 | return null; 26 | } 27 | if (verified && (isLoading || isAuthenticated)) { 28 | return null; 29 | } 30 | 31 | return <>{children}; 32 | } 33 | -------------------------------------------------------------------------------- /convex/convex.config.ts: -------------------------------------------------------------------------------- 1 | import aggregate from '@convex-dev/aggregate/convex.config'; 2 | import rateLimiter from '@convex-dev/rate-limiter/convex.config'; 3 | import resend from '@convex-dev/resend/convex.config'; 4 | import { defineApp } from 'convex/server'; 5 | 6 | const app = defineApp(); 7 | app.use(rateLimiter); 8 | app.use(resend); 9 | 10 | // Register all aggregates 11 | app.use(aggregate, { name: 'aggregateUsers' }); 12 | app.use(aggregate, { name: 'aggregateTodosByUser' }); 13 | app.use(aggregate, { name: 'aggregateTodosByProject' }); 14 | app.use(aggregate, { name: 'aggregateTodosByStatus' }); 15 | app.use(aggregate, { name: 'aggregateTagUsage' }); 16 | app.use(aggregate, { name: 'aggregateProjectMembers' }); 17 | app.use(aggregate, { name: 'aggregateCommentsByTodo' }); 18 | 19 | export default app; 20 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Separator as SeparatorPrimitive } from 'radix-ui'; 4 | import type * as React from 'react'; 5 | 6 | import { cn } from '@/lib/utils'; 7 | 8 | function Separator({ 9 | className, 10 | orientation = 'horizontal', 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ); 26 | } 27 | 28 | export { Separator }; 29 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import type * as React from 'react'; 2 | 3 | import { cn } from '@/lib/utils'; 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) { 6 | return ( 7 |