Blueprints/Build a white-label client portal

Build a white-label client portal

Clients log in, submit tickets, pick a priority. You see everything in one dashboard. That is the whole thing.

Updated March 2026

You need a place for clients to send you requests. Something with a login, a form, and a way for you to see what came in. Clerk handles auth. Cloudflare Pages hosts the portal. Sutrena stores the tickets and gives you a dashboard with priority breakdowns and a timeline. Brand it with your own domain and Clerk's branding settings. $9/month total on Builder, or $29/month on Pro if you need more forms. An agent can set this up end-to-end: deploy the portal page with Clerk auth, create the ticket form with client_id tracking, build the admin dashboard with priority and timeline charts.

Architecture

ToolRoleCost
ClerkClient authenticationFree (50K MAU)
Cloudflare PagesPortal hostingFree
SutrenaTicket form, submission storage, admin dashboard$9/mo (Builder)

Total cost: $9/mo

Builder starts at $9/month for 50 projects. Pro at $29/month covers 200 projects and all your other work too. Build five blueprints on Pro and it is still $29/month. That is where the value is — at scale.

Clerk gives each client a user_id. The portal is a React app on Cloudflare Pages with a ticket form. When a client submits, the frontend injects their Clerk user_id as a hidden client_id field. Sutrena stores the ticket.

The admin dashboard shows tickets by priority (bar chart), total count, and a table sorted by most recent with the client_id column for filtering. For white-labeling, use your own domain on Cloudflare Pages and customize Clerk's branding.

Form Definition

Hidden client_id from Clerk, a subject line, a message, and a priority level. The client_id links each ticket to a specific client for filtering.

{
  "name": "Client Support Tickets",
  "fields": [
    {
      "name": "client_id",
      "type": "hidden"
    },
    {
      "name": "subject",
      "label": "Subject",
      "type": "text",
      "required": true
    },
    {
      "name": "message",
      "label": "Message",
      "type": "textarea",
      "required": true
    },
    {
      "name": "priority",
      "label": "Priority",
      "type": "select",
      "options": [
        "Low",
        "Normal",
        "Urgent"
      ],
      "required": true
    }
  ]
}

Dashboard Definition

Total tickets, priority breakdown, daily volume, and a table of recent tickets with subject, priority, and client ID for triage.

{
  "version": 1,
  "widgets": [
    {
      "type": "metric_card",
      "title": "Total Tickets",
      "value": "count(*)"
    },
    {
      "type": "bar_chart",
      "title": "Tickets by Priority",
      "groupBy": "priority"
    },
    {
      "type": "line_chart",
      "title": "Tickets Per Day",
      "groupBy": "$submitted_at:day"
    },
    {
      "type": "data_table",
      "title": "Recent Tickets",
      "columns": [
        "subject",
        "priority",
        "client_id"
      ],
      "limit": 50,
      "sortBy": "$submitted_at",
      "sortOrder": "desc"
    }
  ]
}

Frontend Integration

A ticket form that gets the client_id from Clerk automatically. After submission, shows a confirmation with an option to submit another. Replace frm_YOUR_FORM_ID with yours.

"use client";
import { useUser } from "@clerk/nextjs";
import { useState } from "react";

const FORM_ID = "frm_YOUR_FORM_ID";

export function TicketForm() {
  const { user } = useUser();
  const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    if (!user) return;
    setStatus("sending");

    const fd = new FormData(e.currentTarget);
    const res = await fetch(
      `https://sutrena.com/api/forms/${FORM_ID}/submit`,
      {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
          client_id: user.id,
          subject: fd.get("subject"),
          message: fd.get("message"),
          priority: fd.get("priority"),
        }),
      }
    );

    setStatus(res.ok ? "sent" : "idle");
  }

  if (status === "sent") {
    return (
      <div>
        <p>Ticket submitted. We will respond within 24 hours.</p>
        <button onClick={() => setStatus("idle")}>Submit another</button>
      </div>
    );
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="subject" placeholder="Subject" required />
      <textarea
        name="message"
        placeholder="Describe your issue or request..."
        rows={5}
        required
      />
      <select name="priority" required>
        <option value="">Select priority</option>
        <option value="Low">Low</option>
        <option value="Normal">Normal</option>
        <option value="Urgent">Urgent</option>
      </select>
      <button type="submit" disabled={status === "sending" || !user}>
        {status === "sending" ? "Submitting..." : "Submit Ticket"}
      </button>
    </form>
  );
}

FAQ

Can clients see their own tickets?

Not directly through Sutrena. You would need to fetch all submissions via the API and filter by client_id in a Cloudflare Worker or server component. Sutrena stores the data, but it does not have per-user views built in.

How do I white-label the portal?

Customize Clerk's sign-in page with your logo and colors. Host on a custom domain via Cloudflare Pages. Clients never see Sutrena -- you use the fetch API, not the embed.

Can I add file attachments to tickets?

Yes. Add a file field to the form. The frontend needs the presigned upload flow from the file upload guide. A bit more wiring, but it works.

How do I get notified of urgent tickets?

Set up a webhook on the form that sends to Slack. All tickets trigger it. If you want only urgent ones, use Zapier or a Cloudflare Worker to filter by priority before forwarding.

What is Sutrena?

Sutrena is the web runtime for AI agents. Three primitives — pages, forms, and dashboards — accessible through one API. Your agent creates web artifacts, humans interact with them, and your agent gets the data back. Framework-agnostic. Works from any MCP client or HTTP client.

Get started in two API calls

1. Get a trial key (no auth, no signup)

curl -X POST https://sutrena.com/api/trial

2. Create a form + dashboard from a template

curl -X POST https://sutrena.com/api/forms \
  -H "Authorization: Bearer st_trial_xxx" \
  -H "Content-Type: application/json" \
  -d '{"templateId": "waitlist", "createDashboard": true}'

Ready to build?

Get a trial API key instantly with no signup, or create an account for the full experience.

Build a white-label client portal — Sutrena | Sutrena