All routes live under src/pages/api/ and run server-side only (export const prerender = false where set). See ARCHITECTURE.md for the middleware that wraps every response (cache-control forces no-store on all /api/* routes regardless of what a route sets) and SECURITY.md for the shared validation/CORS rules these routes follow.
| Route | Method | Auth / origin check | Upstream | Notes |
|---|---|---|---|---|
/api/events |
GET | none (public read) | ICS feed (fetchEvents, EVENTS_ICS_URL) |
Flat array; consumers group into category sections via eventSections.ts |
/api/faq |
GET | none | Notion (fetchNotionFAQ) |
?showAll=yes bypasses Visibility filter |
/api/ideas |
GET | none | Notion (fetchNotionIdeas) |
|
/api/speakers |
GET | none | Notion (fetchNotionAgenda) |
Returns { agendaItems, speakers }; resolves moderator relations to speaker profiles |
/api/e4p-signatories |
GET | none (public read) | Notion (Signatories DB) | Only returns rows where Approved is checked |
/api/e4p-pledge-sign |
POST | Origin: https://techforpalestine.org |
Notion (Signatories DB) | Validates required fields, email format, LinkedIn URL, 2000-char cap; writes with Approved: false (manual moderation) |
/api/endorsement-request |
POST | Origin: https://techforpalestine.org |
Notion (Endorsements DB) | Same validation pattern as pledge-sign |
/api/donation-complete |
POST + OPTIONS | Qgiv transaction verification (server-side lookup via QGIV_API_TOKEN) + Origin allowlist as defence in depth | EmailOctopus | Request body is { transactionId } only. Email and name are read from Qgiv's Transactions API. Verification failure returns 402. On success, tags contact donor. |
/api/membership-complete |
POST + OPTIONS | Qgiv transaction verification (server-side lookup via QGIV_API_TOKEN) + Origin allowlist as defence in depth | Hub API (HUB_API_URL) + EmailOctopus |
Request body is { transactionId } only. Email, name, and tier are read from Qgiv's Transactions API. Verification failure returns 402. On success, tags the contact in EmailOctopus and (for full members) invites them to the Hub (type: "paid"), in parallel via Promise.allSettled. |
/api/pipe |
POST | Origin allowlist (prod + *.pages.dev) |
Plausible Analytics | Server-side proxy for the Plausible events endpoint (avoids ad-blockers hitting plausible.io directly from the browser); on a dropped conversion event it also writes a fallback record to the DROPPED_CONVERSIONS KV namespace — see DONATIONS.md |
/api/sentry-webhook |
POST | HMAC signature (sentry-hook-signature header, SENTRY_WEBHOOK_SECRET) + rejects if an Origin header is present (webhooks shouldn't have one) |
Mattermost | Relays Sentry issue/alert webhooks into a Mattermost channel |
/api/projects |
GET | none (public read) | ProjectHub (projecthub.techforpalestine.org/api/public/projects) |
Sanitizes all URL-shaped fields to strip non-http(s) protocols (XSS guard); retries on 5xx with backoff. Logic lives in src/store/projectsClient.ts, shared with /projects/<slug>. The list is cached server-side for 5 minutes (24 hours stale-if-error); the X-Projects-Source response header reads hit, miss or stale |
/api/project-proxy |
GET, POST | none at this layer — adds Authorization server-side |
Generic upstream (PUBLIC_API_URL) |
Generic authenticated passthrough used by the volunteer/incubator application forms (src/store/api.ts); only forwards paths under /api/method/, explicit header allowlist — see SECURITY.md for the pattern |
/api/admin/conversion-stats |
GET | HTTP Basic Auth (ADMIN_USERNAME/ADMIN_PASSWORD, constant-time compare) |
Plausible Stats API v2 + DROPPED_CONVERSIONS KV |
Backs the /admin/conversions dashboard; merges live Plausible numbers with the KV fallback log — see DONATIONS.md |
export const prerender = false;on any route that reads request data or calls an external API.- Errors: call
reportError(error, { context: "route-name" })fromsrc/lib/report-error.ts, thenctx?.waitUntil(Promise.resolve(Sentry.flush(2000)))before returning — Cloudflare Workers can terminate the request before an unflushed Sentry event is sent. - Return generic error messages to the client (
"Failed to process request"); never leak the caught error or stack trace. - Public write endpoints: validate required fields, email regex, URL fields via
try { new URL(x) } catch, and cap free-text fields at 2000 characters — checkOriginbefore parsing the body. - Use
getEnv(name, locals), neverprocess.envdirectly.