# {{projectName}} / {{appName}}

Voltro collaborative-editing frontend (template: **frontend-collab**).

A document editor whose textarea is bound to a `crdtText()` body over the
framework's reactive loop. Type in **two browser tabs at once** and the edits
**converge** — no last-write-wins loser — because the api merges every update
into the shared document server-side, then broadcasts the merged result back
live. Zero infra: the merge and the broadcast are in-process on a single
`voltro dev`.

## Scaffold the pair

`frontend-collab` consumes a sibling api — scaffold it **together with
[`api-collab`](../api-collab)** (which exposes the `documents.list` subscription
+ `documents.create` / `documents.setBody` mutations):

```bash
voltro create-project collab --api=api-collab --web=frontend-collab
cd collab
pnpm install
pnpm dev            # boots BOTH apps (api on :4000, web on its portRange port)
```

Open the web app, click **Create document**, then open a **second tab** on the
same URL and type in both. Every keystroke merges — both tabs' edits survive.

## How it works

```
 Tab A                              api (api-collab)
 ─────                              ────────────────
 local crdtText() handle           documents.setBody (crdtText 'body' column)
   │  keystroke → insert/delete       └─ runtime folds the update into the
   │  → handle.encode()  ──rpc──►        STORED state (mergeCrdtStates) → merged
   ▼                                        └─► reactive delta ──WS──► every tab
 useSubscription('app',
   'documents.list')  ◄──WS── merged body ── handle.merge(body) → textarea
```

1. **`useSubscription('app', 'documents.list')`** streams the shared document,
   including its **merged** `body` (encoded CRDT bytes), on every write.
2. A **local `crdtText()` handle** (`@voltro/local-first`) is seeded from that
   body. Each keystroke applies a contiguous insert/delete to the handle and
   sends `handle.encode()` via **`useMutation('app', 'documents.setBody')`**.
3. The api's **authoritative server-side merge** folds the update into the
   stored state before broadcasting — so concurrent edits converge.
4. Incoming server bodies are **folded back** into the local handle
   (idempotent), keeping every tab in sync.

This page is exactly the app-level binding the local-first docs describe as the
`SyncClient` transport: `push` = the `setBody` mutation, `onRemoteState` = the
`documents` subscription.

## Not wired here: presence cursors

Live "who else is editing" cursors (`usePresence` from
`@voltro/local-first/react`) need a `PresenceChannel` bound to the app's
broadcast broker — a runtime binding, not zero-infra in a single dev process
across tabs — so this template omits them. The CRDT text convergence above needs
none of that; it rides the reactive engine you already have.

## Files

```
app.config.ts            type:web + the `apis: { app }` wiring
package.json             depends on @{{projectName}}/api (the sibling) + @voltro/local-first
src/pages/
  layout.tsx             root shell (header + <main>)
  page.tsx               the collaborative editor — subscription + CRDT handle + setBody
  page.test.tsx          render + edit-dispatch tests (jsdom)
src/locales/{en,de}.ts   bilingual UI strings
```

## Multi-tenancy note

The dev `AuthMiddleware` resolves the tenant from the `x-tenant` header,
defaulting to `'acme'` (the `TENANT` constant in `page.tsx`). The `documents`
table carries the `tenant()` mixin, so reads/writes are auto-scoped. Wire real
auth before production (see the authentication docs).
