# App-faced Spaces A Space can **wear an app**: the full-screen surface is your app site, and the private room rides over it as a pull-up card — the input line sits exactly where a normal Space's does. The agent's tools execute against the app pane over an origin-pinned bridge, and the card's grab bar shows **exactly the structured fields that crossed** to your site — nothing else ever does. The reference is the Event Finder concierge: a Space whose "chat history area" is a live commercial events site. > **Status:** the app-site mode and the wire contract below are **live** (the Event Finder demo runs on them). > The sign-in patterns in §3 are **documented designs** — the recommended shapes, not yet shipped primitives. ## 1. Declaring an app site A branded deployment declares its app in the brand config: ```json { "agentName": "Concierge", "persona": "…", "appSite": "https://your-app.example", "tools": [ … ] } ``` Opening a Space on that origin renders `appSite` full-screen with the room as the card. Every Space on the origin gets the app face, the brand's tools, and per-room **resume**: the last search is stored in the room's own sealed state doc, so returning to a room (any device, any member) replays it. Three gates for a new app-site origin, all fail-closed: 1. the Spaces app's CSP must allow framing it (`SPACE_VAULT_APP_ORIGINS` at deploy); 2. the app site's `frame-ancestors` must list the **whole ancestor chain** (the Spaces origin, and the vault origin if the app runs vaulted — browsers check every ancestor); 3. the app site should **origin-pin inbound messages** to the Spaces origin(s). ## 2. The wire contract The pane speaks two messages down, one up — all `postMessage`, origin-pinned both ways: | direction | message | meaning | |---|---|---| | Space → pane | `{ type: "search", args }` | run a query; `args` are exactly the disclosure-strip fields | | Space → pane | `{ type: "focus", index }` | highlight the Nth already-shown result | | pane → Space | `{ type: "results", events: [...] }` | the public results — returned **into the agent's turn**, so it recommends from live data | The pane sees **only** these messages. It never receives the conversation, names, or reasons — the same-origin policy walls it off, and the bridge carries nothing but the structured arguments the strip displays. ## 3. Signing into the app pane Three patterns, in ascending capability. A key browser fact shapes all of them: the pane is an iframe, so it **cannot see your normal first-party login cookies** (third-party cookies are blocked or partitioned), and most authenticated sites refuse framing entirely — an arbitrary site can't be pointed at and inherit your session. A **cooperating** app site, however, has clean options. ### 3a. No sign-in (public data) — *what the demo ships* The pane serves public data keyed by nothing. Simplest, fully anonymous toward the site: it learns only the strip's fields per query. Right default for discovery-type apps. ### 3b. Personal account (per member) The standard embedded-app pattern (the same shape Spaces' own central auth broker uses): 1. the pane opens its **login in a popup** — top-level, so cookies are first-party and Google/password/passkeys work normally; 2. the popup hands the session back to the pane, which keeps it in **partitioned storage** (a `Partitioned` cookie or a token in the iframe's storage). Partitioned storage is stable per embedding origin, so you sign in **once per device** and stay signed in across rooms and reloads. What it buys: account prices, saved payment, a "buy" that completes. **Disclosure honesty:** the site can now join your queries to your identity — the strip should say so (`shared → … · as you@example.com`). Each member is signed into *their own* account, so panes may show member-specific views, and tool results come from whoever asked. ### 3c. Shared room account (a "party" account) — *the room-native pattern* If all members use **one stateful account**, the site's own backend becomes the room's shared surface: one cart, one favorites list, one booking — every member's pane converges by definition, with no extra sync protocol. The elegant part is **how the account is shared**: never a password. The site issues a **scoped session token** (a party token), and the token rides **sealed inside the room** — in the same encrypted per-room state doc that already carries the resume search: - **Invite to the room = access to the account.** A new member's pane receives the token over the bridge and is signed in with zero ceremony. The room key already gates who's in; the site token inherits exactly that boundary. - **The platform never sees it** — the token lives in the mk-sealed doc, end-to-end encrypted like the chat. - **Revocation = rotate the token** when membership changes (the same epoch model a membership change already implies). - The site sees one identity doing minimal structured queries; the strip shows `as `. Caveats: one identity for N people (right for a family or a trip crew, wrong where per-person accountability matters); concurrent edits are the site's job (carts handle this well); token rotation must accompany member removal, or a removed member's pane keeps working until expiry. ### Choosing | | site learns | shared state | best for | |---|---|---|---| | **3a** public | queries only | none (per-pane) | discovery, demos | | **3b** personal | queries + who you are | per member | purchases, personal accounts | | **3c** party token | queries + one shared identity | **the site's own state, room-wide** | planning together: shared carts, group bookings | ## Next - [The Concierge — a real app, end to end](./concierge-app.md) — the reference app-faced Space. - [Embed](./embed.md) — the host-page bridge this contract descends from. - [Trust model](./trust-model.md) — where the disclosure boundary sits.