> ## 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.

# Add a settings panel

> Collect the owner's configuration from a JSON Schema and receive it on every server call.

A settings panel is the one form owners see for your extension, on the install's page in
their dashboard. Its values persist as a reserved storage document and reach your server as
`documents.settings` on every call.

```bash theme={null}
tappify extension add settings
```

## The manifest entry

```json theme={null}
{
  "contributes": {
    "settings": {
      "entry": "src/settings.tsx",
      "schema": { "$ref": "./schemas/settings.json" }
    }
  }
}
```

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.

The command also writes `schemas/settings.json` with a single `enabled` boolean in it,
there to be replaced by what owners set. The examples on this page are written
against two properties:

```json theme={null}
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "Settings",
  "type": "object",
  "properties": {
    "refreshMinutes": {
      "type": "number",
      "title": "Refresh minutes",
      "description": "How often the summary asks the server for new numbers"
    },
    "showDelta": {
      "type": "boolean",
      "title": "Show change",
      "description": "Show the change against the previous period"
    }
  },
  "required": ["refreshMinutes"],
  "additionalProperties": false
}
```

Run `tappify extension types` after every edit to the schema, so the generated `TapSettings`
follows it.

## The component

`TapSettingsProps` gives you the current values and one callback. Render the schema with
`TapForm` rather than writing inputs by hand — it maps the same schema the manifest points
at, so the form and the generated `TapSettings` type cannot disagree.

```tsx theme={null}
import {
  TapForm,
  TapPageHeader,
  type TapSettingsProps,
} from "@tappify/extension-sdk";
import "@tappify/extension-sdk/styles.css";
import schema from "../schemas/settings.json";

export default function Settings({ values, onChange }: TapSettingsProps) {
  return (
    <div className="tap-stack">
      <TapPageHeader
        title="Settings"
        description="These values reach your server as documents.settings on every call."
      />
      <TapForm
        schema={schema}
        value={values}
        submitLabel="Save"
        onSubmit={(next) => onChange(next)}
      />
    </div>
  );
}
```

## Where the values go

Saving writes the install-scoped `settings` singleton, which is why `storage` may not
declare a collection of that name. Three things happen at once:

* The document is validated against your schema before it is stored.
* Every mount of your extension in that browser is told, so a widget reflects a saved
  preference without a reload.
* A `settings.changed` event fires, which your UI can subscribe to and your server can
  receive.

Your server sees the current values as `documents.settings` on every call it receives, so
it never asks Tappify for them.

```ts theme={null}
procedures: {
  getSummary: (request) => {
    const minutes = request.documents.settings?.refreshMinutes ?? 15;
    return { installs: 0, delta: 0, refreshedEvery: minutes };
  },
},
```

<CardGroup cols={2}>
  <Card title="Build forms from schemas" icon="table-list" href="/extensions/build/forms-from-schemas">
    What `TapForm` renders for each schema construct.
  </Card>

  <Card title="Store data with hosted storage" icon="database" href="/extensions/build/hosted-storage">
    Collections of your own, scoped per teammate, install or workspace.
  </Card>

  <Card title="Preview in the portal" icon="window" href="/extensions/test/preview-in-the-portal">
    Render the panel against fixture data before anything is installed.
  </Card>
</CardGroup>
