---
title: "Live data"
description: "Render rows on the server and keep them live with Electric shapes."
canonical_url: "https://wervt.app/guides/live-data"
---
# Live data

> Render rows on the server and keep them live with Electric shapes.

A **shape** is a table, optionally filtered, that [Electric](https://electric.ax) streams from
Postgres. In wervt you expose it with a route and read it with `useShape`.

<code-group>

```ts [server/api/shapes/todos.get.ts]
import { proxyShape } from '@wervt/nuxt/server'
import { defineEventHandler } from 'nuxt/server'

export default defineEventHandler((event) => proxyShape(event, { table: 'todos' }))
```

```vue [app.vue]
<script setup lang="ts">
import type { Todo } from './server/db/schema'

const { data: todos, isLive } = await useShape<Todo>('/api/shapes/todos')
</script>
```

</code-group>

## What happens

1. During SSR, `useShape` loads the shape through the route in-process and renders the rows.
2. The rows and the shape's offset go to the browser in the Nuxt payload.
3. The browser continues the Electric stream **from that offset**, so the snapshot is never
downloaded twice, and applies inserts, updates and deletes as they happen.

Writes go through normal API routes. Once Postgres commits, every open tab updates; there is
nothing to refetch.

## Access control

Shape routes are normal routes: check the session first, then narrow the rows with a `where`
clause. Pass values as `params`, never by building the SQL string.

```ts [server/api/shapes/notes.get.ts]
import { proxyShape } from '@wervt/nuxt/server'
import { createError, defineEventHandler, useSession } from 'nuxt/server'

export default defineEventHandler(async (event) => {
  const session = await useSession(event, { password: process.env.SESSION_SECRET! })
  if (!session.data.userId) throw createError({ status: 401 })
  return proxyShape(event, {
    table: 'notes',
    where: '"ownerId" = $1',
    params: [session.data.userId],
  })
})
```

The [Electric gateway](https://wervt.app/self-hosting/architecture#electric-gateway) additionally pins every shape to
the app's own schema, so a route can never expose another app's tables.

## Limits

- A shape is one table. Load several and combine them with `computed`.
- Rows are JSON: timestamps arrive as ISO strings.

<note>

Next up: [TanStack DB](https://tanstack.com/db) collections on top of shapes, for live queries with
joins and optimistic writes.

</note>


## Sitemap

See the full [sitemap](https://wervt.app/sitemap.md) for all pages.
