Mount SupportInbox and SupportTicket behind your own API and auth.
The screens never talk to the database. They call a SupportClient you implement on top of your own API, so your auth decides who's an agent. TanStack Query must be set up in your app (a QueryClientProvider above the screens).
Expose the API
Any API layer works: route handlers, server actions, oRPC, tRPC. Check the agent, then call the service:
// app/api/support/[...path]/route.ts, or your oRPC/tRPC router: anything behind your auth.
import { support } from "@/lib/support";
import { requireAdmin } from "@/lib/auth";
export async function POST(request: Request) {
const agent = await requireAdmin(request);
const { action, input } = await request.json();
const s = support();
switch (action) {
case "listTickets":
return Response.json(
await s.listTickets(
{
status: input.status,
assigneeId: input.assignee === "me" ? agent.id : input.assignee,
search: input.search,
sort: input.status === "open" ? "waiting" : "recent",
limit: input.pageSize,
offset: (input.page - 1) * input.pageSize,
},
agent.id
)
);
case "getTicket":
return Response.json(await s.getTicket(input.id, agent.id));
case "reply":
return Response.json(await s.reply({ ...input, agentId: agent.id }));
// …addNote, assign, setStatus, linkCustomer, retry, uploadAttachment, attachmentUrl
}
}Implement the client
"use client";
import type { SupportClient } from "@/components/support/client";
const call = (action: string, input?: unknown) =>
fetch("/api/support", { method: "POST", body: JSON.stringify({ action, input }) }).then((r) => r.json());
export const supportClient: SupportClient = {
listTickets: (query) => call("listTickets", query),
counts: () => call("counts"),
getTicket: (id) => call("getTicket", { id }),
reply: (input) => call("reply", input),
addNote: (input) => call("addNote", input),
assign: (input) => call("assign", input),
setStatus: (input) => call("setStatus", input),
linkCustomer: (input) => call("linkCustomer", input),
retry: (input) => call("retry", input),
upload: ({ ticketId, file }) => uploadFile(ticketId, file),
attachmentUrl: (input) => call("attachmentUrl", input),
listAgents: () => call("listAgents"),
searchCustomers: (query) => call("searchCustomers", { query }),
};Dates may arrive as Date objects or ISO strings; the screens accept both.
Mount the screens
// app/admin/support/layout.tsx
"use client";
import { SupportProvider } from "@/components/support/provider";
import { supportClient } from "./client";
export default function Layout({ children }: { children: React.ReactNode }) {
const user = useCurrentUser();
return (
<SupportProvider
client={supportClient}
viewerId={user.id}
inboxHref="/admin/support"
ticketHref={(id) => `/admin/support/${id}`}
customerPanel={(detail) => (detail.customer ? <AccountPanel id={detail.customer.id} /> : null)}
>
{children}
</SupportProvider>
);
}
// app/admin/support/page.tsx
export default function Page() {
return <SupportInbox />;
}
// app/admin/support/[id]/page.tsx
export default function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params);
return <SupportTicket ticketId={id} />;
}Provider props
| Name | Type | Description |
|---|---|---|
client* | SupportClient | Your implementation, above. |
viewerId* | string | The signed-in agent's id, as listAgents returns it. |
inboxHref* | string | Where the ticket screen's back link goes. |
ticketHref* | (id) => string | The URL of a ticket. |
customerPanel | (detail) => ReactNode | Your own view of the customer, shown next to every ticket: plan, orders, payments. |
responseTargetMinutes | number | How long a customer may wait before the wait turns red. It turns amber at three quarters.Default: 240 |
t | (key, values) => string | Translations. See below.Default: English |
locale | string | For dates and numbers.Default: "en" |
Try the props
Translations
Every string lives in components/support/messages/en.json. With next-intl, add that file to your messages under a support namespace and pass useTranslations("support") as t. Without an i18n library, call createTranslator(messages) with your own copy. The screens support right-to-left languages.