SDK

React feedback components

Let people file bug reports from your React app and follow the answer inside it, with unstyled components and headless hooks.

@myna-sh/react/feedback is the reporter's side of Myna Feedback: a form built from the board's own questions, the list of what somebody has filed, and the thread where they read the answer and say whether it worked.

It runs inside your product, which is the point. Myna serves no reporter-facing page: your user is already signed in with you, and Myna's own identity model is GitHub OAuth — not a thing to ask of somebody who reported one bug.

$ npm install @myna-sh/react @myna-sh/sdk

React and @myna-sh/sdk are peer dependencies; the package has no others.

The whole thing

import {
  FeedbackProvider,
  ReportForm,
  MyReports,
  ReportThread,
} from "@myna-sh/react/feedback";
import "@myna-sh/react/feedback.css"; // optional

export function Support({ user, signature }) {
  const [open, setOpen] = useState<number>();
  return (
    <FeedbackProvider
      ingestKey="myna_ik_…"
      board="bugs"
      identify={{ id: user.id, email: user.email, signature }}
      context={() => ({ appVersion: BUILD.version, route: location.pathname })}
    >
      <ReportForm onSubmitted={(r) => setOpen(r.number)} />
      <MyReports onSelect={setOpen} />
      {open ? <ReportThread number={open} /> : null}
    </FeedbackProvider>
  );
}

board is only needed when the ingest key is not bound to one. context is whatever your application knows and chooses to send; Myna never collects it itself.

Identity

signature comes from your own backend:

import { signIdentity } from "@myna-sh/sdk/feedback/server";

// Your own API route, for the user this request is already authenticated as.
export async function GET(request) {
  const user = await authenticate(request);
  return Response.json({
    signature: signIdentity(process.env.MYNA_IDENTITY_SECRET, user.id),
  });
}

Without it a report can still be filed — unless the board sets requireIdentity — but nothing can be read back. An unsigned claim about who somebody is would let any visitor read another person's reports.

Pass identify once you know who is using the application, and drop it on sign-out. The provider pushes the change into the existing client rather than rebuilding it, so signing in mid-session works without a remount; clearing it matters, because otherwise the next person on a shared machine reads the last one's reports.

See identifying your users for properties, the ceilings, and how the secret is minted and rotated.

Components

ReportForm

Renders the board's intake form — every question, in the order the board asks them, including the ones that fill the report's own title and description. Nothing is hardcoded, which is what stops this component and a hand-written form from asking the same board two different things.

<ReportForm
  board="bugs"
  onSubmitted={(result) => console.log("filed", result.number)}
  success={(result) => <p>Thanks — that is #{result.number}.</p>}
  labels={{ submit: "Send it" }}
/>

The form arrives one round trip late and renders a status line until it does; a form that has to be corrected after it appears is worse than one that appears a moment later. labels covers only what the board does not declare — the button, the confirmation, the loading and failure lines. Reword the questions in board settings.

Validation errors bind back per field: the API answers with a fields record keyed by path and the form renders each message under the input it belongs to.

MyReports

Everything this identity has filed, newest activity first.

<MyReports
  onSelect={setOpen}
  empty={<p>Nothing reported yet.</p>}
  children={(r) => <>{r.title}{r.status}</>}
/>

Without a signed identity it renders the empty state rather than an error, because "you have not reported anything" is what a signed-out visitor should see, not a sentence about HMAC.

ReportThread

One report and its conversation, with the two buttons the product exists for.

<ReportThread number={42} labels={{ worksNow: "Fixed for me" }} />

"It works now" and "Still broken" appear only while the team is waiting on this person — awaitingYou, which is the report sitting in needs_retest. Showing them all the time would invite somebody to close a report nobody has looked at yet, and the confirmation would then mean nothing.

What the thread shows is deliberately thin: the title, the body, the status, and the public conversation with the team attributed as "team" rather than by name. No priority, no labels, no assignee, no internal notes, no attachment manifest, no board guidance.

Hooks

Every component is built on these, and they are exported for anyone who would rather render their own.

Hook Returns
useFeedback() The underlying FeedbackClient from @myna-sh/sdk/feedback
useBoardSchema(board?) { data, error, isLoading, refresh } — the board's questions
useSubmitReport() { submit, isSubmitting, error, result, reset }
useMyReports() { data, error, isLoading, refresh } — their reports
useReport(number) The above, plus reply, confirm, reopen, isActing
const { data: schema } = useBoardSchema();
const { submit, isSubmitting, error } = useSubmitReport();

There is no shared cache here, unlike the content hooks. A page renders dozens of content reads and one feedback surface; a cache would mostly add a way for somebody to see a stale answer to the question they just asked. Each action refetches, so the pane repaints from the server's answer rather than from a guess about what the action did.

Styling

Every element carries a data-myna attribute and nothing else — no class names, no inline styles, no CSS-in-JS.

[data-myna="submit"] { background: var(--brand); }
[data-myna="field"][data-myna-field="build"] { grid-column: span 2; }
[data-myna="event"][data-myna-author="you"] { text-align: right; }

That is not minimalism for its own sake. These components sit inside somebody else's product, next to their buttons and their type; a component that arrives with opinions is a component that has to be fought, and every widget that ships a stylesheet ends up in a specificity war with the application embedding it.

The optional stylesheet is a starting point, driven by three custom properties:

@import "@myna-sh/react/feedback.css";

.support {
  --myna-accent: #e2653a;
  --myna-radius: 10px;
  --myna-font: "Inter", sans-serif;
}

Everything else comes from currentColor and color-mix, so the form inherits the surrounding light or dark surface instead of asserting one.

Errors

FeedbackError from @myna-sh/sdk/feedback is re-exported here. It carries status, a stable code, and a fields record keyed by path.

Code Meaning
IDENTITY_REQUIRED The board requires a signed identity, or a read-back call carried none
PERMISSION_DENIED The Origin is not registered on the project, or the key may not file on that board
PRODUCT_NOT_ENABLED The project does not run Feedback
RATE_LIMITED Too many submissions from this address
VALIDATION_FAILED A field ceiling or the board's intake form