Skip to content

The relay (Web)

A browser can’t safely hold a writable GitHub token — anything shipped to the page is world-readable. So the web SDK POSTs feedback to a relay you operate: one small serverless function that holds the credential, does abuse control, and creates the issue. You host it; you hold the token.

  1. Verify the submission (and an optional CAPTCHA token).
  2. Throttle — per-IP / global rate limits, dedupe, payload caps (you add what you need).
  3. Format the issue body via @appfeedback/core (identical to every platform).
  4. Create the GitHub issue with your server-held credential and return { issueNumber, issueUrl }.

createFetchHandler returns a standard (Request) => Response handler:

import { createFetchHandler } from '@appfeedback/relay'
const handler = createFetchHandler({
githubToken: env.GITHUB_TOKEN, // server-only secret
owner: 'acme',
repo: 'feedback',
})
export default { fetch: handler } // Cloudflare Worker / Deno
// Vercel/Netlify: `export const POST = (req) => handler(req)`

Use a GitHub App installation token (1-hour, auto-rotated, scoped to one repo + issues/contents) for production, or a server-held fine-grained PAT scoped to a single repo for the simplest setup. Either way it lives only in the relay’s environment.

The browser POSTs JSON and gets back the issue number — so any backend can implement it:

// POST <relayEndpoint>
{ "type": "bug", "title": "", "description": "", "contactEmail": null,
"extraFields": {}, "deviceInfo": { /* appName, appVersion, … osName: "Web" */ },
"captchaToken": null }
// → 200
{ "issueNumber": 123, "issueUrl": "https://github.com/acme/feedback/issues/123" }

Errors use 400 (invalid), 401/403 (auth/CAPTCHA), 413 (too large), 429 (rate-limited), 502 (GitHub upstream).