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.
What the relay does
Section titled “What the relay does”- Verify the submission (and an optional CAPTCHA token).
- Throttle — per-IP / global rate limits, dedupe, payload caps (you add what you need).
- Format the issue body via
@appfeedback/core(identical to every platform). - Create the GitHub issue with your server-held credential and return
{ issueNumber, issueUrl }.
Deploy it
Section titled “Deploy it”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)`import { onRequest } from 'firebase-functions/v2/https'import { firebaseHandler } from '@appfeedback/relay'
export const feedback = onRequest( firebaseHandler({ githubToken: process.env.GITHUB_TOKEN!, owner: 'acme', repo: 'feedback' }),)import { appwriteHandler } from '@appfeedback/relay'
export default appwriteHandler({ githubToken: process.env.GITHUB_TOKEN!, owner: 'acme', repo: 'feedback',})Any stack can implement the relay HTTP contract directly, or call the core handler:
import { handleFeedback } from '@appfeedback/relay'
const result = await handleFeedback(requestBody, { githubToken, owner, repo, verifyCaptcha: async (token) => await turnstileVerify(token), // optional})Credentials
Section titled “Credentials”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 contract
Section titled “The contract”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).