> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tappify.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Store data with hosted storage

> Declare a document collection and read and write it through the bridge, with no server of your own.

Hosted storage is a set of JSON document collections Tappify stores for your install and
validates against a schema you declare. A UI-only extension needs no server to remember
anything.

```bash theme={null}
tappify extension add storage --name preferences --scope user --singleton
```

## The manifest entry

```json theme={null}
{
  "contributes": {
    "storage": {
      "preferences": {
        "scope": "user",
        "singleton": true,
        "schema": { "$ref": "./schemas/preferences.json" }
      }
    }
  },
  "scopes": [{ "key": "storage:write" }]
}
```

The command writes this entry. You can hand-edit `tappify.extension.json` instead — the
`$schema` line gives your editor completion and validation, and
[`tappify extension doctor`](/extensions/test/doctor-checks) checks the result.

| `scope`        | Who the document belongs to                |
| -------------- | ------------------------------------------ |
| `user`         | Each teammate keeps their own copy         |
| `install`      | One copy shared by everyone on the install |
| `organization` | One copy shared across the workspace       |

`singleton: true` means one document per scope key, addressed with `get`, `set` and `patch`.
Leave it off and you get a collection with ids: `list`, `get`, `put`, `delete`.

A collection name is lower snake\_case, and one extension declares at most ten of them. The
name `settings` is reserved — it is the [settings
panel](/extensions/build/add-a-settings-panel)'s own document.

## Reading and writing

A singleton has a hook:

```tsx theme={null}
import { TapButton, useTapStorage } from "@tappify/extension-sdk";

export function CompactToggle() {
  const preferences = useTapStorage("preferences");
  const compact = preferences.data?.compact ?? false;

  return (
    <TapButton onClick={() => void preferences.patch({ compact: !compact })}>
      {compact ? "Expand" : "Compact"}
    </TapButton>
  );
}
```

`useTapStorage` returns `{ data, error, isLoading, refetch, save, patch }`. `save` replaces
the document; `patch` merges and resolves to the merged document. `data` is `undefined` while
the first read is in flight and `null` until the document is first written.

A collection with ids goes through the bridge:

```ts theme={null}
import { useTap } from "@tappify/extension-sdk";

const tap = useTap();

const { items, cursor } = await tap.storage.funnels.list({ limit: 50 });
await tap.storage.funnels.put("weekly", { steps: ["view", "install"] });
await tap.storage.funnels.delete("weekly");
```

Both are typed from your manifest once you have run
[`tappify extension types`](/extensions/build/generated-types) — an unknown collection name
or a document that does not match the schema is a type error, not a runtime surprise.

## Rules

* Writes are validated against the schema. Reads are not, so adding a field costs nothing
  and old documents keep loading. A change that breaks readers is a new collection name.
* Your install reaches only its own documents. There is no path to another install's or
  another extension's.
* `install`- and `organization`-scoped documents are passed to your server read-only as
  `documents` on every call, so your server never asks Tappify for them. Servers cannot
  write storage.
* A write broadcasts to every mount of your extension and every open tab of that install.
* Uninstalling deletes every document. When a teammate leaves the workspace, their
  `user`-scoped documents go with them. An owner can export an install's documents as JSON.
* Personal-data field names — `email`, `name`, `phone`, `address`, `ip` — put your release in
  front of a reviewer. Do not store personal data here.

Limits: 256 KB per document, 1,000 documents per collection per scope key, 50 MB per
install, 300 storage calls per minute. They are all on
[Storage limits and rate limits](/extensions/reference/limits).
