Remediate

reference

every prop, every option.

naming

three things in remediate are called metadata. they're related but not the same:

  • metadata (prop)<Remediate metadata={{...}}>. extra data you attach to every submission. ends up at submission.metadata on the server, verbatim.
  • metadata (form field) — internal name of the json envelope inside the multipart POST. you only see this if you build FormData by hand or curl the route; parseFeedback hides it.
  • submission.metadata (json field) — the value of the metadata prop, sitting on the parsed FeedbackSubmission.

day-to-day you only touch the prop. the form field is a wire-format detail.

<Remediate />

all props optional. omit endpoint and onSubmit and the widget renders but submissions go nowhere.

prop
what is it
endpoint
url to POST FormData to.
onSubmit
called on submit. receives blobs inline. if both endpoint and onSubmit are set, the POST runs first; onSubmit runs only on success.
onError
called when the POST fails.
metadata
merged verbatim into submission.metadata.
headers
extra headers on the POST. use a function for auth tokens that may refresh.
captureTypes
which capture modes to expose. defaults to all.
open
controlled open state. pair with onOpenChange.
onOpenChange
called when the user opens or closes the widget.
debug
log lifecycle events to the console.
messages
override any user-visible string.

<RemediateTrigger />

replace the default floating button with your own trigger.

import { Remediate, RemediateTrigger } from "remediate";

<Remediate endpoint="/api/feedback">
  <RemediateTrigger asChild>
    <button>report a bug</button>
  </RemediateTrigger>
</Remediate>

asChild (radix-style) merges props onto the child. omit it for the default button.

imperative api

import { remediate } from "remediate";

remediate.open();
remediate.close();
remediate.submit();   // submit the current draft

parseFeedback(req)

import { parseFeedback } from "remediate/server";

const { submission, files } = await parseFeedback(req);

req is a web Request. returns Promise<ParsedFeedback>. see payload.

throws on malformed multipart bodies. wrap in try/catch.

runtime support

runtime
notes
next.js app router
route.ts exporting POST(req: Request)
next.js pages router
use parseFeedbackPages(req, res)
remix / react router
from a loader/action
hono
c.req.raw
express
use parseFeedbackExpress(req)
fastify
use the express adapter
cloudflare workers
native web Request
bun / deno
native web Request
edge runtime
streamed multipart works on edge

toMarkdown(submission, options?)

import { parseFeedback, toMarkdown } from "remediate/server";

const { submission } = await parseFeedback(req);
const body = toMarkdown(submission);

formats a submission as structured markdown. items grouped by type (annotations → text notes → captures), numbered to match the widget, priority badges, environment line.

works directly for github issues, linear, discord embeds, and email.

prop
what is it
fileUrls
map of file key → public url. screenshots render as inline images, recordings/voice as links. keys match FormData field names (e.g. "screenshot-cap_abc123").
environment
include the browser/os/viewport line at the bottom. default: true.
metadata
include metadata as a fenced json block. default: false.

cors

if your widget origin and endpoint origin differ, the browser will preflight. respond with:

return Response.json(
  { ok: true },
  {
    headers: {
      "Access-Control-Allow-Origin": "https://your-app.com",
      "Access-Control-Allow-Methods": "POST, OPTIONS",
      "Access-Control-Allow-Headers": "Content-Type, Authorization",
    },
  },
);

handle the OPTIONS preflight too.

csp

remediate needs:

media-src 'self' blob:
img-src 'self' data: blob:
connect-src 'self' <your-endpoint-origin>
worker-src 'self' blob:

worker-src only if you use video capture.

bundle

  • core: ~14kb gzipped
  • video capture: +8kb (lazy-loaded on first open)
  • voice capture: +2kb (lazy-loaded on first open)

"sideEffects": false. tree-shakes.

css

styles inject on mount under [data-remediate-widget]. the prefix is namespaced. no global resets, no tailwind reset interaction. override via:

[data-remediate-widget] { --rmd-accent: #ff5722; }