# Manifest (/apps/manifest)

`poltura.app.json` sits at the root of your app project and describes everything Poltura needs to know about
the app: who it is, which store data it asks for, where its server lives, what it draws in the admin and on
the storefront, and which events and actions it handles. `poltura app build` checks it, bundles your workers and
writes the **release manifest**, `dist/manifest.json`. That file is what a reviewer approves and what a
merchant installs.

The CLI is strict on purpose:

- **Unknown fields fail.** A typo such as `redirectUrl` (instead of `redirectUrls`) is an error, not something
  quietly ignored.
- **Names that look like platform internals or credentials fail**, wherever they appear: keys or ids such as
  `token`, `secret`, `api_key`, `password`, `database`, `query`, `code` or `workflow`. An app never receives
  platform access, so the manifest has no place to ask for it.
- **Errors name the field.** The CLI prints the path and the reason, for example
  `app config schema: permissions.required.0.purpose: purpose must explain the merchant-visible use`.
  `0` is the first entry of the list.

A test checks this page against the CLI's schema: every table row, every required mark and default, and every
JSON example below, which must be accepted as part of a real manifest. A field cannot be added or removed without
this page changing too. **Required** means required inside its parent: `actions[].id` is required in every
action, but `actions` itself is optional.

## A small manifest [#a-small-manifest]

This is the complete manifest of the [native widgets example](/apps/examples), Parcel helper: two admin panels
and two actions. Everything not listed takes its default. The shorter JSON examples further down are parts of a
manifest like this one.

```json title="poltura.app.json"
{
  "schemaVersion": 1,
  "apiVersion": "v1",
  "handle": "parcel-helper",
  "releaseVersion": "1.0.0",
  "displayName": "Parcel helper",
  "description": "Suggests a box size on order pages and checks product listings before they go live.",
  "translations": [
    {
      "locale": "hu-HU",
      "displayName": "Csomagsegéd",
      "description": "Dobozméretet javasol a rendeléseknél, és ellenőrzi a termékoldalakat élesítés előtt."
    }
  ],
  "supportUrl": "https://parcel-helper.example.com/support",
  "privacyUrl": "https://parcel-helper.example.com/privacy",
  "backendOrigin": "https://parcel-helper.example.com",
  "redirectUrls": ["https://parcel-helper.example.com/oauth/callback"],
  "permissions": {
    "required": [
      {
        "id": "orders.read",
        "purpose": "Read the items of the order you have open to suggest a box"
      },
      {
        "id": "products.read",
        "purpose": "Read the product you have open to check its listing"
      }
    ],
    "optional": []
  },
  "actions": [
    {
      "id": "suggest_box",
      "purpose": "Suggest a box size from the items of the open order",
      "surfaces": ["admin"]
    },
    {
      "id": "check_listing",
      "purpose": "Check the title, description and image of the open product",
      "surfaces": ["admin"],
      "timeoutMs": 3000
    }
  ],
  "workers": [
    {
      "target": "admin.order",
      "entry": "src/workers/order-panel.tsx",
      "stylesheet": "src/styles.css"
    },
    {
      "target": "admin.product",
      "entry": "src/workers/product-panel.tsx",
      "stylesheet": "src/styles.css"
    }
  ]
}
```

The fields, grouped by what they are for:

| Group                                               | Fields                                                                                                           |
| --------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
| [Identity](#identity)                               | `schemaVersion`, `apiVersion`, `handle`, `releaseVersion`, `displayName`, `description`, `translations`, `appId` |
| [Links and your server](#links-and-your-server)     | `supportUrl`, `privacyUrl`, `homepageUrl`, `backendOrigin`, `redirectUrls`                                       |
| [Store access](#store-access)                       | `permissions`                                                                                                    |
| [Admin and storefront UI](#admin-and-storefront-ui) | `workers`, `elements`, `adminPages`, `storefrontPages`, `adminFrame`                                             |
| [Server calls and events](#server-calls-and-events) | `actions`, `webhooks`, `destinations`                                                                            |
| [App-level settings](#app-level-settings)           | `settings`                                                                                                       |
| [Advanced](#advanced)                               | `extensions`, `uiRuntime`, `sources`                                                                             |

## Identity [#identity]

| Field                        | Type and limits                                                              | Required | Default |
| ---------------------------- | ---------------------------------------------------------------------------- | -------- | ------- |
| `schemaVersion`              | The number `1`                                                               | Yes      | None    |
| `apiVersion`                 | The string `"v1"`                                                            | Yes      | None    |
| `handle`                     | 2 to 64 characters: lowercase letters and digits joined by single hyphens    | Yes      | None    |
| `releaseVersion`             | A semantic version without build metadata, such as `1.0.0` or `1.1.0-beta.1` | Yes      | None    |
| `displayName`                | 1 to 80 characters                                                           | Yes      | None    |
| `description`                | 1 to 1024 characters                                                         | Yes      | None    |
| `translations`               | A list of up to 16 entries, one per language                                 | No       | None    |
| `translations[].locale`      | A language tag such as `hu-HU` or `de`                                       | Yes      | None    |
| `translations[].displayName` | 1 to 80 characters                                                           | No       | None    |
| `translations[].description` | 1 to 1024 characters                                                         | No       | None    |
| `appId`                      | `app_` followed by 26 letters and digits                                     | No       | None    |

- **`schemaVersion` and `apiVersion`** name the manifest format and the App API version your app is written
  for. They are the only accepted values today.
- **`handle`** is your app's permanent id. It must be exactly the **Handle** you chose when you created the app
  in **Developer apps**; a release with any other handle is refused when you publish. You cannot change it later.
- **`releaseVersion`** labels this release for reviewers and merchants. Use a new version for every release you
  publish so you can tell them apart.
- **`displayName` and `description`** are what merchants see on the permission screen and in their list of
  installed apps.
- **`translations`** give the name and description in other languages. Each entry needs `displayName`,
  `description` or both, and each `locale` may appear once.
- **`appId`** is read only by the CLI: `poltura app publish` uses it when you leave out `--app`. It is not copied
  into the release manifest.

```json
{
  "handle": "parcel-helper",
  "releaseVersion": "1.2.0",
  "displayName": "Parcel helper",
  "description": "Suggests a box size on order pages.",
  "translations": [{ "locale": "hu-HU", "displayName": "Csomagsegéd" }],
  "appId": "0199f3a2-5b1c-7d4e-9f20-3a4b5c6d7e8f"
}
```

Common mistakes:

- `"handle": "Parcel-Helper"` fails with `handle must be lowercase letters and digits joined by single hyphens`.
- `"releaseVersion": "v1.0.0"` fails with `must be a semver release without build metadata`. So does
  `1.0.0+build.5`.
- A translation with only a `locale` fails: `translation must include displayName or description`.

## Links and your server [#links-and-your-server]

| Field           | Type and limits                                                       | Required | Default |
| --------------- | --------------------------------------------------------------------- | -------- | ------- |
| `supportUrl`    | An HTTPS URL, at most 2048 characters                                 | Yes      | None    |
| `privacyUrl`    | An HTTPS URL, at most 2048 characters                                 | Yes      | None    |
| `homepageUrl`   | An HTTPS URL, at most 2048 characters                                 | No       | None    |
| `backendOrigin` | An HTTPS origin: scheme and host, optionally a port, nothing after it | Yes      | None    |
| `redirectUrls`  | 1 to 10 different HTTPS URLs                                          | Yes      | None    |

- **`supportUrl` and `privacyUrl`** are shown to reviewers and merchants. The privacy page should say what store
  data you keep and for how long.
- **`backendOrigin`** is your server. Actions are sent to it (see [`actions`](#server-calls-and-events)).
- **`redirectUrls`** are the addresses Poltura may send a merchant back to at the end of the connection. The
  `redirectUri` your server passes to the SDK's OAuth client must match one of them character for character. The list
  is required even if your app never calls the App API; point it at the callback route you would use. See
  [Authentication](/apps/auth).

Every URL must be written in its exact canonical form, the way a browser would print it. That catches a few
things people do not expect:

- An origin on its own needs the trailing slash: `"homepageUrl": "https://parcel-helper.example.com"` fails with
  `HTTPS URL must be the exact canonical href`; write `https://parcel-helper.example.com/`.
- Host names are lowercase, default ports are left out, and there is no `#fragment`, user name or password.
- `http://` fails, as do wildcards such as `https://*.example.com/callback`.
- `"backendOrigin": "https://parcel-helper.example.com/api"` fails with
  `backend origin must not include a path or query`.

## Store access [#store-access]

| Field                            | Type and limits                                                                     | Required | Default |
| -------------------------------- | ----------------------------------------------------------------------------------- | -------- | ------- |
| `permissions`                    | An object with two lists                                                            | Yes      | None    |
| `permissions.required`           | Up to 32 permission requests, together with `optional`                              | Yes      | None    |
| `permissions.required[].id`      | One of the [permission ids](/apps/permissions#permissions), such as `products.read` | Yes      | None    |
| `permissions.required[].purpose` | 8 to 280 characters                                                                 | Yes      | None    |
| `permissions.optional`           | Up to 32 permission requests, together with `required`                              | Yes      | None    |
| `permissions.optional[].id`      | One of the [permission ids](/apps/permissions#permissions)                          | Yes      | None    |
| `permissions.optional[].purpose` | 8 to 280 characters                                                                 | Yes      | None    |

A permission lets your app read or change one kind of store data through the App API. They are the same
permissions merchants give their team members. The merchant sees every permission with your `purpose` on the
install screen before they approve, and grants all of the `required` ones. They may turn down `optional`
permissions, so check what was granted (see [Authentication](/apps/auth#permissions)) before you call an API
that needs one. An app that reads no store data may ask for none.

Write the purpose for the merchant, about their store: "Read the items of the order you have open to suggest a
box", not "Needed by the API".

```json
"permissions": {
  "required": [{ "id": "products.read", "purpose": "List your products to pick featured ones" }],
  "optional": [{ "id": "products.write", "purpose": "Mark your featured products in the catalog" }]
}
```

Common mistakes:

- Leaving out `optional`: both lists are required, so write `"optional": []`.
- The same permission in both lists, or twice in one list, fails with
  `permission "orders.read" cannot be both required and optional`.
- A required write takes its read with it, so the read cannot be optional: `products.read` next to a required
  `products.write` fails with `permission "products.read" comes with the required "products.write" and cannot be
optional`.
- A purpose shorter than 8 characters fails with `purpose must explain the merchant-visible use`.
- An id that is not in the [catalogue](/apps/permissions#permissions), such as `products:read` written with a colon instead of `products.read`.
- Asking for more than the app uses. The reviewer and the merchant see every permission; ask only for what the
  app needs.

## Admin and storefront UI [#admin-and-storefront-ui]

### `workers` [#workers]

A worker is one piece of native UI, written in React with Poltura's components and bundled by the CLI. Each
worker renders into one **target**: a place in the admin or on the storefront.

| Field                    | Type and limits                                                              | Required                     | Default                        |
| ------------------------ | ---------------------------------------------------------------------------- | ---------------------------- | ------------------------------ |
| `workers`                | 1 to 16 workers                                                              | No (see below)               | None                           |
| `workers[].target`       | One of the [UI targets](/apps/permissions#ui-targets), such as `admin.order` | Yes                          | None                           |
| `workers[].entry`        | Path of the `.tsx` file that calls `startUiWorker`, relative to the project  | Yes                          | None                           |
| `workers[].stylesheet`   | Path of a CSS file, relative to the project                                  | No                           | None                           |
| `workers[].inheritTheme` | `true` or `false`                                                            | No                           | `true`                         |
| `workers[].element`      | The `type` of a `defineBlock` in your `elements` files                       | For storefront block targets | None                           |
| `workers[].path`         | Where the bundle goes in the release: a relative path ending in `.js`        | No                           | `workers/<entry file name>.js` |

- A project needs `workers`, or the advanced [`uiRuntime` and `sources`](#advanced) pair instead. Every app has
  at least one worker; a hosted-only app still ships a small one, such as a home card.
- **`target`** decides where the worker appears. Admin targets are pages, the home and the detail panels of
  products, collections, orders and customers. Storefront targets are blocks merchants place on a template, the
  header and footer zones, and app pages. See [Admin](/apps/admin) and [Storefront](/apps/storefront).
- **`stylesheet`** is compiled with Tailwind and shipped with the release. With **`inheritTheme`** left on, the
  host's colours, fonts, radii, shadows and spacing are passed into the worker's styles, so it matches the admin
  or the store's theme. See [Styling](/apps/styling).
- **`element`** connects a storefront block worker to the `defineBlock` definition that gives it a name, an
  icon and settings in the Customizer. It is required for the storefront template targets and the header and
  footer zones, and optional for `storefront.app-page` and admin targets.

### `elements` [#elements]

| Field      | Type and limits                           | Required                                        | Default |
| ---------- | ----------------------------------------- | ----------------------------------------------- | ------- |
| `elements` | One file path, or a list of 1 to 32 paths | When any worker or extension names an `element` | None    |

The files that hold your `defineBlock` calls. The build reads them without running them and stops with the file,
line and column of any definition it cannot read. See [Settings and translations](/apps/ui#settings-and-translations).

### `adminPages` [#adminpages]

| Field                                | Type and limits                                     | Required | Default |
| ------------------------------------ | --------------------------------------------------- | -------- | ------- |
| `adminPages`                         | Up to 8 pages                                       | No       | None    |
| `adminPages[].id`                    | 1 to 48 characters, `snake_case`                    | Yes      | None    |
| `adminPages[].path`                  | The `path` of a worker whose target is `admin.page` | Yes      | None    |
| `adminPages[].title`                 | Plain text, at most 60 characters                   | Yes      | None    |
| `adminPages[].translations`          | Up to 16 entries, one per language                  | No       | None    |
| `adminPages[].translations[].locale` | A language tag such as `hu-HU`                      | Yes      | None    |
| `adminPages[].translations[].title`  | Plain text, at most 60 characters                   | Yes      | None    |
| `adminPages[].icon`                  | One of the page icons listed below                  | No       | None    |

An `admin.page` worker on its own is not reachable. Listing it here adds it to the **Apps** group of the admin
navigation, at `/apps/<installation id>/<page id>`. The admin shows the title of the translation whose `locale`
matches the merchant's language exactly, then one with the same language (`hu` for `hu-HU`), then `title`.

Page icons: `home`, `box`, `boxes`, `cart`, `receipt`, `users`, `user`, `tag`, `settings`, `chart`, `globe`,
`mail`, `file`, `files`, `image`, `link`, `star`, `heart`, `grid`, `list`, `search`, `bell`, `calendar`,
`message`.

### `storefrontPages` [#storefrontpages]

| Field                                     | Type and limits                                                      | Required | Default                  |
| ----------------------------------------- | -------------------------------------------------------------------- | -------- | ------------------------ |
| `storefrontPages`                         | Up to 8 pages                                                        | No       | None                     |
| `storefrontPages[].id`                    | 1 to 48 characters, `snake_case`, different from every admin page id | Yes      | None                     |
| `storefrontPages[].path`                  | The `path` of a worker whose target is `storefront.app-page`         | Yes      | None                     |
| `storefrontPages[].title`                 | Plain text, at most 60 characters                                    | Yes      | None                     |
| `storefrontPages[].slug`                  | 1 to 48 characters: lowercase letters, digits and hyphens            | Yes      | None                     |
| `storefrontPages[].translations`          | Up to 16 entries, one per language                                   | No       | None                     |
| `storefrontPages[].translations[].locale` | A language tag such as `hu-HU`                                       | Yes      | None                     |
| `storefrontPages[].translations[].title`  | Plain text, at most 60 characters                                    | Yes      | None                     |
| `storefrontPages[].icon`                  | One of the page icons above                                          | No       | None                     |
| `storefrontPages[].seo`                   | An object                                                            | No       | `{ "indexable": false }` |
| `storefrontPages[].seo.indexable`         | `true` or `false`                                                    | No       | `false`                  |

A storefront page is served at `/apps/<slug>` on the merchant's store. Leave `indexable` off unless the page has
content search engines should list. These slugs are taken by the store and cannot be used: `products`, `cart`,
`checkout`, `account`, `profile`, `blog`, `guides`, `legal`, `contact`, `faq`, `claim`, `review`, `order`,
`auth`, `pages`, `apps`, `api`, `sitemap`, `robots`.

```json
"workers": [
  { "target": "admin.page", "entry": "src/workers/settings-page.tsx" },
  { "target": "storefront.app-page", "entry": "src/workers/wishlist.tsx", "element": "wishlist-page" }
],
"elements": "src/elements.ts",
"adminPages": [{ "id": "settings", "path": "workers/settings-page.js", "title": "Settings", "icon": "settings" }],
"storefrontPages": [
  {
    "id": "wishlist",
    "path": "workers/wishlist.js",
    "title": "Wishlist",
    "slug": "wishlist",
    "translations": [{ "locale": "hu-HU", "title": "Kívánságlista" }]
  }
]
```

Common mistakes:

- A storefront block worker without `element` fails with
  `worker storefront.home is a block merchants place: name its defineBlock type in "element"`.
- `element` without `elements` fails with `"element": "parcel" needs "elements" pointing at the files with its
defineBlock call`.
- A page `path` that is not a built worker path fails with `admin page "parcel" path "workers/parcel.js" must
match a uiRuntime worker whose target is admin.page`. The built path is `workers/` plus the entry file's name
  with `.js`, unless the worker sets its own `path`.
- `"slug": "cart"` fails with `slug "cart" is reserved`.
- Titles are plain text: HTML tags, `javascript:` and similar are refused.

### `adminFrame` [#adminframe]

| Field                | Type and limits                                               | Required | Default |
| -------------------- | ------------------------------------------------------------- | -------- | ------- |
| `adminFrame`         | An object                                                     | No       | None    |
| `adminFrame.url`     | An HTTPS URL with no query or fragment, on a public host name | Yes      | None    |
| `adminFrame.actions` | A list of the bridge actions the page may use, each once      | Yes      | None    |

A **hosted page** is a page you serve from your own server, shown inside the admin in an iframe. It is the
choice when you need your own HTML and JavaScript. `actions` lists what the page may ask the admin to do:
`toast`, `set_loading`, `navigate_host`, `pick_resource`, `resize`, `get_session_token` and `get_appearance`.
Add `get_session_token` if the page calls your server; see
[Authentication](/apps/auth#session-tokens-for-hosted-pages).

```json
"adminFrame": { "url": "https://catalog-glance.example.com/app", "actions": ["get_session_token"] }
```

`localhost`, `*.localhost` and IP addresses fail with `App pages require a public HTTPS hostname.` Try hosted
pages on a real HTTPS host name, such as a tunnel.

## Server calls and events [#server-calls-and-events]

### `actions` [#actions]

An **action** is a call from a native worker to your server. The worker calls `useAction('suggest_box')`, the
platform signs the request and sends it to your server, and your server's answer comes back to the worker. Your
server never receives the merchant's credentials, and the worker never receives yours.

| Field                      | Type and limits                                                     | Required | Default         |
| -------------------------- | ------------------------------------------------------------------- | -------- | --------------- |
| `actions`                  | Up to 32 actions                                                    | No       | None            |
| `actions[].id`             | 1 to 48 characters, `snake_case`, starting with a letter            | Yes      | None            |
| `actions[].path`           | A path on `backendOrigin` starting with `/`, at most 256 characters | No       | `/actions/<id>` |
| `actions[].purpose`        | 8 to 280 characters                                                 | Yes      | None            |
| `actions[].surfaces`       | `admin`, `storefront` or both                                       | Yes      | None            |
| `actions[].timeoutMs`      | 1000 to 10000                                                       | No       | `5000`          |
| `actions[].maxInputBytes`  | 1 to 32768                                                          | No       | `32768`         |
| `actions[].maxOutputBytes` | 1 to 65536                                                          | No       | `65536`         |

- **`surfaces`** says where workers may call the action. A storefront call also carries the shopper.
- **`timeoutMs`** is how long the platform waits for your server before the worker's call fails.
- **`maxInputBytes` and `maxOutputBytes`** cap the JSON going each way; lower them to what your action needs.

How to receive and verify actions on your server, with `createActionHandler`, is on
[Authentication](/apps/auth#signed-actions).

Common mistakes: `"id": "suggestBox"` fails with `action id must be snake_case`; `"timeoutMs": 20000` fails with
`Number must be less than or equal to 10000`; `"id": "token"` fails with `action id "token" is not allowed`; a
path with `?`, `#`, `..` or `//` fails with `action path must be relative to backendOrigin`.

### `webhooks` [#webhooks]

| Field                    | Type and limits                                                                        | Required | Default |
| ------------------------ | -------------------------------------------------------------------------------------- | -------- | ------- |
| `webhooks`               | Up to 16 subscriptions                                                                 | No       | None    |
| `webhooks[].topic`       | One of the [webhook topics](/apps/permissions#webhook-topics), such as `order.created` | Yes      | None    |
| `webhooks[].destination` | An HTTPS URL                                                                           | Yes      | None    |

Poltura sends a signed POST to `destination` when the event happens in a store that installed your app. Each
topic needs the permission that reads its data: `order.*` needs `orders.read`. The `app.*` lifecycle topics need none. Each topic and each destination may
appear once. See [Webhooks](/apps/webhooks).

### `destinations` [#destinations]

| Field                    | Type and limits                  | Required | Default |
| ------------------------ | -------------------------------- | -------- | ------- |
| `destinations`           | Up to 16                         | No       | None    |
| `destinations[].id`      | 1 to 64 characters, `snake_case` | Yes      | None    |
| `destinations[].url`     | An HTTPS URL                     | Yes      | None    |
| `destinations[].purpose` | 8 to 280 characters              | Yes      | None    |

Other addresses on your side that receive store data, declared so they are part of the reviewed release.
Poltura checks and stores them but sends nothing to them today. Ids and URLs must be unique.

## App-level settings [#app-level-settings]

| Field                    | Type and limits                                                                           | Required   | Default |
| ------------------------ | ----------------------------------------------------------------------------------------- | ---------- | ------- |
| `settings`               | Up to 32 fields                                                                           | No         | None    |
| `settings[].id`          | 1 to 64 characters, `snake_case`                                                          | Yes        | None    |
| `settings[].type`        | `string`, `number`, `boolean`, `url` or `enum`                                            | Yes        | None    |
| `settings[].label`       | 1 to 80 characters                                                                        | Yes        | None    |
| `settings[].description` | At most 280 characters                                                                    | No         | None    |
| `settings[].required`    | `true` or `false`                                                                         | No         | None    |
| `settings[].default`     | A value of the field's type: text up to 500 characters, an HTTPS URL, or one of `options` | No         | None    |
| `settings[].options`     | 1 to 32 values, each at most 64 characters                                                | For `enum` | None    |

Poltura checks these fields and keeps them with the release, but no admin screen shows them yet. Settings a
merchant edits today belong to a worker: they come from its `defineBlock` definition and are read with
`useSettings()`. See [Settings and translations](/apps/ui#settings-and-translations).

Common mistake: an enum `default` that is not in `options` fails with `enum default must be one of the options`.

## Sign-in providers [#sign-in-providers]

A sign-in app lets shoppers sign in to a store with an outside account such as Google or Discord. Poltura's own
sign-in apps declare their provider in their manifest as `identity_provider`; the `poltura.app.json` format on
this page does not accept it yet, so a published app cannot add one.

| Key                                                    | Type and limits                                                                              |
| ------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| `identity_provider.spec.protocol`                      | `oidc` or `oauth2`                                                                           |
| `identity_provider.spec.issuer`                        | For `oidc`: an HTTPS URL                                                                     |
| `identity_provider.spec.scopes`                        | Up to 20 scope names; at least one for `oidc`                                                |
| `identity_provider.spec.authorizeParams`               | Up to 8 extra query parameters for the authorization request, never one the sign-in sets itself |
| `identity_provider.spec.authorizationEndpoint`         | For `oauth2`: an HTTPS URL                                                                   |
| `identity_provider.spec.tokenEndpoint`                 | For `oauth2`: an HTTPS URL                                                                   |
| `identity_provider.spec.userinfoEndpoint`              | For `oauth2`: an HTTPS URL                                                                   |
| `identity_provider.spec.pkce`                          | For `oauth2`: `false` only for a provider that refuses PKCE                                  |
| `identity_provider.spec.claims`                        | For `oauth2`: `subject`, and optionally `email`, `emailVerified`, `givenName`, `familyName`, `name`, `username` (the handle used when `name` is empty) |
| `identity_provider.settings.client_id`                 | The id of the app setting that holds the merchant's client id                                |
| `identity_provider.settings.client_secret`             | The id of the app setting that holds the merchant's client secret                            |

- **The merchant brings their own credentials.** They create a client with the provider and enter its id and
  secret in the app's settings. The secret is never sent to the shopper's browser.
- **One redirect address.** The app's setup steps show the single redirect address the merchant registers with
  the provider.
- **One button per app.** While the app is installed, enabled and both credentials are set, the storefront shows
  a sign-in button for it once the registration is confirmed, which takes up to a minute after a change. Switching
  the app off, uninstalling it or clearing a credential removes the button.
- **Themes.** The `SocialSignIn` part draws the buttons; its `providers` prop limits them by app id, such as
  `auth-google`.
- **The shopper's address.** A shopper whose provider vouches for the address signs in directly. If an account
  with that address exists, or the provider does not vouch for the address, a code is emailed to that address
  first. A provider that returns no address makes the storefront ask for one and prove it the same way.

## Advanced [#advanced]

Most apps never need these.

| Field                                         | Type and limits                                                                                       | Required                           | Default |
| --------------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---------------------------------- | ------- |
| `extensions`                                  | Up to 16 declarative JSON documents                                                                   | No                                 | None    |
| `extensions[].kind`                           | `declarative_admin_page` or `declarative_storefront_block`                                            | Yes                                | None    |
| `extensions[].target`                         | `admin.page` for admin pages; `storefront.home`, `storefront.product` or `storefront.cart` for blocks | Yes                                | None    |
| `extensions[].path`                           | Path of the JSON document in the project                                                              | Yes                                | None    |
| `extensions[].digest`                         | The document's SHA-256 as `sha256-<base64>`                                                           | Yes                                | None    |
| `extensions[].mediaType`                      | `application/vnd.poltura.extension.v1+json`                                                           | Yes                                | None    |
| `extensions[].element`                        | The `defineBlock` type of a storefront block document                                                 | For `declarative_storefront_block` | None    |
| `uiRuntime`                                   | An object, instead of `workers`                                                                       | No                                 | None    |
| `uiRuntime.version`                           | The number `1`                                                                                        | Yes                                | None    |
| `uiRuntime.workers`                           | 1 to 16 workers                                                                                       | Yes                                | None    |
| `uiRuntime.workers[].target`                  | One of the [UI targets](/apps/permissions#ui-targets)                                                 | Yes                                | None    |
| `uiRuntime.workers[].path`                    | Where the bundle goes in the release, ending in `.js`                                                 | Yes                                | None    |
| `uiRuntime.workers[].stylesheet`              | An object                                                                                             | No                                 | None    |
| `uiRuntime.workers[].stylesheet.path`         | Where the CSS goes in the release                                                                     | Yes                                | None    |
| `uiRuntime.workers[].stylesheet.inheritTheme` | `true` or `false`                                                                                     | No                                 | `true`  |
| `uiRuntime.workers[].element`                 | The `type` of a `defineBlock`                                                                         | For storefront block targets       | None    |
| `sources`                                     | A map from each `uiRuntime` path to the source file it is built from                                  | With `uiRuntime`                   | None    |

- **`extensions`** are declarative documents: JSON that describes a simple page or block, with no code. New apps
  use workers.
- **`uiRuntime` with `sources`** names the release paths yourself instead of letting `workers` derive them:

```json
"uiRuntime": {
  "version": 1,
  "workers": [{ "target": "storefront.home", "path": "workers/home.js", "element": "home-offer" }]
},
"sources": { "workers/home.js": "src/workers/storefront-home.tsx" },
"elements": "src/elements.ts"
```

## Release limits [#release-limits]

- 16 workers per release, each bundle at most 1 MiB, each stylesheet at most 256 KiB.
- The release manifest is at most 256 KiB, and a whole release upload at most 22 MiB.
- One release of an app can be in review at a time.

## A complete example [#a-complete-example]

The manifest of the [example app walkthrough](/apps/tutorial), with five workers, actions, webhooks and pages:

```json
{
  "schemaVersion": 1,
  "apiVersion": "v1",
  "handle": "recommendations",
  "releaseVersion": "1.0.0",
  "displayName": "Recommendations by Poltura Labs",
  "description": "Product recommendations for admin settings, product panels, storefront blocks, footer capture, and an app-owned page.",
  "translations": [
    {
      "locale": "hu-HU",
      "displayName": "Ajánlások a Poltura Labs-tól",
      "description": "Termékajánlók az adminhoz, a termékoldalhoz, a lábléchez és egy alkalmazásoldalhoz."
    }
  ],
  "supportUrl": "https://recommendations.apps.example.com/support",
  "privacyUrl": "https://recommendations.apps.example.com/privacy",
  "homepageUrl": "https://recommendations.apps.example.com/",
  "backendOrigin": "https://recommendations.apps.example.com",
  "redirectUrls": ["https://recommendations.apps.example.com/oauth/callback"],
  "permissions": {
    "required": [
      {
        "id": "settings.read",
        "purpose": "Read store locale, currency, and public capabilities"
      },
      {
        "id": "products.read",
        "purpose": "List products and collections used as recommendation sources"
      },
      {
        "id": "orders.read",
        "purpose": "Read new orders that refresh recommendation weights"
      }
    ],
    "optional": []
  },
  "webhooks": [
    {
      "topic": "order.created",
      "destination": "https://recommendations.apps.example.com/webhooks/orders"
    },
    {
      "topic": "product.updated",
      "destination": "https://recommendations.apps.example.com/webhooks/products"
    }
  ],
  "settings": [
    {
      "id": "highlight_mode",
      "type": "enum",
      "label": "Highlight mode",
      "options": ["quiet", "featured"],
      "default": "quiet"
    }
  ],
  "actions": [
    {
      "id": "save_featured",
      "purpose": "Save featured product ids for this installation",
      "surfaces": ["admin"]
    },
    {
      "id": "recommend",
      "purpose": "Return a product recommendation for the current shopper",
      "surfaces": ["storefront"]
    }
  ],
  "destinations": [
    {
      "id": "newsletter_capture",
      "url": "https://recommendations.apps.example.com/capture",
      "purpose": "Receive optional newsletter captures from the footer worker"
    }
  ],
  "adminPages": [
    {
      "id": "recommendations",
      "path": "workers/admin-page.js",
      "title": "Recommendations",
      "translations": [
        {
          "locale": "hu-HU",
          "title": "Ajánlások"
        }
      ],
      "icon": "star"
    }
  ],
  "storefrontPages": [
    {
      "id": "recs_page",
      "path": "workers/app-page.js",
      "title": "Recommendations",
      "slug": "recommendations",
      "translations": [
        {
          "locale": "hu-HU",
          "title": "Ajánlások"
        }
      ],
      "icon": "star"
    }
  ],
  "elements": "src/elements.ts",
  "workers": [
    {
      "target": "admin.page",
      "entry": "src/workers/admin-page.tsx",
      "stylesheet": "src/styles.css",
      "element": "recommendations-panel"
    },
    {
      "target": "admin.product",
      "entry": "src/workers/admin-product.tsx",
      "stylesheet": "src/styles.css"
    },
    {
      "target": "storefront.product",
      "entry": "src/workers/storefront-product.tsx",
      "stylesheet": "src/styles.css",
      "element": "recommendations"
    },
    {
      "target": "storefront.layout.footer",
      "entry": "src/workers/storefront-footer.tsx",
      "stylesheet": "src/styles.css",
      "element": "newsletter-capture"
    },
    {
      "target": "storefront.app-page",
      "entry": "src/workers/app-page.tsx",
      "stylesheet": "src/styles.css",
      "element": "recommendations-page"
    }
  ]
}
```

## Next [#next]

- [Authentication](/apps/auth): OAuth, session tokens and signed actions, which several fields above refer to.
- [UI](/apps/ui): what to put in a worker.
