A modern Next.js starter template featuring Convex backend with Better 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
- Bun package manager
- GitHub and/or Google OAuth app credentials
- Clone and install dependencies:
git clone <your-repo-url> cd better-convex bun install
- Set up environment variables:
Create .env.local for Next.js:
cp .env.example .env.local
Create convex/.env for Convex:
cp convex/.env.example convex/.env
- Create GitHub OAuth App
- Create Google OAuth App
- Create Resend API key
Add credentials to convex/.env:
# Required environment variables GITHUB_CLIENT_ID=your_github_client_id GITHUB_CLIENT_SECRET=your_github_client_secret GOOGLE_CLIENT_ID=your_google_client_id GOOGLE_CLIENT_SECRET=your_google_client_secret RESEND_API_KEY=your_resend_api_key
- Start development servers:
# This will start both Next.js and Convex
bun dev- Initialize Convex environment (first time only):
In a new terminal:
bun sync
- Open the app:
Navigate to http://localhost:3005
bun init # Populate with sample data bun reset # Reset all tables bun studio # Open Convex dashboard
Instead of using raw Convex query/mutation/action, this template provides custom wrappers with built-in auth, rate limiting, and type safety:
// Public query - auth optional export const example = createPublicQuery()({ args: { id: zid("items") }, // Always use zid() for IDs returns: z.object({ name: z.string() }).nullable(), handler: async (ctx, args) => { return await ctx.table("items").get(args.id); }, }); // Protected mutation with rate limiting export const createItem = createAuthMutation({ rateLimit: "item/create", // Auto tier limits role: "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 methods return await ctx.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 wrappers const { data, isPending } = usePublicQuery(api.items.list, {}); // ALWAYS pass {} for no args const { data } = useAuthQuery(api.user.getProfile, {}); // Skips if not auth // Mutations with toast integration const updateSettings = useAuthMutation(api.user.updateSettings); toast.promise(updateSettings.mutateAsync({ name: "New" }), { loading: "Updating...", success: "Updated!", error: (e) => e.data?.message ?? "Failed", }); // Paginated queries const { data, hasNextPage, fetchNextPage } = usePublicPaginatedQuery( api.messages.list, { author: "alice" }, { initialNumItems: 10 } );
// Auth helpers for RSC const token = await getSessionToken(); // Returns string | null const user = await getSessionUser(); // Returns SessionUser & { token } | null const isAuthenticated = await isAuth(); // Fetch with auth const data = await fetchAuthQuery(api.user.getData, { id: userId }); const data = await fetchAuthQueryOrThrow(api.user.getData, { id: userId });
The template includes two schemas working together:
// convex/schema.ts - Application data with relationships const schema = 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-loaded SessionUser with full entity methods:
handler: async (ctx, args) => { // ❌ Don't refetch the user const user = await ctx.table("user").get(ctx.user._id); // ✅ Use pre-loaded user await ctx.user.patch({ credits: ctx.user.credits - 1 }); };
Define limits in convex/helpers/rateLimiter.ts:
export const rateLimiter = new RateLimiter(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/*.ts args: { email: z.string().email(), id: zid('user') // Always use zid() for IDs }
bun dev # Start dev servers bun typecheck # Run TypeScript checks bun lint # Check code with Ultracite/Biome bun lint:fix # Fix linting and formatting issues bun check # Run all checks (lint, ESLint, TypeScript) bun seed # Seed database bun reset # Reset database bun 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.ts
src/lib/convex/
├── hooks/ # React hooks
├── server.ts # RSC helpers
├── auth-client.ts # Client auth setup
└── components/ # Auth components
This 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 files
rm convex/todos.ts
rm convex/todoInternal.ts
rm convex/todoComments.ts
rm convex/projects.ts
rm convex/tags.ts
rm convex/seed.tsAdmin users are configured via environment variables and automatically assigned admin role on first login:
# convex/.env ADMIN="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 routes rm -rf src/app/projects/ rm -rf src/app/tags/ # Components rm -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 the users table to remove edges:
user: defineEnt({ // Keep profile fields name: 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:
export default async function HomePage() { return ( <div className="container mx-auto px-4 py-6"> <h1 className="mb-4 text-3xl font-bold">Welcome</h1> <p>Your authenticated app starts here.</p> </div> ); }
After making these changes:
# Regenerate Convex types
bun 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