Embed the portal

The whole customer portal inside your app in one component: token handling, theming, and viewport-scale drawers and dialogs.

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

PropTypeNotes
urlstringThe portal session url. Required.
getPortalToken() => Promise<string>Enables tokenless embedding and silent refresh. Strongly recommended.
surfacePortalSurfaceRenders one portal area with no header and no tabs. See bare surfaces. Omit to embed the whole portal.
darkModebooleanPins 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.
overlaysbooleanLets drawers and dialogs cover your viewport. Default true.
overlayZIndexnumberStacking order for the overlay frame. Default 2147483000.
onNavigationIntent(intent) => voidCalled when a surface frame suppresses a navigation that would have left it, with the surface it was headed for.
className / styleApplied to the container. It fills its parent, so give that parent a height.
loadingFallbackReactNodeRendered 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.

Continue