Adapter

A FeedbackAdapter is how BYO mode talks to your store. Helpers: createLocalAdapter (demo), createByoAdapter (wrap callbacks), createHostedAdapter (used internally when you pass boardId + publishableKey).

Resolve order

  1. If adapter is passed, use it (wins over Hosted props).
  2. Else if boardId and publishableKey are set, use Hosted.
  3. Else fall back to createLocalAdapter.

Modes and pricing: Modes.

Interface

type FeedbackAdapter = {
  listPosts(input: {
    boardId: string;
    status?: FeedbackStatus;
    cursor?: string;
    q?: string;
  }): Promise<{ posts: FeedbackPost[]; nextCursor: string | null }>;
  createPost(input: {
    boardId: string;
    title: string;
    body?: string;
    author: FeedbackAuthor;
    authorToken?: string;
    idempotencyKey?: string;
  }): Promise<FeedbackPost>;
  vote(input: {
    postId: string;
    author: FeedbackAuthor;
    authorToken?: string;
  }): Promise<void>;
  unvote(input: {
    postId: string;
    author: FeedbackAuthor;
    authorToken?: string;
  }): Promise<void>;
};

FeedbackStatus is open | planned | in_progress | shipped | declined. Optional authorToken is a host-signed assertion minted with POST /api/v1/author-tokens (secret key). Required when the board sets requireAuthorToken. See Identity.

Optional q on listPosts is how similar-request search looks past the loaded page. After 4 characters in the compose title, Hosted and BYO debounce listPosts({ q }). Honor q in BYO so duplicates on later pages can still surface. Local mode falls back to in-memory matching. Up to 3 clickable results jump to the post and focus Vote.

BYO example

<Feedback
  boardId="brd_..."
  user={currentUser}
  adapter={{
    listPosts: async ({ boardId, status, cursor, q }) => {
      const params = new URLSearchParams({ boardId });
      if (status) params.set("status", status);
      if (cursor) params.set("cursor", cursor);
      if (q) params.set("q", q);
      return fetch(`/api/feedback?${params}`).then((r) => r.json());
    },
    createPost: async (input) =>
      fetch("/api/feedback", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(input),
      }).then((r) => r.json()),
    vote: async ({ postId, author }) => {
      await fetch(`/api/feedback/${postId}/vote`, {
        method: "POST",
        body: JSON.stringify({ author }),
      });
    },
    unvote: async ({ postId, author }) => {
      await fetch(`/api/feedback/${postId}/unvote`, {
        method: "POST",
        body: JSON.stringify({ author }),
      });
    },
  }}
/>

Routes are illustrative. Enforce authz on your API using the signed-in session, do not trust the browser alone. Enforce one vote per author.id per post if you want honest counts. Forward optional q on your list route so similar requests can search beyond the page currently on screen.

Limits and content

  • Title max 200 characters; body max 5000 (plain text).
  • Return FeedbackPost shapes the widget expects (id, boardId, title, body, status, voteCount, author fields, timestamps).
  • BYO does not emit Bootstrapware feedback.post_* webhooks, emit your own from your API if needed.

Walkthrough: Own your feedback API.

Related: Modes · Identity · Limits · Quickstart · Postgres