> ## Documentation Index
> Fetch the complete documentation index at: https://docs.puntego.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Sub-apps in iframes

> Let the guide work inside parts of your product that load in an iframe from another of your domains: it reads them, points into them and, where you allow actions, acts in them after the visitor approves.

Parts of a product often load in an iframe served from another domain you own:
billing on `billing.example.com`, an editor on `editor.example.com`, a report
builder from a separate deployment. The browser keeps that frame's page closed to
your main page, so on its own the guide could neither read it nor point into it.

**Sub-apps in frames** closes that gap. Your main page keeps the guide as you
installed it; each sub-app adds a small child snippet. The child has no button,
panel or card of its own and holds no key: it answers the guide on your main
page, which stays the only thing your visitor sees and talks to.

> **Off until you allow an origin.** The guide never contacts an iframe unless an
> owner or admin added its exact origin under **Dashboard → Install → Sub-apps in
> frames** and the sub-app runs the child snippet. Frames from any other origin,
> and frames nested inside a sub-app, are ignored.

> **Release preview:** sub-apps in frames are implemented and tested in the
> repository and reach the hosted scripts with their next release.

<h2 id="what-works">
  What works inside a sub-app
</h2>

| What | Inside the frame |
| - | - |
| Answers | The guide reads the frame's page by the same rules as your main page and answers about it. |
| Pointing and rings | The cursor lands on the control inside the frame. If it is scrolled out of the frame's own view, the frame scrolls first, then your page. The ring follows when the frame scrolls or your page resizes. |
| Tours | A step can point at a control inside the frame; steps can mix your page and the frame. A step that waits for the visitor moves on when they use that control inside the frame (child snippet 1.2.0 or later). |
| Actions | **Only in a sub-app where you turned on Allow actions in this frame** (off by default). Pressing, filling a field, choosing an option and ticking a box, each one approved by the visitor on the guide's card on your main page. While the visitor is asked, the frame finishes its own scroll and then your page scrolls so the whole control shows above the guide's panel (never when the visitor asked the guide not to scroll). The frame checks the outcome on the live control and the receipt says what it verified. The card and the receipt name the sub-app by its own page title (or its `data-frame-name`), not your main page. |

Where actions are off, the guide never offers to act in the sub-app: it points at the
control and tells the visitor to do it themselves. Answers, pointing and tours work
the same either way.

While no sub-app of your workspace has actions on (the default), the guide knows it
can't act in any of them before it answers: it never mentions an action it would then
take back, and the visitor (or their screen reader) hears only that it pointed the
control out. Once you turn actions on for one sub-app, the guide may offer to act in
another one whose actions are off; that sub-app refuses, and the card switches to
the pointer at once.

A plan that fills several fields at once doesn't run inside a frame yet: the guide
says so instead of filling part of it.

<h2 id="setup">
  Set it up
</h2>

1. **Allow the sub-app's origin.** Open **Dashboard → Install → Sub-apps in
   frames** and add the exact origin, like `https://billing.example.com`: `https`,
   no path, on a domain you [verified](/domains) (or a subdomain of one). Up to 10.
   Adding one asks for your second factor, like adding a domain; removing one never
   does. If you remove that domain, or it stops being verified, the sub-app stops
   connecting at once. Every origin added or removed, and who did it, shows in
   **Settings → Audit log** under Workspace settings, like your other settings.

2. **Add the child snippet to the sub-app.** Paste it into every page that loads
   inside the frame:

   ```html theme={null}
   <script type="module" src="https://puntego.com/frame.js" data-puntego-frame
     data-parent-origin="https://www.example.com" data-frame-name="billing"
     crossorigin="anonymous" async></script>
   ```

   `data-parent-origin` lists the exact origins of the pages allowed to guide
   inside this sub-app (space separated): your main site. Without it the child
   stays silent. `data-frame-name` is optional; it names the frame in your targets
   (see below).

   The child accepts the guide only with a pass Puntego signs for each connection
   (see [Security](#security)), and it reads Puntego's public keys to check it. If
   the sub-app sends a strict Content Security Policy, allow
   `connect-src https://worker.puntego.com` (see [CSP](/csp#frames)).

   From a bundler, use the typed package instead:

   ```ts theme={null}
   import { startPuntegoFrame } from '@puntego/embed/frame';

   startPuntegoFrame({ parentOrigins: ['https://www.example.com'], name: 'billing' });
   ```

   `stopPuntegoFrame()` stops the child (for example when a component unmounts);
   calling `startPuntegoFrame` again starts it again.

3. **Open your page.** Once a visitor opens the guide, the sub-app connects and
   shows as **Connected** in Install → Sub-apps in frames, with the time and the
   child's version.

4. **Decide whether the guide may act there.** Each allowed sub-app has an **Allow
   actions in this frame** switch, off by default. Off, the guide reads, points and
   runs tours in the sub-app but never presses, types, chooses or submits anything
   there. Turn it on only for a sub-app whose embedding pages run scripts you trust
   (see [Security](#security)). Turning it on asks for your second factor; turning it
   off never does, and both show in **Settings → Audit log**. Sub-apps you allowed
   before this switch existed start with actions off: if you relied on the guide
   acting in one, turn it on for that sub-app.

<h2 id="targets">
  Name a control inside a frame
</h2>

The guide names an element inside a frame `frame:<name>:<id>`, where `<name>` is
the frame's `data-frame-name` and `<id>` is the element's `data-gp-id` inside the
sub-app. To declare a sub-app control in **Guides → Targets and actions**, give it a
stable `data-gp-id` in the sub-app and use the selector
`[data-gp-id="frame:billing:export-payouts"]`.

<h2 id="security">
  Security
</h2>

* **Both sides check each other.** The guide on your page talks only to iframes
  whose origin you allowed and opens each connection with a message posted to that
  exact origin. The child accepts it only from its direct parent, only when the
  parent's origin is one it lists in `data-parent-origin`. Neither side ever posts
  to `"*"`.
* **A pass for every connection.** Before the guide on your page connects to a
  frame, it asks Puntego for a pass that lasts a minute, signed for that frame,
  your page and that one connection. The child checks the signature with
  Puntego's public keys, and each frame page accepts a pass once. A script on
  your page that just sends the frame its own connection request, first or after
  the guide left, gets nothing, and its attempt doesn't stop the guide from
  connecting. No secret is in your pages or the sub-app's: Puntego keeps the
  signing key.
* **A private connection.** That first message hands the child a private
  channel, and everything after it travels there, carrying the fresh code of that
  connection. Nothing goes through your page's window, so other scripts on your
  page (another AI agent, a third-party tag) can't read the frame's content or the
  guide's requests, or send requests of their own into the guide's connection.
* **The child never drives your page.** It only answers and reports changes. Your
  page treats what it says as data about the frame, never as an instruction.
* **Actions only where you allow them.** Whether the guide may act inside a sub-app
  is part of the pass Puntego signs for each connection, from your **Allow actions in
  this frame** switch. Where it is off, the child refuses every press, typing, choice,
  submit and approval sent over that connection, whoever sent it, and the guide only
  points. Reading the frame for answers, pointing and tours keep working.
* **The visitor approves every action on your page.** The child acts only on a
  request your page first prepared with it and then confirmed after the visitor
  approved it on the guide's card. A request it did not prepare, an expired or
  already used one, or one for a control that changed since (or on a page the
  sub-app has since moved away from), is refused. So is one whose control now
  pays, signs or deletes when it didn't as the visitor was asked (the child checks
  again right before it acts). If the frame is hidden or covered on your page by
  the time the visitor approves, the guide doesn't act.
* **No secret crosses.** No visitor token, key or session is ever sent to the
  frame, and no field value comes back from it. The pass is signed, names one
  connection and expires within a minute.
* **One connection at a time.** While the guide's connection is open, the child
  refuses a second one. It ends when the visitor's consent ends, when the guide is
  turned off, or when either page navigates.
* **Frame it only from your site.** Have the sub-app send
  `Content-Security-Policy: frame-ancestors https://www.example.com` so no other
  site can frame it. The child trusts the pages you list in `data-parent-origin`.
  The pass proves that Puntego's servers issued the connection to a visitor
  session on your page, not which script on your page asked for it. A script on
  those pages that starts a Puntego visitor session of its own (with your public
  app id), or that tampers with the page before the guide loads, can connect to
  the sub-app as the guide does. **With actions on for that sub-app, such a script
  can ask it to act, without the visitor's approval: turning actions on means
  trusting every script on the pages that embed it.** With actions off, it can still
  read the sub-app's page by the same privacy rules the guide uses and point at its
  controls, but it can't make the sub-app press, type, choose or submit anything.
  That read access is the price of guiding inside the sub-app at all: the sub-app is
  your own, and what the child shares follows the [privacy rules](#privacy) below (no
  field values, personal data replaced). List only pages you control and keep a strict
  CSP on them.
* **Older snippets.** Child snippets from before the pass (version 1.0.x) accept
  any connection from your page, so they don't connect unless an owner or admin
  turns on **Let older sub-app snippets connect** in Install → Sub-apps in frames
  (it asks for your second factor, and shows in **Settings → Audit log**). They
  can't enforce **Allow actions in this frame** either: the guide never asks one to
  act while actions are off for it, but another script on your page could. The
  hosted `frame.js` is always current; update an older `@puntego/embed/frame` to
  version 1.2.0 or later, then turn the setting off.

<h2 id="privacy">
  Privacy
</h2>

The child reads the frame's page with exactly the rules of your main page: no field
values, contact and password fields named by their label only, payment and
one-time-code fields left out, anything under `data-puntego-ignore` left out, and
personal data replaced by placeholders before it leaves the frame. It reads nothing
until the visitor's consent lets the guide read the page, and stops when it ends.
On your main page, `data-puntego-ignore` on an iframe (or on anything around it)
keeps the guide away from that frame entirely: it doesn't connect to it.
Page vision screenshots never include a cross-origin frame. The connected-frames
list stores only the origin, the child's version, when it connected and whether it
was live or in test mode.

<h2 id="troubleshooting">
  Troubleshooting
</h2>

| What you see | Why, and what to do |
| - | - |
| The sub-app never shows as Connected | Check that its origin is allowed exactly (scheme, host and port), that the child snippet is on the page loaded in the iframe, that `data-parent-origin` lists your main page's origin, and that the sub-app's CSP allows `connect-src https://worker.puntego.com`. `window.PuntegoFrame.inactiveReason` in the frame's console says why the child is silent. |
| Install says "Older snippet" next to a sub-app | Its snippet predates the pass for every connection. Update `@puntego/embed/frame` (or use the hosted `frame.js`). Until then it connects only while **Let older sub-app snippets connect** is on. |
| "isn't on a domain you verified" when allowing | Verify the domain in [Domains](/domains) first, or use a subdomain of one you verified. |
| The guide points at the frame but not at the control | The control may be scrolled out of the frame and hidden behind something of your page; or the frame's page re-rendered under a new id. Give important controls a stable `data-gp-id`. |
| A plan to fill several fields refuses inside a frame | Not supported yet inside frames; the guide fills one field at a time there. |
| The guide points at a control in the sub-app and says to do it yourself | Actions are off for that sub-app. Turn on **Allow actions in this frame** next to it in Install → Sub-apps in frames if every script on the pages that embed it is yours. A page already open picks the change up when it reloads. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.