---
title: "Bare surfaces"
description: "Embed one portal area at a time (endpoints, event catalog, logs, or the dead-letter queue) with no portal header and no tabs, inside your own pages."
---

Pass `surface` and the frame renders exactly one portal area inside your own
page, with no portal header and no portal tabs. Your navigation takes the place
of the portal's tabs: one page per area, each reading as your app's own content.
This is for teams that want their own page structure with none of the UI work:
their layout, our surfaces, iframe isolation.

```tsx
import { RepostPortal } from "@repost/portal-react";

<RepostPortal url={portalUrl} getPortalToken={getToken} surface="logs" />
```

## The four surfaces

| `surface` | What the customer gets |
| --- | --- |
| `"endpoints"` | The portal's endpoint area: list, detail and create, including subscriptions and signing secrets. |
| `"event-catalog"` | The event catalog: every subscribable event type, with schemas and sample payloads. |
| `"logs"` | The delivery log: every attempt, newest first, streaming live, with the delivery detail drawer and replay. |
| `"dead-letter-queue"` | The dead letter queue: deliveries that exhausted their retries, with replay. |

Everything the [full embed](/docs/send/portal-embed) does, a surface does: the
token flow over the private port, theming without reloads, and drawers and
dialogs that cover your viewport rather than being clipped to the frame. The
session comes from the same `portal-access` call; the props are the same
component's props.

## One page per area

The intended shape is one surface per page, with your own navigation linking
them:

```text
/webhooks/endpoints     <RepostPortal surface="endpoints" ... />
/webhooks/catalog       <RepostPortal surface="event-catalog" ... />
/webhooks/logs          <RepostPortal surface="logs" ... />
/webhooks/dlq           <RepostPortal surface="dead-letter-queue" ... />
```

The pages differ by nothing but the `surface` name. Mint one session per
customer server-side and share it across the pages: sessions outlive a
customer's hop between them, so there is no need to pay a platform round-trip
per navigation.

Several surfaces can also share one page. Each is its own frame with its own
session, and at most one drawer is open across all of them.

## Surfaces stay put

A bare frame never renders another surface. It moves freely inside its own:
`surface="endpoints"` still goes from the list to an endpoint and to the create
form. But a navigation that would leave it is suppressed rather than performed.
The customer stays where they are, and the frame tells you where it was headed:

```tsx
<RepostPortal
  url={portalUrl}
  getPortalToken={getToken}
  surface="logs"
  onNavigationIntent={(intent) => router.push(`/webhooks/${intent.surface}`)}
/>
```

`intent.surface` is one of the four names, so it maps onto your own routes
directly. `intent.path` is a portal path and stays opaque. Nothing in the portal
links out of a surface today, so the callback is optional: the lock is a
guarantee about what a frame can render, not a workflow you have to handle.

Swapping `surface` on a mounted component reloads the frame, because it is a
different page. A theme change does not.

## Outside React

[`createPortalEmbed`](/docs/ui/portal-js#embedding) in `@repost/portal-js` takes
the same `surface` and `onNavigationIntent` options, so Vue, Svelte and vanilla
hosts get the same behavior.

## Older portal versions

A portal deployment that predates surfaces renders the requested area with its
chrome, functional, just not bare. Portal deploys precede SDK releases, so in
practice a released SDK never meets such a portal.

## Continue

<CardGroup cols={2}>
  <Card title="Full embed" icon="app-window" href="/docs/send/portal-embed">
    The whole portal in one component, when one page is enough.
  </Card>
  <Card title="Build your own" icon="component" href="/docs/ui">
    The same data as installable blocks, components and hooks. You own the UI.
  </Card>
</CardGroup>
