# Side panel, home and settings pages

> The screens an application serves — a panel beside a site, a home page and a settings page — and what they may show to anyone.

Besides its actions, an application can serve screens of its own: a panel beside a site, a home page
and a settings page.

## Pages

> A page's server knows the person only by `x-apigami-user-ref`, which Apigami's session sets on every
> route of your app's own address: a visitor, the side panel and a page on your own site get none, and
> anyone can load a page, so it renders nothing personal without it. In the page, apigami.js holds the
> person's token, and `apigami.call()` fetches their data through an action. An action learns who it
> serves only from its run token: `GET /v1/me` answers `user_ref`, the person, the same in every browser
> of theirs. Key every per-person row on `user_ref`. Never pick the person from the input, from the most
> recent row, from `install_ref` or `browser_pseudonym`, which name a browser, or from a person a URL
> names. An OAuth callback reads the person in `x-apigami-user-ref`, and checks its `state` against a
> `__Host-` cookie its `/oauth/start` set.

Full page at your app's own address, every request a person's browser sends carries who they are, in
[`x-apigami-user-ref`](/docs/app-page-sessions/#who-is-behind-a-request-x-apigami-user-ref), once they have a session there:
a page, a route of your own API, a file. In the page,
[`apigami.js`](/docs/app-page-sessions/#how-a-page-knows-who-is-viewing-and-calls-its-actions) gets the person's token from
that session, and in the side panel from the panel, by message. A page opened by someone without a
session, from a bookmark or a shared address, is a visitor's until `apigami.connect()` or
`apigami.ready()` gives them one.

An application can show screens of its own: a panel beside a site, a settings screen, and a home.
Your code serves them like any other route, and `x-apigami.pages` declares them:

```json title="manifest.json, in x-apigami"
"domains": [{ "domain": "example.com", "via": "extension" }],
"pages": [
  {
    "kind": "side_panel",
    "id": "reply",
    "title": "Reply",
    "page_url": "/panel/reply",
    "show_only_on_domains": ["example.com"],
    "match": { "type": "regex", "value": "^https://www\\.example\\.com/messages/" }
  },
  { "kind": "settings", "id": "accounts", "title": "Mailboxes", "page_url": "/settings/accounts" },
  { "kind": "home", "id": "home", "title": "Inbox", "page_url": "/" }
]
```

| `kind` | Where it shows | When | Its own fields |
|---|---|---|---|
| `side_panel` | The browser's side panel, beside a site | When the tab's URL matches | `show_only_on_domains`, required; `match`, optional |
| `home` | Full page, right after the user connects through your connect link, and each time they open it again. Never in the side panel | Once connected | None |
| `settings` | Full page from your connect link when you declare no `home`, else from the « Settings » button of your home; then a last tab of your side panel, beside a site one of your panels matches | Once connected; then when the user opens that tab | None |

An application has **at most one `settings` page and one `home` page**. A page's `id` is unique across the application
and keeps the page's identity from one version to the next.

**`page_url` is relative, or your own site.** Your application lives at the root of its own host. A
path such as `/panel/reply` resolves to `https://{slug}.apigami.tech/panel/reply` for the version
in production, and to `https://{slug}--staging.apigami.tech/panel/reply` for the version your latest
deployment put there; once that version is sent to review or refused, `--staging` goes back to the newest
version still deployed. Once a domain of yours is active, production's pages open on it instead, as
`https://app.example.com/panel/reply` ([Your own address](/docs/app-domain/#your-own-address-a-custom-hostname)), and `--staging` stays on
Apigami's. It resolves to nothing until your first deploy
has reserved the slug, and a path may not leave your host. An absolute `https://` URL points at a site
of your own. `app_deploy`, `app_version` and `app_status` give the URL of each page.

Both addresses are known before your first deploy: every version's answer carries `address`,
`{ production, staging }`, built from the slug of its manifest, so you can register OAuth redirect URIs
and JavaScript origins ahead. A new slug moves both once published, and the old ones answer `404`
([The slug](/docs/app-manifest/#the-slug-its-rules-and-what-a-rename-does-no-redirect)).

**Your application's address hands every request to your code**, whatever the method and the path, with no
Apigami token: an action is called on `POST https://api.eu.apigami.com/v1/apps/{app_id}/actions/{action}`,
with the page's token, which `apigami.call()` sends for you. An action is therefore reachable at your address by
anyone: it must still check its token. Its pages are served there whatever the application's
visibility, `private` included: without `x-apigami-user-ref` they must render nothing personal. The
`--staging` address is public but unlisted, like a preview link: anyone who has it reads the pages and files
of your unpublished version, so they must reveal nothing either. Only a member of your group gets a session
there, its pages calling that version's actions; a visitor's call to an open action runs production's version,
so it never tries the new one: `app_run` with `version` does. A token is accepted only from its page's origin.
In the side panel, a page id that production also declares shows production's page. A page that only an
unpublished version declares shows in the side panel of your group's browsers.

**What your code receives, and what it answers, are its own.** Your address is served by your code, with only
Apigami's session in front of it ([Who is behind a request](/docs/app-page-sessions/#who-is-behind-a-request-x-apigami-user-ref)):

- `request.url` carries your public address, `https://acme-basket.apigami.tech/…` or its `--staging`
  host, for a request a browser sent there: build your own URLs from `new URL(request.url).origin` there.
  An action is called by Apigami on your version's internal address instead: never build a public URL,
  such as an OAuth `redirect_uri`, from `request.url` in an action.
- Apigami vouches for no header naming the client's address, `X-Forwarded-For`, `Forwarded` or `X-Real-IP`:
  never decide what a request may do from one.
- Every cookie reaches your code, except Apigami's own (`__Host-apigami_session`, `__Host-apigami_start_…`),
  which it takes out. Apigami removes `Domain` from every `Set-Cookie` you send, so yours stay on your host.
  Do not trust a cookie you did not set: another application's page can still set one for all of `apigami.tech`
  from its script.
- Apigami adds to your answers only what its session needs: a `Set-Cookie` when it renews or ends it, and
  `Cache-Control: private` when the session named someone and you set none (`private, no-store` with that
  `Set-Cookie`). Set these on your pages: `X-Content-Type-Options: nosniff`,
  `Referrer-Policy: no-referrer`, `Cross-Origin-Opener-Policy: same-origin-allow-popups` (a popup you open
  can still answer you), and `X-Robots-Tag: noindex` unless search engines should index the page.
- The side panel shows your page in a frame. `X-Frame-Options: SAMEORIGIN` or `DENY`, which some frameworks
  set by default, leaves the panel blank; a `frame-ancestors` of your own must allow
  `https://app.apigami.com` and `chrome-extension:`.

**A side panel shows only on sites your application reaches through the browser.** Every domain in
`show_only_on_domains` must be declared in `x-apigami.domains` with `"via": "extension"`. `match`
narrows it further, to an exact URL (`{ "type": "url" }`) or a regular expression
(`{ "type": "regex" }`) that stays on those domains. Without `match`, the panel shows on every page
of the domains. A `settings` or `home` page is about the whole application, so it takes neither field.

**A side panel shows only once its domain is approved.** The user must have approved that domain
for your application, and the browser must hold Chrome's permission for it. Until then the panel
stays hidden, and Apigami opens the approval window for that domain on its own.

### The home page

The `home` page is your application itself, the page your connect link opens: a game's podium, a
dashboard, an inbox. Right after a user connects through `/connect?app_id=…`, and each time they open
that link again, Apigami opens it full page in their tab, at its bare address, where their session names
them: `apigami.js`, `apigami.ready()` with `page_kind: "home"`, `apigami.call()` and the browser the user
just connected all work. It never shows in the side panel.

The home is also where a person sets your application up: its own options, and the settings of the
applications it calls ([Show the settings of the bricks you call](/docs/app-dependencies/#show-the-settings-of-the-bricks-you-call)).
A `settings` page is optional. When the version serving the home also declares one, `apigami.js` adds a
« Settings » button at the bottom left of the home, with no code of yours, which opens it full page
(`/connect?app_id=…&view=settings`). Keep what matters in your actions, not in the page.

**Make pages feel instant.** Render the page at once with a loading state, then fill it from
`apigami.call()`. Cache what a page shows for about a minute, in your app's database, keyed
by `user_ref`, so that coming back to it costs no new computation. Call slow third-party services after
the first paint, never before it.

**A public page is cached an hour in production.** On your production address, and on your own domain, a page
a visitor loads (`GET`, `200`, `text/html`) that sets neither `Cache-Control` nor a cookie is kept an hour by
Apigami, which adds `Cache-Control: public, max-age=3600` and `Vary: Cookie`. A person signed in
carries their session cookie, so is never served that copy, and the `--staging` address is never cached. The
copy belongs to one version: a publication or a rollback reaches a new visitor within about a minute, but a
visitor's browser that already loaded the page may keep showing the previous version for up to that hour,
since the same header reaches it, until they reload. A page whose content
changes without a new version, a list of offers read from your database, or that carries a value per visitor,
such as a CSRF token, sets its own: `Cache-Control: no-store`, or `max-age=60`. `app_deploy` names the pages
production will keep.

### The settings page

Your `settings` page opens full page from the « Settings » button of your home, and right after a user
connects through your connect link when you declare no `home`: it is then where the connection lands, and
nothing sends the user back. The same page is the last tab of your side panel.

**Give people one link: your connect link.** `https://app.apigami.com/connect?app_id={your app_id}`
connects them, then opens your home page, else your settings page, at once each time they open it again:
give them that link to bookmark. A bookmark of any page of yours works too: it opens for anyone, and goes
through Apigami's connect page when the page asks for a person
([A visitor on your page](/docs/app-page-sessions/#a-visitor-on-your-page-apigamisession-and-apigamiconnect)). The link opens the
page only if a version serving that user declares one when they arrive: your latest deployed version is
enough for your group; published, for anyone else. Opened before, it ends on « Connected », and they see
the page only on their next visit to the link. An application that connects people of its own, through its
`redirect_uri`, gets its user back on its own site instead ([Connect a user](/docs/connect/)).

**Opened full page, the settings page goes through `apigami.js` too**, as every page does: never read a
token yourself. `apigami.ready()` gives the user's `user_ref` and `page_kind`, `apigami.call()` calls your
actions, and the token stays fresh for as long as the person's session
([A page token's lifetime](/docs/app-page-sessions/#a-page-tokens-lifetime-5-or-15-minutes-renewed)). Your actions drive the
browser the user just connected, as they do from the side panel: `request.fetch` and the browser
operations work there, on the sites the user allowed. `apigami.state.url` is `null` there: no site is
beside the page. `apigami.state.install_ref` names that browser once `apigami.ready()` resolves, or is
`null` when the user connected without one.

```html title="settings/accounts"
<script src="https://api.eu.apigami.com/apigami.js"></script>
<script type="module">
  const { user_ref } = await apigami.ready()
  const { state } = await apigami.call('settingsStatus', {})
  render(state)
</script>
```

**A sign-in to another service is an ordinary redirect.** At your app's own address, leaving the page for
the service and coming back loses nothing: the session comes back with the person, and your
`/oauth/callback` reads them in `x-apigami-user-ref`
([Sign in to a third-party service](/docs/app-sign-in/#sign-in-to-a-third-party-service-from-your-app-oauth-pkce-callback)).
A page on your own site has no session: open the sign-in in a popup there, on a click, since a page that
leaves loses its token, which `apigami.js` says once, in the console and in `app_logs`.

**Keep what a page learns on your server**, through an action, keyed on `user_ref`, not in a cookie
or in the storage of the page. Your host is its own origin, but storage stays in one browser, and
production and `--staging` are two hosts, which a new slug changes. If you do set a cookie, every other
Apigami application counts as the same site: never rely on `SameSite`, prefer the `__Host-` prefix, and
keep every `GET` free of side effects. In the side panel your page is framed by the dashboard, so its
cookies are third-party there: they need `SameSite=None; Secure; Partitioned`.

At your application's own address, a relative `page_url`, a page opens at its bare address, with no Apigami
parameter. A page on your own site gets `apigami_run` and `apigami_app` in its query string, which its
server reads: keep that query string out of your logs, and serve the page with `Referrer-Policy: no-referrer`.

**The home or settings page of a version not yet published** opens full page at the `--staging` address
for a member of your group, when production declares no page of that kind. Try it by opening your
application's connect link. Its files load, and a reload keeps the page, as in production.

<details>
<summary>What the validation refuses</summary>

| Code | What to fix |
|---|---|
| `duplicate_settings_page` | Keep one `settings` page. |
| `duplicate_home_page` | Keep one `home` page. |
| `duplicate_page_id` | Give each page its own `id`. |
| `page_domain_not_declared` | Declare the domain of a side panel in `x-apigami.domains`. |
| `page_domain_not_extension` | Declare it with `"via": "extension"`: a `direct` domain has no browser to show a panel in. |
| `page_match_outside_domains` | Keep `match` on the domains the panel names. |
| `page_script_on_app_host` | A page with a relative `page_url` loads `/apigami.js` or `/__apigami/panel.js` from your app's own address, and no file loads it from the API: nothing serves either path there, so the page never gets `window.apigami`. Load `https://api.eu.apigami.com/apigami.js`. |

A `show_only_on_domains` missing on a side panel, or present on another kind, is refused by the
schema itself, and so is a `connect` page: declare a `settings` page, which the connection opens.

</details>

<details>
<summary>What the validation warns about</summary>

A warning comes with the validation, `"level": "warning"`, and blocks neither deploying nor
publishing: the version stays `valid`. It is a guess made from your files, and it says why it
guessed.

| Code | What it saw |
|---|---|
| `page_without_panel_script` | A page with a relative `page_url`, and no file outside the manifest and the `.md` files that mentions `/apigami.js`. A side panel that writes `APIGAMI_PANEL_READY` by hand gets a gentler word; a `settings` page is reminded that it also opens full page. A page on your own site is not checked, nor a `home` page of a public site: no `settings` page, and no file calling `apigami.call`, `apigami.ready`, `apigami.session`, `apigami.connect` or `apigami.open`. |
| `page_script_relative` | A page with a relative `page_url`, and every file that mentions `apigami.js` does it by a relative path other than the root one, `/static/apigami.js` or a path joined to a variable: that request never reaches Apigami's script, so the page gets no token. Load `https://api.eu.apigami.com/apigami.js`. |
| `per_person_data_without_user_ref` | Pages, a database read through `env.DB` or `DATABASE_URL`, and no file that mentions `user_ref`: every person would read the same rows. |
| `database_not_declared` | A file reads `env.DB`, `DATABASE_URL` or `DIRECT_URL`, and the manifest declares no database: an application that never had a database gets none. |
| `database_kind_mismatch` | The code reads only the other kind of database than the one the manifest declares. |

A deployment also reads the HTML each page answered and says, in its answer only, which page loads
no `apigami.js`, or loads it by a relative path: a page your code writes at run time is invisible to the
validation. A `home` page whose HTML runs no script, with no `settings` page, is not asked for it. The
deployment then asks, once each, up to 20 pages of your app's own address that those pages link to, and
lists what they answered: a broken link is only reported, a page serving an Apigami key or token blocks
publication like a declared one. It also asks up to 40 of the stylesheets, scripts, images and media those
pages load from your address, and names the ones that fail: reported, never blocking. A page whose HTML loads it from `/apigami.js` on your app's own address blocks publication
(`page_failed`).

</details>

### See your side panel

An assistant cannot open the real side panel: Chrome wants your click on the Apigami icon in its toolbar
([The tab beside the side panel](/docs/app-page-sessions/#the-tab-beside-the-side-panel)). It calls
`app_page_preview` instead, which opens the same panel, beside a page of the site, in a small window of
your browser, and answers a `tab_id` with what the panel saw of the page in its first seconds. On that
`tab_id`, without a domain, the usual tools work: `page_read` reads the page, `page_screenshot`
photographs it, `console_list` reads its console, `page_click` and `page_type` act in it,
`tabs_navigate` reloads it and `tabs_close` closes it. Its storage, cookies and network stay out of
reach.

It shows the version the panel would serve you, production's page or else the latest version's, and
needs Apigami Dev Assistant 0.15.0 or later. Beside a site it needs your account connected and the domain
allowed; otherwise, for a home page, or for a settings page with no side panel page, it opens the
page full page, as your connect link does, from Dev Assistant 0.15.1. Unconnected, the preview connects you, as your connect link does. A page whose
`page_url` is outside Apigami opens full page only through your connect link.

One preview per page: opening the same page again closes the previous tab, and its `tab_id` then
points to the new one. While a test waits on a tab, preview other pages only.