Skip to content

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.

License

Notifications You must be signed in to change notification settings

ramonclaudio/tanstack-convex-starter

Repository files navigation

tanstack-convex-starter

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.

Prerequisites

  • Bun runtime
  • Convex account (for cloud backend) or Docker (for local backend)

Quick Start

bun install
bun run setup        # connects to Convex, generates .env.local, sets secrets
bun run dev          # starts dev server on http://localhost:3000

Tip

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:

  1. Runs convex dev --once to create/connect the Convex project
  2. Writes CONVEX_DEPLOYMENT and VITE_CONVEX_URL to .env.local
  3. Derives and adds VITE_CONVEX_SITE_URL from the deployment name
  4. Generates a BETTER_AUTH_SECRET and sets it via convex env set
  5. Sets SITE_URL=http://localhost:3000 via convex env set

Environment Variables

Local Development (.env.local — auto-generated by setup)

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

Convex Dashboard / CLI (server-side secrets)

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.

Production

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.app

Set 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) --prod
Vite 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 endpoint
  • VITE_CONVEX_SITE_URL — Convex site URL (auto-derived from CONVEX_DEPLOYMENT if not set)
  • CONVEX_SITE_URL — alias for server-side access
  • SITE_URL — app base URL (defaults to http://localhost:3000)

Stack

Frontend:

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

Project Structure

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.

Database Schema

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.

users

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

auditLogs

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

migrations

Managed by convex-helpers. Tracks executed database migrations.

Authentication

Built on Better Auth with the @convex-dev/better-auth Convex component.

Sign-in methods:

  • Email + password
  • Username + password

Sign-up flow:

  1. Name, email, password (required) + username, avatar (optional)
  2. Username validation: 3–30 chars, alphanumeric + underscore/dot, checked against 18 reserved names, real-time availability check (500ms debounce)
  3. 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:

  1. Root route fetches auth token via createServerFn
  2. Token is set on convexQueryClient before render
  3. Authenticated queries work during server-side rendering
  4. Client hydrates with the same auth state

Auth mutations available: change password, forgot/reset password, update email, resend verification, delete account, list/revoke sessions.

Security

Row-Level Security (RLS)

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)

Role-Based Access Control (RBAC)

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

Rate Limiting

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

Audit Logging

Database triggers (convex/triggers.ts) automatically log:

  • User insert: user.created with email, name
  • User update: user.role_changed if role changes, otherwise user.updated
  • User delete: user.deleted with email

Admin-only queries available: listAuditLogs, getAuditLogsForUser.

HTTP API

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)

Routes

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

Available Scripts

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

  1. Deploy Convex to production:

    bunx convex deploy --cmd "bun run build"
  2. 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
  3. Set hosting provider environment variables (Vercel, Netlify, etc.):

    CONVEX_DEPLOYMENT=prod:your-project-name
    SITE_URL=https://your-app.vercel.app

License

MIT

About

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.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published