Leave Localhost logoLeave LocalhostDocs
Architecture

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 CRUD

Convex Components

The backend uses four Convex components, registered in convex.config.ts:

ComponentPurpose
@convex-dev/stripeStripe webhook signature verification and routing
@convex-dev/better-authBetter Auth integration (sessions, users, orgs)
@convex-dev/rate-limiterPer-key and global rate limiting
@convex-dev/resendEmail sending via Resend

Function Types

Convex functions come in four types:

TypeUsage
QueryRead data reactively. Clients subscribe and receive updates.
MutationWrite data transactionally. Runs in a transaction.
ActionSide effects (API calls, file uploads). Can call queries/mutations.
HTTP ActionHandle 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:

PathMethodHandler
/webhooks/stripe | /webhooks/polar | /webhooks/lemon-squeezyPOSTBilling webhook for the active provider only (one of these, or none)
/webhooks/resendPOSTResend 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_events older 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:

VariableRequiredPurpose
SITE_URLYesApp URL for redirects and email links
BETTER_AUTH_SECRETYesAuth encryption key (≥32 chars)
AUTH_GOOGLE_ID/SECRETYesGoogle OAuth credentials
BILLING_PROVIDERNopolar, stripe, or lemon_squeezy
RESEND_API_KEYYesAuth and transactional email sending
RESEND_AUTH_FROM_EMAILYesSender 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/:

ModuleResponsibility
betterAuthComponentReads.tsThe only module that calls components.betterAuth.adapter directly. Reads canonical org/member rows and projects them onto app-shaped types.
betterAuthEndpointParsers.tsNormalizes Better Auth server endpoint responses (data, member, invitation, list envelopes) onto app-shaped types.
betterAuthRecord.tsShared, fail-closed record/timestamp parsing primitives.
betterAuthTypes.tsApp-shaped data interfaces consumed by product code.
betterAuthAdapter.tsDeprecated 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:

  1. Update better-auth and @convex-dev/better-auth together.
  2. Run bunx convex codegen from packages/backend if component types change. Never hand-edit convex/_generated/**.
  3. Run bun --cwd packages/backend test. The endpoint-name contract (satisfies readonly (keyof AuthApi)[] in betterAuthHelpers.ts and permissions/betterAuth.ts) fails typecheck on a renamed/removed endpoint; betterAuthEndpointContract.test.ts asserts the configured plugins still expose those endpoints as callable functions at runtime.
  4. Treat any parser failure in betterAuthComponentReads.test.ts / betterAuthEndpointParsers.test.ts as a required integration update, not a flaky test.
  5. Read the Better Auth release notes for organization endpoint response-shape changes (getFullOrganization, listMembers, listInvitations, hasPermission, getActiveMemberRole) and update the parsers accordingly.

Next Reads

On this page