Data sources
Endpoint, store, or fully custom — how the inbox talks to your data.
The inbox reads and writes through a small abstraction with three ways to supply it. Pick exactly one — source, store, or endpoint. They're mutually exclusive in the type, so there's no precedence to remember and no way to pass two and wonder which one won.
Endpoint mode (production default)
<SitepingInbox projects="my-project" endpoint="/api/siteping" apiKey={KEY} />Talks to your SitePing endpoint: GET for lists, PATCH for statuses, DELETE for removals, always with cache: "no-store". Errors arrive typed by code (AUTH for 401/403 and VALIDATION for other 4xx, each with the HTTP status, SERVER for 5xx, NETWORK for fetch failures). There are deliberately no retries — a triage UI should tell you immediately.
Behind your app's login, leave apiKey out: mounted on the endpoint's origin, the inbox sends your session cookie with every request, for a server access policy to read — see the Better Auth recipe. From another origin, send a token with headers.
Store mode
<SitepingInbox projects="my-demo" store={new LocalStorageStore()} />Runs against any SitepingStore in-process — the same stores the widget's client-side mode uses. Demos and prototypes never need a server.
Custom source
For tRPC, GraphQL, or server actions, implement the three-method InboxSource contract and pass it as source:
import type { InboxSource } from "@siteping/dashboard";
const source: InboxSource = {
async list(query) {
// query: projectName, page, limit, status?, type?, search?, url?, urlPattern?
const { feedbacks, total } = await trpc.siteping.list.query(query);
// Important: createdAt / updatedAt / resolvedAt must be real Date objects
return { feedbacks: feedbacks.map(reviveDates), total };
},
async setStatus(id, projectName, status) {
return reviveDates(await trpc.siteping.setStatus.mutate({ id, projectName, status }));
},
async remove(id, projectName) {
await trpc.siteping.remove.mutate({ id, projectName });
},
};Discussion threads
Threads are optional in a custom source. Records carry theirs in comments (with createdAt as a Date, like the record's); to let the inbox post and delete replies, add the two thread methods:
const source: InboxSource = {
// …list, setStatus, remove as above
async addComment(feedbackId, projectName, input) {
// input: body, authorName, authorEmail, authorRole ("team"), clientId
return reviveDates(await trpc.siteping.addComment.mutate({ feedbackId, projectName, ...input }));
},
async removeComment(feedbackId, projectName, commentId) {
await trpc.siteping.removeComment.mutate({ feedbackId, projectName, commentId });
},
};list() may also return capabilities: { comments: false } to make threads read-only for a backend that keeps none, or deleteComments: false to hide the delete buttons of a backend that cannot delete replies; left out, addComment and removeComment alone decide. A source without removeComment posts replies but offers no delete. When the reply is gone already, have removeComment throw an error whose status is 404: the inbox then drops it instead of failing. The inbox replies as the team, whose replies never meet a thread's cap: a 409 from addComment means the server did not take the reply as the team's (see who speaks as the team), and the composer says it failed. The endpoint source's errors carry the response's status, and the store source's the store's own errors. The endpoint source maps the server's advertised capabilities (and a server that advertises nothing to read-only); the store source defines the methods only when the store implements addComment / deleteComment.
Permissions
A record may carry permissions — { canChangeStatus, canDelete, canComment, canDeleteComment }, what the requester may do with it — from list() and from setStatus(). The inbox then hides what they refuse (see Permissions); a record without them refuses nothing. When setStatus() resolves a record without them, it keeps the ones list() gave. The endpoint source passes on the server's.
The two building blocks behind the built-in modes are exported too — createEndpointSource({ endpoint, apiKey, headers }) and createStoreSource(store) — so you can wrap, compose, or decorate them (e.g. add logging around the HTTP client) instead of starting from scratch.
Types without extra dependencies
@siteping/dashboard re-exports everything you need to type a wrapper around it, so you never install another package:
| What you're typing | Exports |
|---|---|
| Feedback data | FeedbackRecord, FeedbackStatus, FeedbackType, SitepingStore, plus the FEEDBACK_STATUSES / FEEDBACK_TYPES constants and isClosedStatus() |
| Permissions | FeedbackPermissions (what the requester may do with a record), InboxRecord (a record with its optional permissions), InboxPage (what list() resolves) |
| Threads | CommentRecord (a stored reply), CommentCreateInput (what addComment receives), SitepingCapabilities (what list() may advertise) |
| Hook options | UseSitepingInboxOptions and its three members — InboxEndpointOptions, InboxStoreOptions, InboxCustomSourceOptions — plus the shared InboxSharedOptions |
| Hook result | InboxState, InboxStatusFilter, InboxTypeFilter |
| Sources | InboxSource, EndpointSourceOptions (the argument of createEndpointSource) |
| Component props | SitepingInboxProps, or SitepingInboxPresentationProps for the presentation half alone |
| Theming | InboxTheme ("light" | "dark" | "auto") and ResolvedTheme ("light" | "dark" — what auto resolves to) |