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
| Tool | Role | Cost |
|---|---|---|
| Clerk | Client authentication | Free (50K MAU) |
| Cloudflare Pages | Portal hosting | Free |
| Sutrena | Ticket 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>
);
}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.
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.
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.
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.
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.
1. Get a trial key (no auth, no signup)
curl -X POST https://sutrena.com/api/trial2. 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}'Get a trial API key instantly with no signup, or create an account for the full experience.