---
title: Channels, envelopes, wraps, claims
order: '10'
summary: "The four primitives that make ScaiClip work \u2014 where clips live, how\
  \ they travel, how keys are shared, how one-shot secrets get consumed exactly once."
path: concepts/channels
status: published
---

Four concepts carry the whole ScaiClip data model. Once these click, the rest of the product is straightforward.

## Channels

A **channel** is where clips live. Every clip belongs to exactly one channel.

Three kinds ship in v1:

| Kind | Members | Created | Use case |
|---|---|---|---|
| `personal` | One user | Auto on first WS connect | Cross-device clipboard for a single person |
| `directed` | Two users | Idempotent on first use | 1:1 private clipboard with a peer |
| `team` | ScaiKey group | Explicit `channel.create` | Shared clipboard for a project or team |

Team channels ship in M3. Personal and directed are live today.

A channel carries:

- A **key version** (starts at 1, bumps on membership change for team channels).
- A **TTL policy** — a per-clip default and a per-channel maximum.
- A **history depth** — how many past clips are retained.
- The set of member **devices** that hold wraps of the channel key.

The grid never sees the channel key. Every device holds an HPKE-wrapped copy under its own KEM public key. Rotating the key means rewrapping for every current device — expensive, so we do it only on membership change.

Personal channels never rotate — the membership is one user, and that user's devices come and go by re-onboarding, not rekey.

## Envelopes

An **envelope** is a signed record of a clip. It's what the grid stores and fans out. Envelopes are append-only per channel — every append gets a monotone `seq` and a grid-assigned timestamp.

Envelope structure (canonical CBOR keys):

```
1  clip_id      UUIDv7 — client-generated, time-ordered
2  channel_id   16 bytes
3  seq          grid-assigned, monotone per channel
4  sender       'usr_alice' — subject the caller authenticated as
5  device_id    'dev_alice_laptop' or 32-char hex
6  client_ts    ms since epoch
7  grid_ts      ms since epoch, grid-assigned
8  type_hint    0 text, 1 rich-text, 2 image, 3 files, 4 other
9  size_class   0 inline · 1 reference
10 cflags       bit0 SENSITIVE · bit1 ONE_SHOT · bit2 PINNED
11 ttl_s        client-requested, grid-clamped
12 key_version  channel's key version at publish time
14 payload_hash SHA-256 of the ciphertext
15 sig          Ed25519 over the canonical CBOR of keys 1-14
20 payload_ref  optional MediaRef pointer (§sharing modes)
```

The **signature covers keys 1-14 only** — grid-assigned fields (`seq`, `grid_ts`) are omitted from the signature input so grid assignment doesn't invalidate the sig.

Receivers verify the signature against the sender device's `sig_pub` (fetched from ScaiKey directory, cached locally).

## Wraps

A **wrap** is an HPKE-encrypted copy of a key. Two kinds:

- **Channel-key wrap** — the channel key wrapped for one device. One per `(channel, key_version, device)`. Installed when the device is onboarded.
- **One-shot wrap** — a per-clip content key, wrapped for one device. One per `(clip_id, device)`. Only present when the envelope carries `ONE_SHOT`.

Wraps let the grid deliver the same envelope to every member device without knowing any of their private keys. Every device unwraps its own copy locally.

Wraps live in `mod_scaiclip_key_wrap` with a `wrap_kind` discriminator (`channel` | `oneshot` | `scratch`).

### Onboarding a new device

When a new device joins a channel, it doesn't have a channel-key wrap yet. The onboarding flow:

1. New device calls `key.wraps.get` — grid returns nothing yet, queues a `WrapPending` row.
2. Grid pushes `wrap.request` to every online sibling device of the same user.
3. First sibling to answer calls `key.wraps.put` with a fresh HPKE-wrap of the channel key targeted at the new device's KEM public key.
4. Grid persists the wrap, marks pending fulfilled, pushes `wraps.ready` to the new device.
5. New device re-issues `key.wraps.get` and receives the wrap.

If no sibling is online, the new device waits — `wraps.ready` fires when a sibling comes back.

## Claims

A **claim** is how a `ONE_SHOT` clip gets consumed exactly once.

Publishing a `ONE_SHOT` clip requires wraps for every current member device (`E_WRAPS_INCOMPLETE` if any device is missing). Once published, the ciphertext sits waiting.

The first member device to call `clip.claim`:

- Gets the ciphertext + its own one-shot wrap in the response.
- Wins an atomic INSERT into `mod_scaiclip_claim` (unique on `channel_id, seq`).
- Triggers grid-side crypto-shred of the envelope's ciphertext + every sibling one-shot wrap.
- Grid fans out `clip.claimed` to remaining subscribers so their UIs update.

Every subsequent claim call gets `E_ALREADY_CLAIMED { claimed_by, at }`.

**Right primitive for**: MFA codes, one-time credentials, single-use tokens, "please rotate this after you've used it" secrets. The user knows: nobody else got it, and it's gone.

## What the grid holds

For clarity — this is what the server's database and cache contain, and nothing more:

- **Envelope rows.** Signed, but ciphertext is opaque bytes (or empty + a MediaRef pointer).
- **Wrap rows.** HPKE-encrypted opaque bytes.
- **Membership metadata.** Which device holds a wrap for which channel.
- **Presence.** Last-seen timestamps per device.
- **Audit rows.** `scaiclip.*` events for every state transition.
- **Redis proxy cache** for `ref_origin` transfers, TTL bounded (60 s default).
- **Scratch objects in Garage S3** — again, opaque ciphertext.

Never:

- Plaintext of any clip.
- Any channel key.
- Any device private key.
- Any decrypted MediaRef payload.

That posture is the whole point.

## What's next

- [Encryption & consent](./encryption.md) — the crypto stack that makes the "blind coordinator" real.
- [Scratch storage & public links](./scratch-and-public-sharing.md) — how bytes bigger than the envelope get shared.
- [Server-side integrations](./service-consumers.md) — SCSG for ScaiWave / ScaiTerm / ScaiDrive-server / ScaiEdit-server.
- [SCWP reference](../reference/scwp.md) — every wire verb.