# Building a SweetHive app — developer guide

> Drop this file into your app repo and build with Claude Code. It is the
> complete contract for a SweetHive custom app: the two app types, the embed
> handshake, auth, the data API, and how to test and publish.

You register your app once in SweetHive (**Developer → Apps → Register app**),
receive a `client_id` + `client_secret`, then a hive admin installs it into a
context. This guide is everything you need to build the app itself.

---

## 1. Two kinds of app

| | **Embedded** | **External** |
|---|---|---|
| Where it runs | inside the SweetHive context UI (a sandboxed `<iframe>`) | its own URL / domain, launched from SweetHive |
| Good for | data views, forms, dashboards scoped to a context | full products with their own UI (e.g. a site manager) |
| Integration | iframe + `postMessage` handshake (this guide, §3–5) | signed launch (§7) |

Both authenticate the same way: **a short-lived scoped token**, bounded server
side by `installScope ∩ the viewer's live visibility`. Your app never sees more
than the person using it can see, and access is revoked the instant they lose it.

---

## 2. Hard requirement: allow SweetHive to frame you

An embedded app **must** send this header (or it will load in a browser but be
blank inside the SweetHive mobile apps):

```
Content-Security-Policy: frame-ancestors https://sweethive.com https://app.sweethive.com capacitor://localhost http://localhost
```

- `https://sweethive.com` / `app.sweethive.com` — the web app.
- `capacitor://localhost` (iOS) and `http://localhost` (Android) — the mobile
  apps are Capacitor webviews served from those origins. Omit them and your app
  works on web but not on mobile.

Do **not** set `X-Frame-Options: DENY`. Serve over **HTTPS**. Don't rely on
third-party cookies (iOS WKWebView blocks them in cross-origin frames) — use the
token from the handshake instead.

---

## 3. The embed handshake (postMessage)

When SweetHive mounts your iframe it sends one message; you reply `app-ready`,
then use the token it gave you.

```js
// in your app
let sh = null; // { token, endpoint, context, hive, theme, capability, locale }

window.addEventListener('message', (e) => {
  // Trust only SweetHive origins.
  const ok = ['https://sweethive.com', 'https://app.sweethive.com',
              'capacitor://localhost', 'http://localhost'];
  if (!ok.includes(e.origin)) return;

  const msg = e.data;
  if (msg?.type === 'sweethive:init') {
    sh = msg.payload;          // { token, endpoint, context: {id,name}, hive, theme, capability, locale }
    applyTheme(sh.theme);      // 'light' | 'dark'
    boot();                    // start your app with sh.token + sh.context.id
  }
  if (msg?.type === 'sweethive:theme') applyTheme(msg.payload.theme);
});

// tell SweetHive you're mounted (it will then send sweethive:init)
parent.postMessage({ type: 'sweethive:app-ready' }, '*');
```

Messages **to** SweetHive you may send:

```js
parent.postMessage({ type: 'sweethive:resize', payload: { height } }, '*'); // auto-size the frame
parent.postMessage({ type: 'sweethive:navigate', payload: { contextId } }, '*'); // ask SH to open a context
parent.postMessage({ type: 'sweethive:toast', payload: { text } }, '*'); // show a SH toast
```

---

## 4. Auth

`sh.token` is a short-lived scoped token for the current viewer + install. Send
it as a Bearer credential to the App API (§5). Refresh: if a call returns `401`,
post `{ type: 'sweethive:refresh-token' }` to the parent; SweetHive replies with
`sweethive:init` carrying a fresh token.

Server-to-server (webhooks, background jobs) uses your `client_id` +
`client_secret` to mint an app token — never ship the secret to the browser.

---

## 5. The App Data API (scoped, token-authenticated)

Base URL is `sh.endpoint` (e.g. `https://sweethive.com/server/web`). Every call:
`Authorization: Bearer <sh.token>`. Every response includes `effectiveScope`.

| Call | Returns |
|---|---|
| `GET /app/context` | the install's context + your granted scope for this viewer |
| `GET /app/items?context=&limit=&since=` | messages/notes/files in scope |
| `GET /app/search?q=&context=` | scoped search |
| `POST /app/messages { context, body, groups[] }` | post into the context (needs `read_post` capability + user confirmation the first time) |

Reads are bounded to the viewer's visibility; writes to the install's context
with group targeting. Same law, same audit as any SweetHive actor.

---

## 6. A minimal embedded app

```html
<!doctype html>
<meta http-equiv="Content-Security-Policy"
      content="frame-ancestors https://sweethive.com capacitor://localhost http://localhost">
<div id="app">Loading…</div>
<script type="module">
  let sh;
  addEventListener('message', async (e) => {
    if (e.data?.type !== 'sweethive:init') return;
    sh = e.data.payload;
    const res = await fetch(`${sh.endpoint}/app/items?context=${sh.context.id}&limit=10`,
      { headers: { Authorization: `Bearer ${sh.token}` } });
    const { items } = await res.json();
    document.getElementById('app').textContent =
      `${sh.context.name}: ${items.length} recent items`;
    parent.postMessage({ type: 'sweethive:resize', payload: { height: document.body.scrollHeight } }, '*');
  });
  parent.postMessage({ type: 'sweethive:app-ready' }, '*');
</script>
```

---

## 7. External apps (own URL / domain)

For a full product on its own domain (e.g. the AerariumChain hub): register it
as `type: external` with a `base_url`. SweetHive shows a launch tile; on click it
opens your `base_url` with a signed launch:

```
<base_url>?sh_launch=<jwt>&context=<id>&hive=<id>
```

Verify `sh_launch` against SweetHive's JWKS (endpoint published in the dev
console), then start your own session. On mobile, SweetHive opens external apps
in the native in-app browser (SFSafariViewController / Chrome Custom Tabs), so
your normal cookie-based session works there.

---

## 8. Manifest (what you register)

```jsonc
{
  "name": "My App",
  "slug": "my-app",
  "type": "embedded",              // or "external"
  "base_url": "https://my-app.example.com/sweethive",
  "icon": "insights",              // material icon name, or an https image URL
  "description": "What it does, one line.",
  "capability": "read",            // read | read_draft | read_post
  "scopes": ["items", "search"]    // data surfaces you need
}
```

---

## 9. Test locally

1. Register your app with `base_url: http://localhost:5173` (dev), `type: embedded`.
2. Run your app locally with the `frame-ancestors` header above.
3. In SweetHive (or the mock), install it into a test context and open it — the
   handshake delivers a real scoped token against your data.
4. For mobile, run the SweetHive app on a simulator and confirm your app loads in
   the frame (this catches a missing `capacitor://localhost` in your CSP).

## 10. Publish

Set the app `status: active` in the dev console. Hive admins can then install it
from **Context → Apps → Add app**. Revoke or disable at any time; installs stop
immediately, and every install's token dies with it.
