---
title: "How it works"
description: "How the Convex client, the Nuxt server and the same-origin auth proxy fit together, and what the one backend component mounts beside the upstream ones."
canonical_url: "https://nuxt-backend.dev/docs/start/concepts/how-it-works"
---
# How it works

> How the Convex client, the Nuxt server and the same-origin auth proxy fit together, and what the one backend component mounts beside the upstream ones.

The package ships two things in one:

<table>
<thead>
  <tr>
    <th>
      Layer
    </th>
    
    <th>
      What it does
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <strong>
        Nuxt module
      </strong>
      
       (<code>
        nuxt-backend
      </code>
      
      )
    </td>
    
    <td>
      Registers plugins, composables, server utilities, the auth proxy, and auto-scaffolds Convex root files
    </td>
  </tr>
  
  <tr>
    <td>
      <strong>
        Convex component
      </strong>
      
       (<code>
        nuxt-backend/convex.config
      </code>
      
      )
    </td>
    
    <td>
      Defines the all-in-one <code>
        backend
      </code>
      
       component: the Better Auth adapter + auth tables, transactional email (the provider component nested inside), the billing entitlement cache, gift purchases, the AI metering records, and the webhook delivery log
    </td>
  </tr>
</tbody>
</table>

The scaffolded `convex.config.ts` mounts the `backend` component plus the six upstream `@convex-dev/*` components (`aggregate`, `migrations`, `persistentTextStreaming`, `polar`, `rateLimiter`, `workflow`) at the app level — the seventh, `resend`, is mounted inside the `backend` component — one explicit `app.use` each — see the [component tree](https://nuxt-backend.dev/docs/start/concepts/how-it-works).

## Request flows

Three transports run side by side, each with its own path through the stack:

<card-group>
<card icon="i-lucide-zap" title="Real-time">

`Browser ⇄ Convex Cloud` over a **WebSocket** (`ConvexVueClient`) — queries subscribe, Convex pushes live updates.

</card>

<card icon="i-lucide-server" title="SSR">

`Browser → Nuxt → Convex` over HTTP (`ConvexHttpClient`) — the server renders HTML and ships a hydration payload.

</card>

<card icon="i-lucide-shield-check" title="Auth">

`Browser → /api/auth/* → Nuxt proxy → Better Auth → Convex` — the session token and `Set-Cookie` return same-origin.

</card>
</card-group>

### Real-time data

The client `ConvexVueClient` connects over a **WebSocket** for real-time reactivity. `useQuery`, `useMutation`, `useAction`, and pagination subscribe through this client and update live.

<demo-connection>



</demo-connection>

### SSR

On the server, `fetchQuery`, `fetchMutation`, `fetchAction`, and `preloadQuery` use a `ConvexHttpClient` under the hood. Pass `{ token }` when authentication is required. `preloadQuery` produces a payload that hydrates seamlessly into the live client via `usePreloadedQuery`.

### Authentication

`/api/auth/*` is **proxied** from Nuxt to the Convex site URL so cookies stay same-origin. Better Auth persists users and sessions through the Convex adapter inside the `backend` component, and issues a session token the client uses to authenticate its WebSocket.

### Agents and inbound webhooks

Beyond those three, two more paths end in the backend without a browser in front:

- **Agents** — `Agent → /mcp (Nitro, OAuth gate) → POST <site URL>/mcp/exchange → Convex`. The gate exchanges the agent's Bearer token for a Convex-authenticated session, and the tools run as the user who consented. See [MCP server](https://nuxt-backend.dev/docs/agents/mcp).
- **Inbound webhooks** — `Provider → <site URL>/billing/events | /email/events → Convex HTTP router`. `registerBackendRoutes` in the scaffolded `http.ts` mounts both (and the metered AI stream at `/ai/stream`); every delivery is signature-verified, deduped, logged, and fail-closed (`503`) until its secret is set. See [Webhooks](https://nuxt-backend.dev/docs/ship/production/webhooks).

<note>

This mirrors the official Convex + Better Auth integration: real-time over WebSocket, SSR over HTTP, and a same-origin auth proxy keeping cookies on your domain.

</note>

## What one call mounts

`nuxt-backend` ships one [Convex component](https://www.convex.dev/components) of its own — the all-in-one **backend** component — and bundles seven upstream `@convex-dev/*` components (six mounted beside it at the app level, plus [`@convex-dev/resend`](https://www.convex.dev/components/resend) nested inside it), all wired into Better Auth and configured by the [required env vars](https://nuxt-backend.dev/docs/reference/configuration/environment-variables#convex-deployment-variables). [Ecosystem](https://nuxt-backend.dev/docs/reference/ecosystem) lists the components that are planned, the ones that work beside these as recipes, and the ones left out.

<table>
<thead>
  <tr>
    <th>
      Capability
    </th>
    
    <th>
      Component
    </th>
    
    <th>
      Entrypoint
    </th>
    
    <th>
      Setup helper
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      Auth
    </td>
    
    <td>
      <code>
        backend
      </code>
      
       (this package — Better Auth tables + adapter)
    </td>
    
    <td>
      <code>
        nuxt-backend/auth
      </code>
    </td>
    
    <td>
      <code>
        setupAuth
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Email
    </td>
    
    <td>
      <code>
        backend
      </code>
      
       (nests <a href="https://www.convex.dev/components/resend" rel="nofollow">
        <code>
          @convex-dev/resend
        </code>
      </a>
      
      )
    </td>
    
    <td>
      <code>
        nuxt-backend/email
      </code>
    </td>
    
    <td>
      <code>
        setupEmail
      </code>
      
       — <code>
        components.backend.email.send
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Billing
    </td>
    
    <td>
      <a href="https://www.convex.dev/components/polar" rel="nofollow">
        <code>
          @convex-dev/polar
        </code>
      </a>
      
       + <code>
        backend
      </code>
      
       (entitlement/credit cache, gifts)
    </td>
    
    <td>
      <code>
        nuxt-backend/billing
      </code>
    </td>
    
    <td>
      <code>
        setupBilling
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Webhooks
    </td>
    
    <td>
      — (HTTP routes over auth + billing + email)
    </td>
    
    <td>
      <code>
        nuxt-backend/http
      </code>
    </td>
    
    <td>
      <code>
        registerBackendRoutes
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Rate limiting
    </td>
    
    <td>
      <a href="https://www.convex.dev/components/rate-limiter" rel="nofollow">
        <code>
          @convex-dev/rate-limiter
        </code>
      </a>
    </td>
    
    <td>
      <code>
        nuxt-backend/rate-limit
      </code>
    </td>
    
    <td>
      <code>
        setupRateLimiter
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Metered AI
    </td>
    
    <td>
      <a href="https://www.convex.dev/components/persistent-text-streaming" rel="nofollow">
        <code>
          @convex-dev/persistent-text-streaming
        </code>
      </a>
      
       + <code>
        backend
      </code>
      
       (credits)
    </td>
    
    <td>
      <code>
        nuxt-backend/ai
      </code>
    </td>
    
    <td>
      <code>
        setupAi
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Migrations
    </td>
    
    <td>
      <a href="https://www.convex.dev/components/migrations" rel="nofollow">
        <code>
          @convex-dev/migrations
        </code>
      </a>
    </td>
    
    <td>
      <code>
        nuxt-backend/migrations
      </code>
    </td>
    
    <td>
      <code>
        setupMigrations
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Aggregates
    </td>
    
    <td>
      <a href="https://www.convex.dev/components/aggregate" rel="nofollow">
        <code>
          @convex-dev/aggregate
        </code>
      </a>
    </td>
    
    <td>
      <code>
        nuxt-backend/aggregate
      </code>
    </td>
    
    <td>
      <code>
        TableAggregate
      </code>
      
      , <code>
        withTriggers
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Workflows
    </td>
    
    <td>
      <a href="https://www.convex.dev/components/workflow" rel="nofollow">
        <code>
          @convex-dev/workflow
        </code>
      </a>
    </td>
    
    <td>
      <code>
        nuxt-backend/workflows
      </code>
    </td>
    
    <td>
      <code>
        setupWorkflows
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      Search
    </td>
    
    <td>
      Convex native full-text search
    </td>
    
    <td>
      <code>
        nuxt-backend/search
      </code>
    </td>
    
    <td>
      <code>
        search
      </code>
      
      , <code>
        defineSearch
      </code>
    </td>
  </tr>
</tbody>
</table>

### The scaffolded `convex.config.ts` mounts everything

The module scaffolds `backend/convex.config.ts` for you (so does `npx nuxt-backend init`). It mounts the all-in-one `backend` component plus every bundled upstream component, declares the typed env contract (`backendEnv` from `nuxt-backend/app` — `AUTH_SECRET` + `SITE_URL` required, the rest optional), and forwards the `EMAIL_*` refs to the `backend` component — so you set the env vars and auth OTP / verification / invitation email just works:

```ts [backend/convex.config.ts]
import { defineApp } from 'convex/server'
import { backendEnv } from 'nuxt-backend/app'
import backend from 'nuxt-backend/convex.config'
import aggregate from '@convex-dev/aggregate/convex.config'
import migrations from '@convex-dev/migrations/convex.config'
import persistentTextStreaming from '@convex-dev/persistent-text-streaming/convex.config'
import polar from '@convex-dev/polar/convex.config'
import rateLimiter from '@convex-dev/rate-limiter/convex.config'
import workflow from '@convex-dev/workflow/convex.config'

const app = defineApp({ env: backendEnv })

// Components are isolated from the app env — forward the email config by
// reference so the deployment's values reach the backend component (auth,
// email, billing cache, gifts — the email provider is nested inside).
app.use(backend, {
  env: {
    EMAIL_API_KEY: app.env.EMAIL_API_KEY,
    EMAIL_FROM: app.env.EMAIL_FROM,
    EMAIL_TEST_MODE: app.env.EMAIL_TEST_MODE,
    EMAIL_WEBHOOK_SECRET: app.env.EMAIL_WEBHOOK_SECRET,
  },
})
app.use(aggregate)
app.use(migrations)
app.use(persistentTextStreaming)
app.use(polar)
app.use(rateLimiter)
app.use(workflow)

export default app
```

<note>

**Why not a one-call helper?** Convex discovers components from the `convex.config` imports it meets while bundling this file, and current Convex backends crash the push (`start_push 500`) when such an import is reached through an intermediate module or `app.use` runs in a loop — so the definition is deliberately explicit: every component imported and mounted in the root file, one `app.use` per component. Keeping the imports and `use` calls in the root file is what makes the push work — and it keeps the component tree obvious.

</note>

<tip>

Need to add env vars or skip an upstream component? Edit the file:

```ts
import { v } from 'convex/values'

const app = defineApp({
  env: { ...backendEnv, STRIPE_SECRET_KEY: v.optional(v.string()) },   // extend the declared env
})
// … and leave out the import + app.use(...) of a component nothing in backend/ uses
```

`backend` is the package's core — keep it mounted. Drop an upstream component only when nothing in `backend/` depends on it (`setupAuth` uses `rateLimiter`, `setupAi` needs `persistentTextStreaming` and `rateLimiter`, `setupBilling` needs `polar`, …). The env proxy throws on undeclared vars, so declare every var you read.

</tip>

### One `backend` component — plus the upstream components

The package ships **one Convex component of its own**, with flat function modules split by concern:

- **adapter** — the Better Auth tables + adapter (`components.backend.adapter.*`). Better Auth itself is *not* mounted as a component; it runs as app-level code over the component's adapter (the hybrid pattern).
- **email** — transactional email, with [`@convex-dev/resend`](https://www.convex.dev/components/resend) nested inside the component: `components.backend.email.send` / `status` / `get` / `cancel` / `cleanup` / `cleanupAbandoned` / `handleWebhook`. The component owns the `EMAIL_*` env.
- **billing** — the reactive entitlement / credit cache: `components.backend.billing.getByUser` / `upsert` / `userByCustomer` / `deleteByUser` / `clear`.
- **gifts** — gift purchases (the `billingGifts` table): `components.backend.gifts.*`, driven by [`setupBilling`](https://nuxt-backend.dev/docs/billing/credits/gifts).
- **ai** — the metered-stream request registry (the `aiRequests` table): `components.backend.ai.createRequest` / `getByStream` / `markSettled` / `markReleased` / `clear`. [`setupAi`](https://nuxt-backend.dev/docs/ai/streaming)'s `start` action records each request after reserving credits, the stream route loads it by stream id, and settlement / release mirror the reservation's fate.
- **webhooks** — the webhook delivery log, never a source of truth: `components.backend.webhooks.record` / `find` / `listRecent`. Every request on `/billing/events` and `/email/events` records its outcome there: a verified delivery as a row among the newest 200, a request refused before verification as a count per kind. It powers redelivery dedupe and the [DevTools](https://nuxt-backend.dev/docs/start/develop/devtools#webhooks) Webhooks page — see [the delivery log](https://nuxt-backend.dev/docs/email/delivery-tracking#the-delivery-log).

Resend is the one component nested **inside** another (it's self-contained, so `backend` re-exposes it through its `email` functions). The upstream components mount at the **app level** — a nested component is sandboxed from the parent app, and polar, rateLimiter, workflow, migrations, aggregate, and persistentTextStreaming all operate over **your** tables, functions, auth, and HTTP routes.

The dashboard component tree looks like this:

```text
app
├─ backend
│  └─ backend/resend
├─ polar
├─ rateLimiter
├─ workflow
│  └─ workflow/workpool
├─ migrations
├─ aggregate
└─ persistentTextStreaming
```

Because [components are isolated from the app's env](https://docs.convex.dev/components), the scaffolded `convex.config.ts` declares the deployment env vars with typed validators (`backendEnv`) and **forwards** `EMAIL_API_KEY`, `EMAIL_FROM`, `EMAIL_TEST_MODE`, and `EMAIL_WEBHOOK_SECRET` to the `backend` component by reference — the scaffold writes that wiring for you.

### Server helpers vs. composables — two halves, not duplicates

The `nuxt-backend/*` entrypoints are **server** code: you import them *inside your Convex functions* to construct and configure each component client (with sensible defaults baked in — auth rate limits, workflow retries, etc.). The auto-imported `useSearch` / `useAggregate` / `useBilling` / `useWorkflowStatus` composables are **client** code: thin Vue-reactive wrappers over `useQuery` that subscribe to the queries those functions expose.

You need both — the helper builds the server endpoint; the composable consumes it reactively in your components.

<note>

Each helper lives on its **own** entrypoint (rather than one barrel) so a Convex function only bundles the component it actually imports — the same reason the underlying `@convex-dev/*` packages are split, and what keeps `better-auth` out of a file that only does rate limiting.

</note>


## Sitemap

See the full [sitemap](https://nuxt-backend.dev/sitemap.md) for all pages.
