Overview
Real-time database with Convex
StarterApp uses Convex for real-time data synchronization with TypeScript type safety, reactive queries, and server functions. This guide covers the actual implementation patterns used in production.
Quick Start
All database operations use centralized table definitions from convex/tables.ts
and custom builders from convex/_helpers/builders.ts
for automatic authentication and permissions.
Architecture
Convex provides a complete backend infrastructure:
Real-Time Sync
Reactive queries that update automatically when data changes
Type Safety
Generated TypeScript types from schema definitions
Server Functions
Backend logic with authentication and authorization
ACID Transactions
Reliable data operations with consistency guarantees
Tables Pattern (Single Source of Truth)
Never Hardcode Table Names
Always import from convex/tables.ts
instead of using string literals or inline v.id()
calls.
The codebase uses a centralized table definition system in /convex/tables.ts
:
Import Table Constants
import { T, V, type UserId, type SupportTicketId } from "./tables";
Use Constants in Queries
// ✅ Good - uses T.supportTickets constant
export const getTicket = query({
args: { id: V.supportTicketId },
handler: async (ctx, { id }) => {
return await ctx.db.get(id);
},
});
// ❌ Bad - hardcoded string and type
export const getTicket = query({
args: { id: v.id("supportTickets") },
handler: async (ctx, { id }) => {
return await ctx.db.get(id as Id<"supportTickets">);
},
});
Type Safety Throughout
The pattern provides three exports:
- T - Table name constants (
T.user
,T.supportTickets
) - V - Validator helpers (
V.userId
,V.supportTicketId
) - Types - Type aliases (
UserId
,SupportTicketId
)
Why This Pattern?
Centralized table definitions prevent typos, enable refactoring, and ensure consistency across your entire codebase.
Builder Pattern (Auth + Guards)
All user-facing functions use custom builders from /convex/_helpers/builders.ts
that auto-inject authentication and permissions:
userQuery
import { userQuery } from "./_helpers/builders";
import { T } from "./tables";
export const getUserSupportTickets = userQuery({
args: {},
handler: async (ctx) => {
// ctx.viewerId is auto-injected and authenticated
// ctx.db automatically scopes to current user
return await ctx.db
.query(T.supportTickets)
.order("desc")
.collect();
},
});
userMutation
import { userMutation } from "./_helpers/builders";
import { T, type UserId } from "./tables";
import { assertLimitAction } from "./rateLimits";
export const createSupportTicket = userMutation({
args: {
title: v.string(),
description: v.string(),
},
handler: async (ctx, { title, description }) => {
const userId = ctx.viewerId as UserId;
// Rate limiting: 10 tickets per minute
await assertLimitAction(ctx, {
scope: "support:create",
viewerId: userId,
max: 10,
windowMs: 60_000,
});
return await ctx.db.insert(T.supportTickets, {
userId,
title,
description,
status: "Open",
updatedAt: Date.now(),
});
},
});
userAction
import { userAction } from "./_helpers/builders";
export const premiumExport = userAction({
args: { format: v.literal("csv") },
handler: async (ctx, args) => {
// Auto-injected Autumn billing integration
await ctx.check({ featureId: "exports" });
// Generate export...
const url = await generateExport(ctx, args.format);
// Track usage
ctx.track({ featureId: "exports", amount: 1 });
return { url };
},
});
Always Use Builders
Never use raw query
, mutation
, or action
for user-facing functions. Builders enforce authentication, rate limiting, and billing checks automatically.
Authentication & Authorization
The application uses identity helpers from /convex/lib/identity.ts
:
import { getUserIdOrThrow, assertOwnerByUserId } from "./lib/identity";
import { T, type UserId } from "./tables";
export const getMyNote = query({
args: { noteId: V.supportTicketId },
handler: async (ctx, { noteId }) => {
// Get authenticated user ID (throws UNAUTHENTICATED if not logged in)
const userId = await getUserIdOrThrow(ctx);
const note = await ctx.db.get(noteId);
// Verify ownership (throws FORBIDDEN or NOT_FOUND)
assertOwnerByUserId(note, userId, { concealExistence: true });
return note;
},
});
Security Best Practice
Use concealExistence: true
to prevent leaking information about records that exist but don't belong to the current user.
Error Handling
The application uses typed errors from /convex/_helpers/errors.ts
:
import { AppError } from "./_helpers/errors";
// Throw typed errors with proper HTTP status codes
throw new AppError("UNAUTHENTICATED"); // 401
throw new AppError("FORBIDDEN"); // 403
throw new AppError("NOT_FOUND"); // 404
throw new AppError("RATE_LIMITED"); // 429
throw new AppError("PLAN_REQUIRED"); // 402
throw new AppError("USAGE_EXCEEDED"); // 429
Each error code maps to the appropriate HTTP status in /convex/_helpers/errors.ts
.
Rate Limiting
Durable rate limiting implementation in /convex/rateLimits.ts
:
import { assertLimitAction } from "./rateLimits";
export const sendMessage = action({
args: { content: v.string() },
handler: async (ctx, { content }) => {
const userId = await getUserIdOrThrow(ctx);
// 10 messages per minute
await assertLimitAction(ctx, {
scope: "messages",
viewerId: userId,
max: 10,
windowMs: 60_000,
});
// Send message logic here
},
});
The rate limiter uses bucket-based counting with automatic cleanup of expired entries.
Project Structure
Schema Definition
/convex/schema.ts
- Define your database structure
Table Constants
/convex/tables.ts
- Export table names, validators, and types
Functions
/convex/*.ts
- Implement queries, mutations, and actions
Helpers
/convex/_helpers/builders.ts
- Custom function builders
/convex/_helpers/errors.ts
- Typed error handling
/convex/lib/identity.ts
- Authentication helpers
Naming Convention
Singular Table Names
This codebase uses SINGULAR table names (e.g., user
not users
). Convex uses the EXACT key you define - there's no pluralization magic.
Always:
- Use
user
notusers
- Use
supportTickets
notsupportTicket
(for compound names) - Import from
tables.ts
to avoid typos
Available Guides
Real-Time Sync
Reactive query patterns and live updates
Schema Updates
Schema evolution and migration strategies
AI Patterns
Database patterns optimized for AI development