- Notifications
You must be signed in to change notification settings - Fork7
A modern Convex + Next.js template with best practices
License
udecode/better-convex
Folders and files
| Name | Name | Last commit message | Last commit date | |
|---|---|---|---|---|
Repository files navigation
A modern Next.js starter template featuringConvex backend withBetter Auth integration, showcasing type-safe backend patterns and custom authentication helpers. Includes React Compiler for automatic optimizations.
- 🔐 Better Auth Integration: Complete authentication with GitHub and Google OAuth, session management, and organization support
- 👥 Multi-Organization Support: Personal and team organizations with role-based access (owner/member)
- 💳 Subscription Ready: Polar payment integration with Premium subscriptions and monthly credits (coming soon)
- 📊 Full-Stack Type Safety: End-to-end TypeScript with Convex Ents for relationships and custom function wrappers
- ⚡ Rate Limiting: Built-in protection with tier-based limits (free/premium)
- 🎯 Starter Features: Todo management, projects, tags, and comments with soft delete
- 🔍 Search & Pagination: Full-text search indexes and efficient paginated queries
- 🚀 Developer Experience: Pre-configured hooks, RSC helpers, auth guards, and skeleton loading
- Framework: Next.js 16 with App Router & React 19.2 (React Compiler enabled)
- Backend: Convex with Ents (entity relationships)
- Authentication: Better Auth with better-auth-convex package & organization plugin
- Payments: Polar integration (subscriptions & credits)
- Styling: Tailwind CSS v4 with CSS-first configuration
- State: Jotai-x for client state, React Query for server state
- Forms: React Hook Form + Zod validation
- UI: shadcn/ui components with Radix UI primitives
- Code Quality: Ultracite (Biome preset) for linting/formatting, Lefthook for git hooks
- Node.js 18 or later
- pnpm package manager
- GitHub and/or Google OAuth app credentials
- Clone and install dependencies:
git clone<your-repo-url>cd better-convexpnpm install
- Set up environment variables:
Create.env.local for Next.js:
cp .env.example .env.local
Createconvex/.env for Convex:
cp convex/.env.example convex/.env
- CreateGitHub OAuth App
- CreateGoogle OAuth App
- CreateResend API key
Add credentials toconvex/.env:
# Required environment variablesGITHUB_CLIENT_ID=your_github_client_idGITHUB_CLIENT_SECRET=your_github_client_secretGOOGLE_CLIENT_ID=your_google_client_idGOOGLE_CLIENT_SECRET=your_google_client_secretRESEND_API_KEY=your_resend_api_key
- Start development servers:
# This will start both Next.js and Convexpnpm dev- Initialize Convex environment (first time only):
In a new terminal:
pnpm sync
- Open the app:
Navigate tohttp://localhost:3005
pnpm init# Populate with sample datapnpm reset# Reset all tablespnpm studio# Open Convex dashboard
Instead of using raw Convexquery/mutation/action, this template provides custom wrappers with built-in auth, rate limiting, and type safety:
// Public query - auth optionalexportconstexample=createPublicQuery()({args:{id:zid("items")},// Always use zid() for IDsreturns:z.object({name:z.string()}).nullable(),handler:async(ctx,args)=>{returnawaitctx.table("items").get(args.id);},});// Protected mutation with rate limitingexportconstcreateItem=createAuthMutation({rateLimit:"item/create",// Auto tier limitsrole:"admin",// Optional role check (lowercase)})({args:{name:z.string().min(1).max(100)},returns:zid("items"),handler:async(ctx,args)=>{// ctx.user is pre-loaded SessionUser with ent methodsreturnawaitctx.table("items").insert({name:args.name,userId:ctx.user._id,});},});
Available function types:
createPublicQuery()- No auth requiredcreateAuthQuery()- Requires authenticationcreatePublicMutation()- Auth optionalcreateAuthMutation()- Requires authcreatePublicPaginatedQuery()- With paginationcreateAuthPaginatedQuery()- Auth + paginationcreateInternalQuery/Mutation/Action()- Convex-only
// Never use useQuery directly - use these wrappersconst{ data, isPending}=usePublicQuery(api.items.list,{});// ALWAYS pass {} for no argsconst{ data}=useAuthQuery(api.user.getProfile,{});// Skips if not auth// Mutations with toast integrationconstupdateSettings=useAuthMutation(api.user.updateSettings);toast.promise(updateSettings.mutateAsync({name:"New"}),{loading:"Updating...",success:"Updated!",error:(e)=>e.data?.message??"Failed",});// Paginated queriesconst{ data, hasNextPage, fetchNextPage}=usePublicPaginatedQuery(api.messages.list,{author:"alice"},{initialNumItems:10});
// Auth helpers for RSCconsttoken=awaitgetSessionToken();// Returns string | nullconstuser=awaitgetSessionUser();// Returns SessionUser & { token } | nullconstisAuthenticated=awaitisAuth();// Fetch with authconstdata=awaitfetchAuthQuery(api.user.getData,{id:userId});constdata=awaitfetchAuthQueryOrThrow(api.user.getData,{id:userId});
The template includes two schemas working together:
// convex/schema.ts - Application data with relationshipsconstschema=defineEntSchema({user:defineEnt({name:v.optional(v.string()),bio:v.optional(v.string()),personalOrganizationId:v.string(),// Every user has a personal org}).field("email",v.string(),{unique:true}).edges("subscriptions",{ref:"userId"})// Polar subscriptions.edges("todos",{ref:true}).edges("ownedProjects",{to:"projects",ref:"ownerId"}),todos:defineEnt({title:v.string(),description:v.optional(v.string()),}).field("completed",v.boolean(),{index:true}).deletion("soft")// Soft delete support.edge("user").edge("project",{optional:true}).edges("tags")// Many-to-many.searchIndex("search_title_description",{searchField:"title",filterFields:["userId","completed"],}),});
// convex/authSchema.ts - Auto-generated auth tables// Generated via: cd convex && npx @better-auth/cli generate -y --output authSchema.ts// Includes: user, session, account, organization, member, invitation
In authenticated functions,ctx.user is a pre-loadedSessionUser with full entity methods:
handler:async(ctx,args)=>{// ❌ Don't refetch the userconstuser=awaitctx.table("user").get(ctx.user._id);// ✅ Use pre-loaded userawaitctx.user.patch({credits:ctx.user.credits-1});};
Define limits inconvex/helpers/rateLimiter.ts:
exportconstrateLimiter=newRateLimiter(components.rateLimiter,{'comment/create:free':{kind:'fixed window',period:MINUTE,rate:10},'comment/create:premium':{kind:'fixed window',period:MINUTE,rate:30},});// Auto-selects tier based on user plan (free/premium)createAuthMutation({rateLimit:'comment/create'})({...});
Two different validator systems are used:
- Schema files (
convex/schema.ts): Usev.validators ONLY - Function files (
convex/*.ts): Usez.validators ONLY
// Schema (v.) - in convex/schema.ts.field('email',v.string(),{unique:true})// Functions (z.) - in convex/*.tsargs:{email:z.string().email(),id:zid('user')// Always use zid() for IDs}
pnpm dev# Start dev serverspnpm typecheck# Run TypeScript checkspnpm lint# Check code with Ultracite/Biomepnpm lint:fix# Fix linting and formatting issuespnpm check# Run all checks (lint, ESLint, TypeScript)pnpm seed# Seed databasepnpm reset# Reset databasepnpm studio# Open Convex dashboard
- Never use raw
query/mutation- Always use custom wrappers - Use
zid()for IDs in functions,v.id()in schemas - Pass
{}for no args in queries, notundefined - Use
ctx.table()instead ofctx.db(banned, except for streams first param) - Leverage pre-loaded
ctx.userin auth contexts - Use
.optional()not.nullable()for optional args - Never create indexes for edge-generated fields
convex/├── functions.ts # Custom function wrappers├── schema.ts # Database schema├── auth.ts # Better Auth setup├── todos.ts # Example CRUD operations└── helpers/ └── rateLimiter.tssrc/lib/convex/├── hooks/ # React hooks├── server.ts # RSC helpers├── auth-client.ts # Client auth setup└── components/ # Auth componentsThis template includes specialized AI agents and coding rules to enhance your development experience:
- convex.mdc ⭐ -CRITICAL: Complete Convex patterns guide (MUST READ for backend work)
- convex-client.mdc - Client-side Convex integration patterns
- convex-ents.mdc - Entity relationships and edge patterns
- convex-aggregate.mdc - Efficient counting with O(log n) performance
- convex-optimize.mdc - Performance optimization patterns
- convex-search.mdc - Full-text search implementation
- convex-streams.mdc - Advanced filtering with consistent pagination
- convex-trigger.mdc - Database triggers and reactive patterns
- convex-scheduling.mdc - Cron jobs and scheduled functions
- convex-http.mdc - HTTP endpoints and webhooks
- convex-examples.mdc - Reference implementations
- react.mdc - React component patterns
- nextjs.mdc - Next.js routing and RSC patterns
- jotai-x.mdc - State management patterns
- toast.mdc - Notification patterns
- ultracite.mdc - Code quality standards and formatting rules
To remove all starter code and keep only auth/user functionality:
# Function filesrm convex/todos.tsrm convex/todoInternal.tsrm convex/todoComments.tsrm convex/projects.tsrm convex/tags.tsrm convex/seed.tsAdmin users are configured via environment variables and automatically assigned admin role on first login:
# convex/.envADMIN="admin@example.com,another@example.com"
Auto-runs on dev server startup (--run init):
- Creates admin users from
ADMINenv variable - Assigns
role: 'admin'to pre-configured emails - Runs seed data in development environment
Development data population:
seedUsers: Creates Alice, Bob, Carol, Dave test usersgenerateSamples: Creates sample projects with todos (auth-protected action)- Preserves admin user and existing sessions during cleanup
Database cleanup utilities (dev only)
# Page routesrm -rf src/app/projects/rm -rf src/app/tags/# Componentsrm -rf src/components/todos/rm -rf src/components/projects/# Breadcrumb navigation (optional - uses todo examples)rm src/components/breadcrumb-nav.tsx
Remove these tables and their edges from the schema:
todostableprojectstabletagstabletodoCommentstableprojectMemberstable (join table)todoTagstable (join table)commentRepliestable (join table)
Update theusers table to remove edges:
user:defineEnt({// Keep profile fieldsname:v.optional(v.string()),bio:v.optional(v.string()),image:v.optional(v.string()),role:v.optional(v.string()),deletedAt:v.optional(v.number()),}).field("emailVerified",v.boolean(),{default:false}).field("email",v.string(),{unique:true});// Remove all todo/project related edges
Keep only:
aggregateUsers
Remove:
aggregateTodosByUseraggregateTodosByProjectaggregateTodosByStatusaggregateTagUsageaggregateProjectMembersaggregateCommentsByTodo
Remove aggregate registrations:
// Keep only:app.use(aggregate,{name:"aggregateUsers"});// Remove all todo/project/tag related aggregates
Remove all todo/project/tag related triggers if any exist.
Replace with a simple authenticated landing page:
exportdefaultasyncfunctionHomePage(){return(<divclassName="container mx-auto px-4 py-6"><h1className="mb-4 text-3xl font-bold">Welcome</h1><p>Your authenticated app starts here.</p></div>);}
After making these changes:
# Regenerate Convex typespnpm devThis will give you a clean authentication-only starter with:
- ✅ Better Auth integration
- ✅ User management
- ✅ Rate limiting
- ❌ No todo/project/tag starter code
- Convex Documentation
- Better Auth Convex Package - Local installation without component boundaries
About
A modern Convex + Next.js template with best practices
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Contributors2
Uh oh!
There was an error while loading.Please reload this page.