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

# Headless portal

> Build a support portal inside your own product using the Plain API, so customers read and reply to their threads without leaving your UI.

A headless portal lets your customers read, create, and reply to their threads inside your own product. You build the interface; Plain stores the threads, routes them to your team, and gives you the API to read and write them.

Because you own the markup, the portal matches your product rather than looking like an embedded widget, and customers stay signed in with your existing session instead of a separate support login. Threads created this way arrive with the tenant, priority, and labels already set, so they land in your queue ready to triage.

<Frame>
  <img src="https://mintcdn.com/plain/esnLSCL6AbjPxXFS/public/images/headless-portal.png?fit=max&auto=format&n=esnLSCL6AbjPxXFS&q=85&s=d3fb437a3f081c019950ba03a36772de" alt="Headless portal in Plain" width="2280" height="1200" data-path="public/images/headless-portal.png" />
</Frame>

Building one is four pieces of work:

1. Create a tenant for each of your customers.
2. Fetch that tenant's threads to list them.
3. Let customers open new threads.
4. Let customers reply to a thread.

Read the [data model](/docs/graphql/introduction) first, since the rest of this page assumes it.

## Create a tenant for each customer

When a customer signs up to your product, create a tenant for them. Each tenant in Plain should map 1:1 to the workspace, team, or organization concept in your own product. [Tenants](/docs/graphql/tenants) covers the model in full.

This is the step that scopes the portal. Without tenants, there is no reliable way to tell which threads belong to which of your customers' teams, so a portal cannot show one team its own threads and nothing else.

Create tenants by [upserting them](/docs/graphql/tenants/upsert), then [add individual customers to a tenant](/docs/graphql/tenants/add-customers).

You can skip tenants while prototyping and filter by customer instead. The trade-off is that if John and Lucy are on the same team in your product, John sees only his own threads and not Lucy's. For most B2B products that is the wrong experience, but it is fine for a first pass.

## Fetch a tenant's threads

Once threads carry a tenant, fetch them by filtering on it:

```ts theme={null}
const threads = await plainClient.getThreads({
    filters: {
      tenantIdentifiers: [{ externalId: tenantExternalId }],
      statuses: [ThreadStatus.Todo, ThreadStatus.Snoozed],
    },
  });
```

If you skipped tenants, filter by `customerId` instead to get one customer's threads.

That returns the threads but not their contents. To read the messages on a thread, query its timeline entries:

```graphql theme={null}
query threadTimeline($threadId: ID!, $first: Int, $after: String, $last: Int, $before: String) {
  thread(threadId: $threadId) {
    title
    description
    priority
    status
    createdAt {
      __typename
      iso8601
    }
    customer {
      fullName
    }
    updatedAt {
      __typename
      iso8601
    }
    timelineEntries(first: $first, after: $after, last: $last, before: $before) {
      edges {
        cursor
        node {
          id
          timestamp {
            __typename
            iso8601
          }
          actor {
            __typename
            ... on UserActor {
              user {
                fullName
              }
            }
            ... on CustomerActor {
              customer {
                fullName
              }
            }
            ... on MachineUserActor {
              machineUser {
                fullName
              }
            }
          }
          entry {
            __typename
            ... on CustomEntry {
              title
              components {
                __typename
                ... on ComponentText {
                  text
                }
              }
            }
            ... on ChatEntry {
              chatId
              text
            }
          }
        }
      }
      pageInfo {
        __typename
        hasPreviousPage
        hasNextPage
        startCursor
        endCursor
      }
    }
  }
}
```

## Let customers open new threads

Build a contact form in your portal that [creates a thread](/docs/graphql/threads/create). Pre-fill the customer's tenant ID on the thread, or it will not appear in the portal.

A form with only a title and a message works, but structured questions are worth the extra fields: what the request is about, how urgent it is, which product area it affects. Those answers become thread fields and labels, which is what makes the thread routable without a human reading it first.

## Let customers reply to a thread

Technically this is one call to the `replyToThread` mutation. The work is in the interface rather than the API.

What you build depends on the channels you support. If your portal does not handle email, you do not need `Cc` or `Bcc` fields.

## Example implementation

There is a working Next.js example that puts all four pieces together:

[Headless portal example on GitHub](https://github.com/team-plain/example-headless-portal)

## Security

A portal calls [Plain's GraphQL API with an API key](/docs/graphql/authentication). That key needs broad permissions, because it reads threads and customer details and performs actions such as sending email.

<Warning>
  Never call the Plain API directly from the browser. Doing so exposes an API key that can read every thread and customer in your workspace.
</Warning>

Make every Plain API call from a backend you control. That is also where you enforce access control, so a customer can only reach their own threads.

If a key leaks, delete it in your workspace settings straight away, then contact Plain so we can help with mitigation and investigation.

## Getting help

A polished portal involves more than the four steps above: formatting, attachments, and file uploads all add work. Plain's engineering team can help you plan and scope it against your product and stack.

The headless portal is available on the Horizon and Frontier plans. To talk it through, contact us in Plain or at [help@plain.com](mailto:help@plain.com).
