# Hosting on Storeware

> Run your app on Storeware's edge, free, at its own address, with no server of your own. In beta.

Source: https://storeware.ai/docs/build-apps/hosting

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

**In beta.** Hosting is free while it is in beta. The limits below may change before it leaves beta, and we will say so in the [changelog](https://storeware.ai/docs/changelog) first.

An app does not need a server of its own. Storeware can run it for you, on its edge, close to whoever calls it. Your app lives at its own address:

```text
https://<your slug>.storewareapps.com
```

Every call Storeware makes to an app reaches a hosted app exactly as it reaches one on your own server: webhooks, privacy requests, your home in the admin with its `?session=`, storefront blocks and sections, the app proxy, delivery rates, automation actions and tools. Each is signed the same way, and your code verifies it with the same SDK.

## Start one

```bash
npx @storeware/cli init --template hosted
```

This makes a folder with `storeware.app.toml`, `src/worker.ts` and a `package.json`. Every address in the manifest is already on your app's own name. To send it:

```bash
npx @storeware/cli app deploy
```

Deploy bundles `src/worker.ts` into one file, uploads it, and sends the manifest as a new release. A change of code is a new release, checked like any other: shops that have your app take it straight away when it reaches no further than before, and are asked when it does.

## [runtime]

A `[runtime]` table in the manifest is what makes an app hosted.

```toml title="storeware.app.toml"
[app]
slug = "sizing"
url = "https://sizing.storewareapps.com/"

[webhooks]
url = "https://sizing.storewareapps.com/webhooks"
events = ["product.created.v1"]

[runtime]
main = "src/worker.ts"
egress = ["api.sizes.example"]
```

| Key      | Meaning                                                                                                           |
| -------- | ----------------------------------------------------------------------------------------------------------------- |
| `main`   | The file deploy bundles. `src/worker.ts` when not given.                                                          |
| `egress` | Hosts your code may call besides Storeware's API, at most 20. Each is shown to the merchant who installs the app. |
| `bundle` | Written by deploy into the copy it sends. Never write it yourself.                                                |

Only your own address may appear on `storewareapps.com`, and only in an app with `[runtime]`.

## Your code

`src/worker.ts` exports a `fetch(request, env)` handler, like a Cloudflare Worker. Build it with `createHostedApp`, which gives you the same object as `createStorewareApp`:

```ts title="src/worker.ts"
import { createHostedApp, type HostedEnv } from '@storeware/plugin-sdk';

export default {
  async fetch(request: Request, env: HostedEnv): Promise<Response> {
    const storeware = createHostedApp(env);
    storeware.webhooks.on('product.created.v1', async (event) => {
      const client = await storeware.clientFor(event.storeId!);
      // Call the API for this shop.
    });

    const url = new URL(request.url);
    if (url.pathname === '/webhooks') return storeware.webhooks.handle(request);
    if (url.pathname === '/') {
      const { claims } = await storeware.authenticateAdmin(request);
      return new Response(`Hello, ${claims.store_id}`);
    }
    return new Response('Not found', { status: 404 });
  },
};
```

What is different from an app on your own server:

- **No client secret and no installation store.** Storeware already holds every installation of your app and hands over the one a request is for.
- **Installs need no grant.** `app.installed.v1` still arrives, but there is nothing to trade.
- **Tokens are never refreshed.** `clientFor` uses a live token from Storeware and asks for a new one when it is about to run out.

The same app can move between hosted and your own server by swapping `createHostedApp(env)` for `createStorewareApp({ ... })`, and back. The handlers you mount do not change.

## env.STOREWARE

`env` holds four things and nothing else:

| Name                   | What it is                                                                      |
| ---------------------- | ------------------------------------------------------------------------------- |
| `STOREWARE`            | Your app's installations and its data. It only ever answers about your own app. |
| `STOREWARE_API_URL`    | `https://api.storeware.ai`                                                      |
| `STOREWARE_APP`        | Your slug.                                                                      |
| `STOREWARE_APP_ORIGIN` | `https://<your slug>.storewareapps.com`                                         |

`env.STOREWARE.installation({ storeId })` or `({ installationId })` returns an installation: its shop, its scopes, its status, its two secrets and a fresh access token. You rarely need it directly, since `createHostedApp` calls it for you.

Your code only sees the shops that run its own release. A shop that has not yet approved a new release keeps the older one, and the new code gets `null` for it, as if it were not installed. Requests that name no shop run the release merchants were last given. If a shop asks for too many tokens in one minute, the installation comes back with `accessToken: null` for a moment; its secrets are still there.

### App data

Each installation has a small store of its own, for settings, a cursor, or what a shop chose. Use `appData` for typed reads and writes:

```ts
import { appData } from '@storeware/plugin-sdk';

const data = appData(env, installation.installationId);
await data.put('settings', { unit: 'cm' });
const settings = await data.get<{ unit: string }>('settings');
const page = await data.list({ prefix: 'size:' });
```

Values are kept as JSON. Per installation:

| Limit               | Value          |
| ------------------- | -------------- |
| A key               | 200 characters |
| A value, as JSON    | 64 KB          |
| Keys                | 1,000          |
| All values together | 5 MB           |
| A page of `list`    | 100 entries    |

A write past a limit throws, with a message that says which.

## What your code may reach

Your code may call Storeware's API and the hosts in `egress`, over https, and nothing else:

- Only `GET`, `HEAD`, `POST`, `PUT`, `PATCH` and `DELETE`.
- Your headers are passed on, `Authorization` included, except cookies and headers that say where a request came from.
- A redirect is handed back to your code, not followed. If you follow it, the new address is checked again.
- A request body may be up to 1 MB, and an answer up to 5 MB.
- No hosted app may call another, at any address on `storewareapps.com`.

Cookies your app sets are kept to its own address: Storeware drops any `Domain` from them, so no other app can read them.

A refused call answers 403 with the reason. Adding a host is a new release that each merchant approves.

## Limits

Each call to your app may use:

| Limit                   | Value      |
| ----------------------- | ---------- |
| CPU time                | 100 ms     |
| Calls out (`fetch`)     | 50         |
| Time to start answering | 30 seconds |

A call past the CPU or `fetch` limit stops and answers 500. Waiting on the network does not count as CPU time.

Each installation also has a daily allowance of calls and CPU time, and so does your app for calls that name no installation. Past it, calls answer 429 with `Retry-After` until midnight UTC. Storeware's own signed calls and merchants signed in to your app in the admin are never refused for the allowance, so strangers cannot use it up and shut them out.

One address may call your app 600 times a minute. Past that, it gets 429 for the rest of the minute. Storeware's own signed calls are not counted.

## Logs

```bash
npx @storeware/cli app logs --console --follow
```

Shows what your code logged with `console.log`, its errors, and each call that stopped early. A call keeps its first 50 lines, each up to 2,000 characters.
