# DuckDB

> Embedded DuckDB AnalyticsSink — real column-store OLAP in-process, no external service to run.



---

<!-- source: en/plugins/duckdb.md -->
## DuckDB

_Embedded DuckDB AnalyticsSink — real column-store OLAP in-process, no external service to run._

`@voltro/plugin-duckdb` is the embedded `AnalyticsSink` — it runs DuckDB
**in-process** (via `@duckdb/node-api`) so you get real column-store +
vectorized OLAP performance without deploying an external service. Events land
in a `voltro_events` column-store table inside the DuckDB instance; the sink
implements all four contract methods against it. It's the sweet spot between
[postgres-lite](/docs/plugins/analytics-postgres) (no infra, but row-store) and
[ClickHouse](/docs/plugins/clickhouse) (fastest, but an external cluster). The
catch: it's single-process. For the shared `AnalyticsSink` API and the
`useAnalytics()` read methods, see [Analytics & warehouse sinks](/docs/plugins/analytics).

## Install

```
pnpm add @voltro/plugin-duckdb
```

`@duckdb/node-api` is an optional dependency — installed alongside the plugin.

## Wiring

```ts
// app.config.ts
import { duckdbAnalytics } from '@voltro/plugin-duckdb'

export default {
  type:      'api' as const,
  name:      'myApi',
  store:     'postgres' as const,
  analytics: duckdbAnalytics({ path: '.voltro/analytics.duckdb' }),
}
```

At boot the plugin opens the DuckDB connection and creates the events table; the
connection + instance are closed on graceful shutdown.

## Options

`duckdbAnalytics(options?)` — all optional:

| Option | Type | Default | Notes |
|---|---|---|---|
| `path` | `string` | in-memory | A file path (`.voltro/analytics.duckdb`) → durable across restarts, single-process. Omit or pass `:memory:` → events live in process memory, lost on restart (ephemeral dev / tests). |
| `mirrorTables` | `ReadonlyArray<string>` | `[]` (events-only) | Reactive tables to CDC-mirror into `voltro_mirror_<table>` (`{ id, data, version, is_deleted }`) inside DuckDB so analytical queries JOIN events against live user data. Deletes write a tombstone — filter `is_deleted = false`. |
| `mirrorPrimaryKey` | `string` | `'id'` | Primary-key column on the mirrored source rows. |

```ts
analytics: duckdbAnalytics({
  path: '.voltro/analytics.duckdb',
  mirrorTables: ['users', 'teams'],
})
```

## Tenant isolation

Parity with [postgres-lite](/docs/plugins/analytics-postgres): `track` stamps the event's `tenantId` into a `tenant_id` column, and `aggregate`/`timeseries`/`topN` filter by the query's `tenantId` (injected by `useAnalytics()` from the caller's subject), so a tenant-scoped read never spans tenants. A query with no `tenantId` is an unscoped system read. Swapping `postgresAnalytics()` → `duckdbAnalytics()` preserves the same tenant semantics.

## Single-process constraint

DuckDB can't open the same file from two workers. For multi-instance
deployments, either pin all analytics traffic to one replica
(sticky-session style) or use [`@voltro/plugin-clickhouse`](/docs/plugins/clickhouse)
instead. There is no provider escape-hatch Tag — the contract methods are the
whole surface.

## See also

- [Analytics & warehouse sinks](/docs/plugins/analytics) — the shared
  `AnalyticsSink` contract, the `useAnalytics()` read API, `composeAnalytics`,
  and the CDC-mirror details (incl. the `voltro_mirror_<table>` JOIN pattern).
