# Settings

> Fields a merchant fills in for your app, the form Storeware draws for them, how your app reads them, and pictures for your components.

Source: https://storeware.ai/docs/extensions/settings

Most apps need a few answers from each shop: an API key for another service, a colour, how many items to show. Declare them in `[[settings]]` and Storeware draws the form for the merchant, keeps the answers, encrypts the secrets and hands them to your app. You build no settings page of your own.

> **Early access.** The CLI and the SDK are not on npm yet, so the commands below do not run for everyone. To build an app now, write to https://storeware.ai/contact.

## Declaring settings

Each `[[settings]]` entry is one field. Up to 30.

```toml
[[settings]]
key = "greeting"
label = "Greeting"
help = "Shown above the reviews on every product page."
type = "single_line_text"
default = "What people say"

[[settings]]
key = "reviews_shown"
label = "Reviews to show"
type = "integer"
required = true
default = 3

[[settings]]
key = "layout"
label = "Layout"
type = "select"
options = [
  { value = "list", label = "A list" },
  { value = "grid", label = "A grid" },
]

[[settings]]
key = "api_key"
label = "Your Reviews.io API key"
type = "secret"
required = true
```

| Field      | What it is                                                                               |
| ---------- | ---------------------------------------------------------------------------------------- |
| `key`      | How your app names it. `lower_snake_case`, never shown to the merchant.                  |
| `label`    | What the merchant reads beside the box. Up to 60 characters.                             |
| `help`     | A sentence under the box. Optional, up to 200 characters.                                |
| `type`     | One of the types below.                                                                  |
| `required` | Whether a save may leave it empty. `false` unless you say so.                            |
| `default`  | What your app reads until the merchant saves something. Not allowed on a `secret`.       |
| `options`  | For a `select` only: 2 to 30 choices, each a `value` your app reads and a `label` shown. |

| Type               | What the merchant sees           | What your app reads                    |
| ------------------ | -------------------------------- | -------------------------------------- |
| `single_line_text` | A text box                       | A string of one line, at most 500      |
| `multi_line_text`  | A larger text box                | A string, at most 2,000 characters     |
| `integer`          | A number box                     | A whole number                         |
| `decimal`          | A number box                     | A number                               |
| `boolean`          | A tick box                       | `true` or `false`                      |
| `url`              | A web address box                | A string starting `https://`           |
| `color`            | A colour picker and its code     | A string like `#1a6bd4`                |
| `select`           | A list to choose from            | The chosen option's `value`            |
| `secret`           | A hidden box, and Set or Not set | The string, in the clear, to you alone |

`storeware validate` checks every default against its type, with the same rule the form and the API use, so a default that validates always saves.

## What the merchant sees

On your app's page in their settings (**Settings**, **Apps**, then your app), the merchant finds a **Settings** card with one control per field, in the order you declared them. It appears only when your app declares settings.

A secret is never shown again once saved. The merchant sees **Set** or **Not set** beside it. Leaving its box empty keeps the saved one, typing in it replaces it, and **Remove it** clears it. A field that does not fit its type, or a required field left empty, is refused with the reason beside that box, and nothing is saved.

Fields come from the release the shop has approved. A field a newer release adds appears once the merchant approves that release. Only the merchant, or a key or person with permission to manage apps, can change them; your app cannot.

## Reading them

`GET /api/v1/app/settings`, with your app's token. It needs no permission.

```ts
const { values, updatedAt } = await client.settings();
// { values: { greeting: 'What people say', reviews_shown: 3, layout: null, api_key: 'rk_...' }, updatedAt: null }
```

```http
GET /api/v1/app/settings
Authorization: Bearer swt_...
```

```json
{
  "data": {
    "values": {
      "greeting": "What people say",
      "reviews_shown": 3,
      "layout": null,
      "api_key": "rk_live_..."
    },
    "updatedAt": "2027-02-07T09:12:40.000Z"
  }
}
```

Every field your installed release declares is there, by key: what the merchant saved, else its `default`, else `null`. A secret comes back in the clear, and only to your app. A key that an older release declared and yours no longer does is left out, but it is kept, so a release that brings it back finds it again. `updatedAt` is `null` until the merchant first saves.

Read them when you need them rather than copying them into your own database, or keep your copy fresh with the event below.

## When they change

When the merchant saves and something changed, your webhook URL is sent `app.settings_updated.v1`. You need not subscribe to it, and only your app hears it.

```json
{
  "event": "app.settings_updated.v1",
  "data": {
    "installationId": "9b1c...",
    "app": "reviews",
    "changed": ["greeting", "api_key"]
  }
}
```

`changed` lists the keys whose value your app would now read differently, and never the values: a delivery is logged and may be retried, and a secret has no place in either. Call `GET /api/v1/app/settings` for the new values. A save that changes nothing sends nothing.

With the SDK's router, it arrives at `onLifecycle`, or at a handler of its own:

```ts
router.on('app.settings_updated.v1', async (event) => {
  // `client` is the StorewareClient for the shop the event is about.
  const { values } = await client.settings();
  await saveForShop(event.storeId, values);
});
```

## Pictures for your components

An image in your app's components may only come from `cdn.mystoreware.com`, so a shop page never makes a shopper's browser fetch from your servers. To put a picture there, send its bytes to `POST /api/v1/app/assets` with your app's token. It needs no permission: you do not need `products:write` to show a picture.

```ts
const { url } = await client.uploadAsset({
  data: await readFile('badge.png'),
  contentType: 'image/png',
});
// https://cdn.mystoreware.com/apps/<your app>/<shop>/<sha256>.png
```

```http
POST /api/v1/app/assets
Authorization: Bearer swt_...
Content-Type: application/json

{ "data": "<base64>", "contentType": "image/png" }
```

- PNG, JPEG, WebP, GIF or AVIF, at most 5 MB. SVG is refused, because it can carry script.
- The bytes are checked. A file that is not a picture, or is a different type from the one you sent, is refused with a 400 saying why.
- A picture is kept under your app and the shop your token is for, named by the hash of its bytes. Sending the same picture again gives the same address, so you can upload on every deploy without filling the shop with copies.
