Full-stack starter with TanStack Start, Convex, and Better Auth. Includes authentication (email/password + username), user profiles with avatar uploads, row-level security, role-based access control, rate limiting, audit logging, and SSR.
bun install
bun run setup # connects to Convex, generates .env.local, sets secrets
bun run dev # starts dev server on http://localhost:3000Tip
For local Convex backend, run bun run setup:local instead, then keep bunx convex dev --local running in a separate terminal.
The setup script handles everything automatically:
- Runs
convex dev --onceto create/connect the Convex project - Writes
CONVEX_DEPLOYMENTandVITE_CONVEX_URLto.env.local - Derives and adds
VITE_CONVEX_SITE_URLfrom the deployment name - Generates a
BETTER_AUTH_SECRETand sets it viaconvex env set - Sets
SITE_URL=http://localhost:3000viaconvex env set
| Variable | Source | Example |
|---|---|---|
CONVEX_DEPLOYMENT |
convex dev --once |
dev:your-project-name |
VITE_CONVEX_URL |
convex dev --once |
https://your-project-name.convex.cloud |
VITE_CONVEX_SITE_URL |
Setup script | https://your-project-name.convex.site |
| Variable | Purpose | Set By |
|---|---|---|
SITE_URL |
App base URL, used for CORS and auth redirects | Setup script (http://localhost:3000) |
BETTER_AUTH_SECRET |
32-byte base64 secret for signing auth tokens | Setup script (auto-generated) |
Note
These server-side secrets are stored in the Convex deployment, not in .env.local. View or change them via the Convex Dashboard or bunx convex env commands.
Important
You must set environment variables in both your hosting provider and the Convex Dashboard for production deployments.
Set in your hosting provider (Vercel, Netlify, etc.):
CONVEX_DEPLOYMENT=prod:your-project-name
SITE_URL=https://your-app.vercel.appSet in Convex Dashboard (or via CLI):
bunx convex env set SITE_URL https://your-app.vercel.app --prod
bunx convex env set BETTER_AUTH_SECRET $(openssl rand -base64 32) --prodVite config — how env vars are bridged to client and server
vite.config.ts makes these available on both client and server via process.env.*:
VITE_CONVEX_URL— Convex API endpointVITE_CONVEX_SITE_URL— Convex site URL (auto-derived fromCONVEX_DEPLOYMENTif not set)CONVEX_SITE_URL— alias for server-side accessSITE_URL— app base URL (defaults tohttp://localhost:3000)
Frontend:
- TanStack Start — full-stack React framework with SSR
- TanStack Router — file-based, type-safe routing
- TanStack Query — data fetching, caching, SSR query integration
- TanStack Form — form state management with Zod validation
- React 19 — UI library
- Tailwind CSS v4 — utility-first styling (OKLch color space)
- shadcn/ui — Radix-based component library (new-york style, zinc base)
- Lucide React — icon library
- Zod — schema validation (v4)
Backend:
- Convex — real-time backend (database, serverless functions, file storage)
- Better Auth — authentication via
@convex-dev/better-auth - convex-helpers — RLS, triggers, migrations, custom functions, relationships
- @convex-dev/rate-limiter — token bucket & fixed window rate limiting
Expand full project tree
├── src/
│ ├── components/
│ │ ├── ui/ # shadcn/ui primitives (avatar, button, dropdown, sidebar, etc.)
│ │ ├── app-sidebar.tsx # Main app sidebar (logo, nav, user menu)
│ │ ├── mode-toggle.tsx # Light/dark/system theme switcher
│ │ ├── nav-main.tsx # Primary nav items (collapsible)
│ │ ├── nav-secondary.tsx # Secondary nav items (flat)
│ │ ├── nav-user.tsx # User auth status + sign out
│ │ ├── site-header.tsx # Alternative header component
│ │ └── theme-provider.tsx # Theme context (localStorage, system detection, flash prevention)
│ ├── hooks/
│ │ └── use-mobile.ts # Mobile breakpoint detection (768px)
│ ├── lib/
│ │ ├── auth-client.ts # Better Auth client (convex + username plugins)
│ │ ├── auth-server.ts # Server-side auth (token management, SSR helpers)
│ │ ├── convex-cache.tsx # ConvexQueryCacheProvider wrapper (5min expiry, 250 max entries)
│ │ └── utils.ts # cn() — clsx + tailwind-merge
│ ├── routes/
│ │ ├── __root.tsx # Root layout (providers, sidebar, SSR auth token)
│ │ ├── index.tsx # Home page (personalized greeting)
│ │ ├── auth.tsx # Sign in / sign up (email, username, avatar upload)
│ │ ├── profile.tsx # User profile (edit name, username, bio, avatar)
│ │ ├── $.tsx # 404 catch-all
│ │ └── api/auth/$.ts # Better Auth API handler (GET/POST)
│ ├── router.tsx # TanStack Router config + Convex QueryClient
│ ├── routeTree.gen.ts # Auto-generated route tree (do not edit)
│ └── styles.css # Global styles + CSS variables (OKLch light/dark themes)
├── convex/
│ ├── _generated/ # Auto-generated types & API (do not edit)
│ ├── schema.ts # Database schema (users, auditLogs, migrations)
│ ├── auth.ts # Better Auth integration + user helpers
│ ├── auth.config.ts # Auth provider configuration
│ ├── authMutations.ts # Auth mutations (password, email, sessions, delete account)
│ ├── convex.config.ts # App config (betterAuth + rateLimiter components)
│ ├── errors.ts # Structured error codes & factory functions
│ ├── functions.ts # Custom function wrappers (authQuery, authMutation, RLS, RBAC)
│ ├── helpers.ts # Re-exports from convex-helpers (relationships, utilities)
│ ├── http.ts # HTTP endpoints (/api/health, /api/users) with CORS
│ ├── migrations.ts # Database migrations (addDefaultRole, backfillTimestamps)
│ ├── pagination.ts # Cursor-based pagination helpers
│ ├── rateLimit.ts # Rate limiter config (apiRead, apiWrite, userAction, criticalAction)
│ ├── security.ts # Row-level security rules + RBAC wrappers
│ ├── testing.ts # Test utilities (mock users, assertions)
│ ├── triggers.ts # Database triggers (audit logging on user changes)
│ ├── users.ts # User CRUD (getMe, getUser, updateProfile, avatar management)
│ ├── validators.ts # Centralized validators (pagination, profiles)
│ ├── zodFunctions.ts # Zod-validated function builders
│ └── tsconfig.json # Convex TypeScript config
├── scripts/
│ └── setup.ts # Project setup automation
├── public/
│ └── favicon.ico # Static favicon
├── package.json
├── tsconfig.json # Path aliases: @/* → src/*, @convex/* → convex/*
├── vite.config.ts # SSR config, env vars, plugins
├── eslint.config.js # @tanstack/eslint-config
├── prettier.config.js # No semis, single quotes, trailing commas
├── components.json # shadcn/ui config
└── .cta.json # Create TanStack App metadata
Caution
Do not edit files in convex/_generated/ or src/routeTree.gen.ts — these are auto-generated by Convex and TanStack Router respectively.
Note
Better Auth manages its own tables (user, session, account, verification) separately via the @convex-dev/better-auth component. The tables below are app-specific.
| Field | Type | Description |
|---|---|---|
authId |
string |
Better Auth user ID (indexed) |
email |
string |
User email (indexed) |
username |
string? |
Normalized lowercase username (indexed, unique) |
displayUsername |
string? |
Original casing username |
firstName |
string? |
Parsed from Better Auth name |
lastName |
string? |
Parsed from Better Auth name |
avatar |
StorageId | null? |
Uploaded avatar (overrides auth provider image) |
bio |
string? |
User bio |
role |
'user' | 'admin' | 'moderator'? |
Defaults to 'user' |
createdAt |
number? |
Creation timestamp |
updatedAt |
number? |
Last update timestamp |
Indexes: email, authId, username, createdAt
Immutable log of user and system actions, written by database triggers.
| Field | Type | Description |
|---|---|---|
action |
AuditAction |
Action type (e.g. user.created, auth.sign_in) |
userId |
Id<'users'>? |
App user ID |
authUserId |
string? |
Better Auth user ID |
targetId |
string? |
Affected resource ID |
targetType |
string? |
Affected resource type |
metadata |
any? |
Additional context |
timestamp |
number |
Event timestamp |
Actions: user.created, user.updated, user.deleted, user.role_changed, auth.sign_in, auth.sign_out, auth.password_changed, auth.email_changed, profile.updated
Indexes: userId, action, timestamp, userId_timestamp
Managed by convex-helpers. Tracks executed database migrations.
Built on Better Auth with the @convex-dev/better-auth Convex component.
Sign-in methods:
- Email + password
- Username + password
Sign-up flow:
- Name, email, password (required) + username, avatar (optional)
- Username validation: 3–30 chars, alphanumeric + underscore/dot, checked against 18 reserved names, real-time availability check (500ms debounce)
- Avatar upload:
image/*only, max 5MB, uploaded to Convex file storage
Session config:
| Setting | Value |
|---|---|
| Expiration | 7 days |
| Refresh | After 1 day |
| Fresh session | 10 minutes |
| Cookie cache | 5 minutes (compact strategy) |
Auth rate limits (HTTP layer)
| Endpoint | Limit |
|---|---|
/sign-in/* |
5/min |
/sign-up/* |
3/min |
/forgot-password |
3/hour |
/reset-password/* |
3/min |
/send-verification-email |
3/min |
/list-sessions |
30/min |
/get-session |
60/min |
SSR auth flow:
- Root route fetches auth token via
createServerFn - Token is set on
convexQueryClientbefore render - Authenticated queries work during server-side rendering
- Client hydrates with the same auth state
Auth mutations available: change password, forgot/reset password, update email, resend verification, delete account, list/revoke sessions.
Defined in convex/security.ts. Default policy: deny.
| Table | Read | Insert | Modify |
|---|---|---|---|
users |
Public | Deny (auth triggers only) | Owner only |
auditLogs |
Deny (admin queries bypass) | Allow (system/triggers) | Deny (immutable) |
Custom function wrappers in convex/functions.ts
| Wrapper | Auth Required | RLS | Role |
|---|---|---|---|
authQuery / authMutation |
Yes | No | Any |
optionalAuthQuery / optionalAuthMutation |
No | No | Any |
queryWithRLS / mutationWithRLS |
No | Yes | Any |
authQueryWithRLS / authMutationWithRLS |
Yes | Yes | Any |
adminOnlyQuery / adminOnlyMutation |
Yes | No | admin |
moderatorQuery / moderatorMutation |
Yes | No | admin or moderator |
Application-level rate limits via @convex-dev/rate-limiter (component-based, manages its own tables):
| Name | Rate | Burst | Use Case |
|---|---|---|---|
apiRead |
100/min | 20 | HTTP GET endpoints |
apiWrite |
30/min | 10 | HTTP POST/PUT endpoints |
userAction |
60/min | 10 | Authenticated user actions |
criticalAction |
10/min | 5 | Sensitive operations |
Database triggers (convex/triggers.ts) automatically log:
- User insert:
user.createdwith email, name - User update:
user.role_changedif role changes, otherwiseuser.updated - User delete:
user.deletedwith email
Admin-only queries available: listAuditLogs, getAuditLogsForUser.
Defined in convex/http.ts with CORS support (origin from SITE_URL env var).
| Method | Path | Auth | Rate Limit | Description |
|---|---|---|---|---|
GET |
/api/health |
No | No | Health check ({ status, timestamp }) |
GET |
/api/users?id=<userId> |
No | apiRead |
Public user profile |
GET |
/api/users/list?cursor=...&limit=... |
No | apiRead |
Paginated user list |
* |
/api/auth/* |
— | See auth limits | Better Auth routes (auto-registered) |
| Path | Component | Auth | Description |
|---|---|---|---|
/ |
index.tsx |
No | Home page with personalized greeting |
/auth |
auth.tsx |
No | Sign in / sign up (redirects if already authenticated) |
/profile |
profile.tsx |
Yes | Profile editor (redirects to /auth?redirect=/profile if unauthenticated) |
/api/auth/* |
api/auth/$.ts |
— | Server-side Better Auth handler |
/* |
$.tsx |
No | 404 catch-all |
| Script | Command | Description |
|---|---|---|
setup |
bun run setup |
Connect to Convex cloud, generate .env.local, set secrets |
setup:local |
bun run setup:local |
Same but with local Convex backend |
dev |
bun run dev |
Start Vite dev server on port 3000 |
build |
bun run build |
Production build |
serve |
bun run serve |
Preview production build |
test |
bun run test |
Run Vitest |
lint |
bun run lint |
Run ESLint |
format |
bun run format |
Run Prettier |
check |
bun run check |
Prettier write + ESLint fix |
clean |
bun run clean |
Remove node_modules, dist, .output, bun.lock and reinstall |
-
Deploy Convex to production:
bunx convex deploy --cmd "bun run build" -
Set Convex production environment variables:
bunx convex env set SITE_URL https://your-app.vercel.app --prod bunx convex env set BETTER_AUTH_SECRET $(openssl rand -base64 32) --prod
-
Set hosting provider environment variables (Vercel, Netlify, etc.):
CONVEX_DEPLOYMENT=prod:your-project-name SITE_URL=https://your-app.vercel.app
MIT