# Create a Space

A Space is created by the client. The server stores a public record, but the secret that opens the Space is minted
and distributed by the browser.

This page describes the production shape used by the Spaces app.

---

## The create-time contract

Creating a Space registers:

| Field | Visibility | Purpose |
|---|---|---|
| `room` | public | The stable Space id, usually `sp-...`. |
| `commit` | public | A commitment to the room key `mk`; proves future requests carry the right key. |
| `configSealed` | ciphertext | Agent persona, tools, admission policy, allow-list, model config, owner policy. |
| `gated` | public marker | Present only when the room is read-gated, so keyless reads can fail closed. |
| `gk` | public | Optional room invite public key for per-member signed attribution. |
| `invite` | ciphertext or pad | Used by multi-share invite flows; solo rooms carry a random pad to avoid leaking key model. |

The server does not receive `mk` on `create`.

## Key models

The current client supports:

| Model | Meaning |
|---|---|
| `1-of-1` | One link carries the full room key in the fragment. |
| `2-of-2` | Two shares are required; used for paired/couple-style rooms. |
| `{ k, n }` | Shamir threshold sharing for teams. |

The link shape keeps the room id visible and the key material in the fragment:

```text
https://your-app.example/space?room=sp-example#mk=...
https://your-app.example/space?room=sp-example#s=...
```

Fragments are not sent to the server by the browser. The app reads them locally and reconstructs `mk`.

## Basic create flow

The reference client wraps the details:

```js
const client = makeSpaceClient({ endpoint: '/space' })

const { room, mk, commit, links } = await client.create({
  keyModel: '1-of-1',
  agentName: 'Guide',
  persona: 'Help the group make decisions clearly.',
  tools: ['show_chart', 'write_pdf']
})
```

Under the hood, this calls:

```json
{
  "op": "create",
  "room": "sp-example",
  "commit": "<commit(mk)>",
  "configSealed": "<sealed config envelope>",
  "invite": "<pad or sealed invite>"
}
```

## Gated Spaces

For an email-gated Space, the admission rule and allow-list live inside the sealed config. The public record also gets
a `gated:true` marker. That marker leaks only that the room is gated, not who is allowed.

```js
await client.create({
  admission: 'email',
  allow: ['ada@example.com', 'grace@example.com'],
  requireChallenge: true
})
```

Reads and writes are then refused unless the member presents an allow-listed identity. Holding the link and `mk` is
not enough.

## Owner-governed Spaces

The owner rule adds a signed App Policy at create time. This lets an app owner mandate admission tiers for every room
created under that app. It is built, opt-in, and inert unless configured.

Read: [Owner-governed rooms](./the-owner-rule.md)

## What can go wrong

- A duplicate `room` returns `409 exists`.
- Missing `commit` returns `400 commit_required`.
- A governed room with an invalid owner policy is refused.
- A private/gated room without the right identity may create successfully but later refuse reads or writes.

## Code map

| Concern | File |
|---|---|
| Client create wrapper | `spaces/public/spaceClient.js` |
| Key creation and link parsing | `spaces/public/spaceConfig.js` |
| Create handler | `agent/spaceHandler.mjs` |
| Sealed config | `spaces/public/e2ee.js`, `agent/envelope.mjs` |
