---
title: "Aggregates"
description: "Denormalized counts and sums kept in sync automatically, read reactively with useAggregate."
canonical_url: "https://nuxt-backend.dev/docs/data/aggregates"
---
# Aggregates

> Denormalized counts and sums kept in sync automatically, read reactively with useAggregate.

Construct one `TableAggregate` per mounted instance and keep it in sync automatically with triggers.

```ts [backend/aggregates.ts]
import { TableAggregate, Triggers, withTriggers } from 'nuxt-backend/aggregate'
import { components } from './_generated/api'
import { mutation as rawMutation, query } from './_generated/server'
import type { DataModel } from './_generated/dataModel'

export const messagesCount = new TableAggregate<{
  Key: null
  DataModel: DataModel
  TableName: 'messages'
}>(components.aggregate, { sortKey: () => null })

const triggers = new Triggers<DataModel>()
triggers.register('messages', messagesCount.trigger())

// Use this wrapped `mutation` instead of the raw one so writes keep the aggregate in sync.
export const mutation = withTriggers(rawMutation, triggers)

// The query `useAggregate` reads.
export const countMessages = query({
  args: {},
  handler: ctx => messagesCount.count(ctx),
})
```

That query is the server half; read it reactively on the client:

```vue
<script setup lang="ts">
import { api } from '#backend/api'

// Live aggregate count: `data` is undefined until the first read lands.
const { data: messageCount, error } = useAggregate(api.aggregates.countMessages)
</script>

<template>
  <p v-if="error">{{ error.message }}</p>
  <p v-else>{{ messageCount ?? '…' }} messages</p>
</template>
```

`useAggregate(query, args?)` takes the query's arguments the way `useQuery` does, `'skip'` included, and returns `data`, `isLoading` and `error`. `data` is `undefined` until the first read lands, so a count never shows a `0` that is not an answer, and a query that answers something other than a number reads `0`. A failed read lands in `error` and never throws; [Composable conventions](https://nuxt-backend.dev/docs/reference/nuxt/conventions) has the rules every data composable follows.

## Sums and more instances

A sum is the same aggregate with a `sumValue` — `new TableAggregate<…>(components.aggregate, { sortKey: () => null, sumValue: message => message.text.length })` — read with `.sum(ctx)` in the query and `useAggregate` on the client.

Every `TableAggregate` on one component instance shares its storage: one B-tree per namespace. Two aggregates over the same table with the same key and no namespace write the same entries, and the second insert fails with *"key already exists"*. Give each one a place of its own, in either of two ways:

- **A namespace** on the same mount. The scaffold mounts one instance (`app.use(aggregate)` → `components.aggregate`). Add `namespace: () => 'size'` to the second aggregate and pass `{ namespace: 'size' }` to its reads (`.sum(ctx, { namespace: 'size' })`). Nothing changes in `convex.config.ts`; the playground does it this way.
- **A second mount**: `app.use(aggregate, { name: 'messagesSize' })` in `convex.config.ts`, with `components.messagesSize` passed to that `TableAggregate`. Use it when the aggregates should be cleared or migrated separately.

Triggers only see writes made after they are registered. Backfill a table that already has rows once with a [migration](https://nuxt-backend.dev/docs/data/jobs/migrations) whose `migrateOne` calls `messagesCount.insertIfDoesNotExist(ctx, doc)` — idempotent, so it is safe to re-run.

<page-keys>

- Try it live in the playground: [/playground/platform/aggregates](https://nuxt-backend.dev/playground/platform/aggregates)
- API reference: [/docs/reference/api/aggregate](https://nuxt-backend.dev/docs/reference/api/aggregate)
- Source: [https://github.com/qruto/nuxt-backend/blob/main/src/convex/integrations/aggregate.ts](https://github.com/qruto/nuxt-backend/blob/main/src/convex/integrations/aggregate.ts)

</page-keys>


## Sitemap

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