Convex Backend
The Convex backend lives at packages/backend/convex. It contains all server- side logic: database schema, queries, mutations, actions, authentication, billing, permissions, email, webhooks, and cron jobs.
The Convex backend lives at packages/backend/convex. It contains all server-
side logic: database schema, queries, mutations, actions, authentication,
billing, permissions, email, webhooks, and cron jobs.
Directory Structure
packages/backend/convex/
├── _generated/ # Auto-generated types and API references (do not edit)
├── auth/ # Organization roles, Better Auth helpers
├── auth.config.ts # Auth provider configuration
├── auth.ts # Better Auth setup with all plugins
├── better-auth/ # Better Auth Convex component config
├── billing/ # Billing adapters, catalog, grants, webhooks
│ ├── stripe/ # Stripe-specific adapter, mutations, queries, webhook
│ ├── polar/ # Polar-specific adapter, mutations, queries, webhook
│ └── lemonSqueezy/ # Lemon Squeezy-specific adapter, mutations, queries, webhook
├── email/ # Email sending facade and templates
│ └── templates/ # React Email templates (magic link, invitation, etc.)
├── organizations/ # Workspace CRUD, active org resolution, safety checks
├── permissions/ # App permissions, capabilities, resource policies
├── security/ # Step-up verification challenges, grants, crypto
├── utils/ # Shared validators
├── billing.ts # Public billing queries and actions
├── convex.config.ts # Convex app configuration with components
├── crons.ts # Scheduled jobs
├── env.ts # Environment variable validation (Zod + @t3-oss/env)
├── errors.ts # Structured error types and factories
├── http.ts # HTTP router (webhook endpoints)
├── invitations.ts # Invitation acceptance flow
├── members.ts # Member management (list, invite, update role, remove)
├── permissions.ts # Permission checking (canAppPermission, requireAppPermission)
├── rateLimit.ts # Rate limiter configuration
├── schema.ts # Database schema definition
├── settings.ts # User settings mutations
├── users.ts # User queries (getUser, getProfile, etc.)
├── web.ts # Marketing site actions (newsletter subscribe)
└── workspaceRecords.ts # Removable demo workspace records CRUDConvex Components
The backend uses four Convex components, registered in convex.config.ts:
| Component | Purpose |
|---|---|
@convex-dev/stripe | Stripe webhook signature verification and routing |
@convex-dev/better-auth | Better Auth integration (sessions, users, orgs) |
@convex-dev/rate-limiter | Per-key and global rate limiting |
@convex-dev/resend | Email sending via Resend |
Function Types
Convex functions come in four types:
| Type | Usage |
|---|---|
| Query | Read data reactively. Clients subscribe and receive updates. |
| Mutation | Write data transactionally. Runs in a transaction. |
| Action | Side effects (API calls, file uploads). Can call queries/mutations. |
| HTTP Action | Handle incoming HTTP requests (webhooks). |
Internal vs Public
- Public functions (
query,mutation,action) are callable from the client. - Internal functions (
internalQuery,internalMutation,internalAction) are only callable from other server-side functions.
HTTP Endpoints
All HTTP routes are defined in http.ts:
| Path | Method | Handler |
|---|---|---|
/webhooks/stripe | /webhooks/polar | /webhooks/lemon-squeezy | POST | Billing webhook for the active provider only (one of these, or none) |
/webhooks/resend | POST | Resend delivery tracking events |
/api/auth/* | * | Better Auth routes (login, signup, session) |
The billing webhook route is registered by the active provider's integration,
resolved from BILLING_PROVIDER via billing/providers.ts. With
BILLING_PROVIDER unset, none of the three billing routes exist.
Cron Jobs
Three scheduled cleanup jobs are configured in crons.ts. Each deletes in
bounded batches and reschedules itself while a backlog remains, so large tables
drain without exceeding Convex transaction limits:
- Cleanup expired verification rows — runs every hour, pruning expired
sensitive-action challenges and grants
(
security.cleanup.deleteExpiredVerificationRows). - Cleanup expired read notifications — runs daily, deleting notifications
that were read more than 90 days ago
(
notifications.cleanup.deleteExpiredReadNotifications). Unread notifications are never deleted by this job. - Cleanup expired audit events — runs daily, deleting
audit_eventsolder than 365 days (audit.cleanup.deleteExpiredAuditEvents).
The notification (90 days) and audit (365 days) retention windows are starter
defaults. Change them — or remove the cron — before launch if your product or
compliance requirements differ; the constants live at the top of each
cleanup.ts.
Environment Variables
All backend environment variables are validated in env.ts using Zod schemas
via @t3-oss/env-core. Variables are conditionally required based on the
configured BILLING_PROVIDER. Key variables:
| Variable | Required | Purpose |
|---|---|---|
SITE_URL | Yes | App URL for redirects and email links |
BETTER_AUTH_SECRET | Yes | Auth encryption key (≥32 chars) |
AUTH_GOOGLE_ID/SECRET | Yes | Google OAuth credentials |
BILLING_PROVIDER | No | polar, stripe, or lemon_squeezy |
RESEND_API_KEY | Yes | Auth and transactional email sending |
RESEND_AUTH_FROM_EMAIL | Yes | Sender for verification, reset, magic-link, and invitation emails |
Error Handling
The backend uses structured application errors via ConvexAppError. Every
error carries a machine-readable code and human-readable message that
survive the network boundary (unlike raw Error which Convex redacts in
production).
See Error Handling for details.
Better Auth Integration Boundary
Better Auth is the source of truth for identities, sessions, organizations, members, invitations, and organization roles. App-owned Convex tables store product data keyed by Better Auth ids, but never competing canonical organization/member state.
The integration is isolated in convex/auth/:
| Module | Responsibility |
|---|---|
betterAuthComponentReads.ts | The only module that calls components.betterAuth.adapter directly. Reads canonical org/member rows and projects them onto app-shaped types. |
betterAuthEndpointParsers.ts | Normalizes Better Auth server endpoint responses (data, member, invitation, list envelopes) onto app-shaped types. |
betterAuthRecord.ts | Shared, fail-closed record/timestamp parsing primitives. |
betterAuthTypes.ts | App-shaped data interfaces consumed by product code. |
betterAuthAdapter.ts | Deprecated compatibility barrel; do not import in new code. |
Parsers fail closed: a malformed Better Auth response raises a
Better Auth integration error rather than producing partial authorization
data. Treat such a failure as a blocked dependency upgrade, not a runtime edge
case.
Upgrading Better Auth
better-auth and @convex-dev/better-auth are version-pinned and validated at
the integration boundary. When upgrading:
- Update
better-authand@convex-dev/better-authtogether. - Run
bunx convex codegenfrompackages/backendif component types change. Never hand-editconvex/_generated/**. - Run
bun --cwd packages/backend test. The endpoint-name contract (satisfies readonly (keyof AuthApi)[]inbetterAuthHelpers.tsandpermissions/betterAuth.ts) fails typecheck on a renamed/removed endpoint;betterAuthEndpointContract.test.tsasserts the configured plugins still expose those endpoints as callable functions at runtime. - Treat any parser failure in
betterAuthComponentReads.test.ts/betterAuthEndpointParsers.test.tsas a required integration update, not a flaky test. - Read the Better Auth release notes for organization endpoint response-shape
changes (
getFullOrganization,listMembers,listInvitations,hasPermission,getActiveMemberRole) and update the parsers accordingly.
Next Reads
- Data Model — database tables and relationships.
- Server Actions and Functions — Convex function patterns.
- Security Model — auth and permissions architecture.
Monorepo
Leave Localhost is organized as a Turborepo monorepo managed with Bun. This structure lets all apps and packages share types, utilities, and build configuration while remaining independently deployable.
Data Model
The database schema is defined in packages/backend/convex/schema.ts. Convex uses a document-oriented model — each table stores JSON documents with validated shapes.