Store feedback posts in Postgres

Prefer your own schema, RLS, and backups? Use BYO: Bootstrapware hosts published board config; your API writes Postgres. Post title and body never leave your infrastructure.

Live publishable config still needs a BYO ($9.99) or Hosted ($19.99) entitlement. Live Hosted post storage is a different product tier, skip it if Postgres is the source of truth.

Adapter contract

Implement listPosts, createPost, vote, and unvote. Pass the object as adapter (or wrap with createByoAdapter). Full contract: adapter docs.

<Feedback
  boardId="brd_..."
  user={currentUser}
  adapter={{
    listPosts: async ({ boardId, status, cursor }) =>
      fetch(`/api/feedback?boardId=${boardId}`).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 }),
      });
    },
  }}
/>

Enforce title ≤ 200 and body ≤ 5000 characters on your API to match the widget (TITLE_MAX / BODY_MAX).

Illustrative customer schema (not Bootstrapware tables)

The SQL below is an example for your database. It is not the Hosted schema and not something Bootstrapware creates for you.

-- Illustrative customer schema, your tables, not Bootstrapware Hosted tables.
create table feedback_posts (
  id text primary key,
  board_id text not null,
  title text not null check (char_length(title) <= 200),
  body text check (body is null or char_length(body) <= 5000),
  status text not null check (
    status in ('open', 'planned', 'in_progress', 'shipped', 'declined')
  ),
  vote_count integer not null default 0,
  author_id text not null,
  author_name text,
  created_at timestamptz not null default now(),
  updated_at timestamptz not null default now(),
  archived_at timestamptz,
  merged_into_post_id text
);

create table feedback_votes (
  post_id text not null references feedback_posts (id),
  author_id text not null,
  created_at timestamptz not null default now(),
  primary key (post_id, author_id)
);

Status values must stay in that set, use shipped, not completed. Map adapter responses to the FeedbackPost shape your UI expects (id, boardId, title, body, status, voteCount, authorId, timestamps, etc.).

Authz on your API

Prefer session-derived author ids over trusting the client body. Unique votes belong in the database constraint above. Cursor pagination in listPosts is yours to design.

Pitfalls

  • Copying this SQL as if it were Hosted internals, Hosted storage is separate and opaque to you.
  • Expecting Bootstrapware Feedback webhooks for BYO creates, those events are for Hosted store activity.
  • Skipping live entitlement and wondering why live publishable config fetch fails.

Related: FeedbackAdapter guide · BYO vs Hosted · Adapter contract · Modes · Privacy