The embed is the zero-custom-UI path. You render one component, your backend mints a token, and your customers get the full portal inside your own app: deliveries, endpoints, event catalog, replay.
import { RepostPortal } from "@repost/portal-react";
<RepostPortal url={portalUrl} getPortalToken={getToken} />;url comes from POST /v1/customers/{id}/portal-access, the same
portal-access call that powers hosted links. getPortalToken
returns a fresh token from your backend whenever the portal needs one. See
token endpoint. Provide it and the token never
touches the URL: it travels over a private MessageChannel and is refreshed
silently, ahead of expiry and again on any auth failure.
Not using React? createPortalEmbed in
@repost/portal-js is the same thing without the wrapper.
Want one portal area instead of the whole portal, with your navigation around our surfaces? That is bare surfaces, and it is the same component with one more prop.
Props#
| Prop | Type | Notes |
|---|---|---|
url | string | The portal session url. Required. |
getPortalToken | () => Promise<string> | Enables tokenless embedding and silent refresh. Strongly recommended. |
surface | PortalSurface | Renders one portal area with no header and no tabs. See bare surfaces. Omit to embed the whole portal. |
darkMode | boolean | Pins the portal's color scheme to your app's. Omit to follow the viewer's OS preference. Seeded pre-paint so the first render never flashes the wrong theme, then applied live: toggling your theme does not reload the portal or lose the customer's place. |
overlays | boolean | Lets drawers and dialogs cover your viewport. Default true. |
overlayZIndex | number | Stacking order for the overlay frame. Default 2147483000. |
onNavigationIntent | (intent) => void | Called when a surface frame suppresses a navigation that would have left it, with the surface it was headed for. |
className / style | — | Applied to the container. It fills its parent, so give that parent a height. |
loadingFallback | ReactNode | Rendered until the portal frame loads. |
Theming#
Pass darkMode and the portal matches your app. Omit it and the portal follows
the viewer's OS preference, tracking changes to it live.
<RepostPortal url={portalUrl} getPortalToken={getToken} darkMode={theme === "dark"} />The scheme is seeded into the frame URL for the first paint, so the portal never renders light for a frame and then flips. After that it travels over the private port: toggling your theme repaints the portal in place and does not reload it, so a customer reading a delivery keeps their scroll position, filters and open drawer.
The frame survives a theme change. It does not survive a reload of your page, but the customer's place does: the portal returns them to the tab and filters they were on, for the rest of the browser session. Closing the tab forgets it, deliberately. Bare surfaces never take part, since your own page decides where they land.
Drawers and dialogs#
Anything inside an iframe is clipped to it, including position: fixed. A
delivery drawer would be a panel inside a box rather than a drawer over your
app.
So the SDK mounts a second, hidden frame on document.body and the portal
renders its modal surfaces there: full viewport, with a backdrop over your
page, focus moved in and returned on close, and your page's scroll locked while
it is open. Nothing is required of you: it is on by default, there is no second
component to render and no container to provide.
The frame lives on document.body on purpose. Any ancestor with a transform,
filter, backdrop-filter, perspective, will-change, contain or
content-visibility becomes the containing block for position: fixed
descendants, which would clip a full-viewport overlay to that element. Hanging
off body means there are no such ancestors.
Turn it off if you would rather keep everything inside the inline frame:
<RepostPortal url={portalUrl} getPortalToken={getToken} overlays={false} />Raise overlayZIndex if your app has fixed furniture that would otherwise paint
over the portal's modals.
Overlays need getPortalToken: the overlay frame authenticates its own
session. A url-only embed keeps its modals inline.
Older SDK versions#
The embed protocol negotiates on connect. An SDK that predates overlays advertises nothing, and the portal keeps rendering modals inside the inline frame, indefinitely, with no error and no degraded behavior. Upgrading is a version bump with no code change.
The same negotiation means new portal surfaces never require an SDK release: the host is told where to render, never what, so it needs no knowledge of the portal's screens.
Origins and Content-Security-Policy#
The portal answers with frame-ancestors built from the allow-listed origins in
Settings → Portal for that environment (up to 20). With no origins listed,
the embed renders anywhere; once you list any, your app's origin must be among
them. Pair an embedded portal with short-lived or read-only links minted per
page view.
On your side, if your page sets frame-src (or child-src), it must allow the
portal origin, https://portal.repost.sh by default. The overlay frame is the
same origin as the inline one, so a policy that already works needs no change.
Troubleshooting#
The overlay doesn't cover my page. Something in your layout is establishing a
containing block for fixed positioning. On <body> itself the SDK warns in
development with the property it found; move that style onto a child of body.
My own modal paints over the portal's. Raise overlayZIndex above it.
Modals still open inside the frame. Either overlays is false,
getPortalToken is missing, or the installed @repost/portal-react predates
overlay support. All three are the documented fallback, not a failure.
The frame doesn't load at all. Check your frame-src policy and the
allow-listed origins in Settings → Portal.