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.
Building one is four pieces of work:
- Create a tenant for each of your customers.
- Fetch that tenant’s threads to list them.
- Let customers open new threads.
- Let customers reply to a thread.
Read the data model 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 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, then add individual customers to a tenant.
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:
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:
Let customers open new threads
Build a contact form in your portal that creates a thread. 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
Security
A portal calls Plain’s GraphQL API with an API key. That key needs broad permissions, because it reads threads and customer details and performs actions such as sending email.
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.
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.