Skip to content

Side panel, home and settings pages

Markdown

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

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, once they have a session there: a page, a route of your own API, a file. In the page, apigami.js 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:

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), 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).

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):

  • 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 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). 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.

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

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

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

What the validation refuses
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.

What the validation warns about

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

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