An open API service indexing awesome lists of open source software.

https://github.com/ramonclaudio/tanstack-convex-starter

Minimal starter. TanStack Start + Convex + Better Auth.
https://github.com/ramonclaudio/tanstack-convex-starter

better-auth convex fullstack react shadcn-ui ssr starter-template tailwindcss tanstack-query tanstack-router tanstack-start typescript vite

Last synced: 2 months ago
JSON representation

Minimal starter. TanStack Start + Convex + Better Auth.

Awesome Lists containing this project

README

          

# 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](https://bun.sh) runtime
- [Convex](https://convex.dev) account (for cloud backend) or Docker (for local backend)

## Quick Start

```bash
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](https://dashboard.convex.dev) 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.):

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

Set in **Convex Dashboard** (or via CLI):

```bash
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:**

- [TanStack Start](https://tanstack.com/start) — full-stack React framework with SSR
- [TanStack Router](https://tanstack.com/router) — file-based, type-safe routing
- [TanStack Query](https://tanstack.com/query) — data fetching, caching, SSR query integration
- [TanStack Form](https://tanstack.com/form) — form state management with Zod validation
- [React 19](https://react.dev) — UI library
- [Tailwind CSS v4](https://tailwindcss.com) — utility-first styling (OKLch color space)
- [shadcn/ui](https://ui.shadcn.com) — Radix-based component library (new-york style, zinc base)
- [Lucide React](https://lucide.dev) — icon library
- [Zod](https://zod.dev) — schema validation (v4)

**Backend:**

- [Convex](https://convex.dev) — real-time backend (database, serverless functions, file storage)
- [Better Auth](https://better-auth.com) — authentication via `@convex-dev/better-auth`
- [convex-helpers](https://github.com/get-convex/convex-helpers) — RLS, triggers, migrations, custom functions, relationships
- [@convex-dev/rate-limiter](https://github.com/get-convex/rate-limiter) — token bucket & fixed window rate limiting

## Project Structure

Expand full project tree

```text
├── 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](#audit-logging).

| 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](https://better-auth.com) 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`](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`](https://github.com/get-convex/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`](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`](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=` | No | `apiRead` | Public user profile |
| `GET` | `/api/users/list?cursor=...&limit=...` | No | `apiRead` | Paginated user list |
| `*` | `/api/auth/*` | — | See [auth limits](#authentication) | 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:**

```bash
bunx convex deploy --cmd "bun run build"
```

2. **Set Convex production environment variables:**

```bash
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.):

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

## License

MIT