Not trusted from a browser
Private tokens, service identity, signed-event details, and trusted proxy headers.
How browser requests reach the API and how the API checks accounts, permissions, and rate limits.
A dashboard button sends its request to the website, usually through /api/v1/.... It does not call the public API address. The website then sends the request to the API Worker through a private Cloudflare link called a service binding.
This extra step separates browser data from trusted service data. The browser can send cookies and form values. It cannot add the private headers that prove a trusted Worker sent the request.
System flow
The website sends the browser request to the API. The API checks it, runs the right handler, and sends the response back.
Request path
The website’s catch-all API route receives every request under /api/*. It builds a new request before sending it to the API Worker.
It does four things:
API_WORKER service binding instead of a public network request.Cookies and authorization headers still go to the API because Better Auth, the account library, needs them to find the session. The API can also tell that the full request came through an approved service.
Not trusted from a browser
Private tokens, service identity, signed-event details, and trusted proxy headers.
Sent when needed
Session cookies, authorization headers, content type, body, method, query string, and safe browser details.
Added by the website
The shared private token, plus rebuilt host, protocol, and client-IP details. The website removes x-pcc-internal-source and does not add a website source label.
/api/v1 and /api/auth use different pathsThe website’s request code adds a version to normal app routes. For example, /api/gallery becomes /api/v1/gallery. The API then maps that versioned path to its internal route.
Better Auth keeps /api/auth/.... Its sign-in callbacks and cookies depend on that exact path, so adding /v1 would break them.
The API can also be reached through a custom domain. That path does not skip the private checks. When a private token is configured, every API request still passes the internal authorization rules. This page describes the route and shared checks in the repository. The live external route was not tested for this guide.
The API runs these checks in order before any feature code handles the request.
| Order | Check | What it prevents |
|---|---|---|
| 1 | Browser permission check (CORS preflight) and health route | Failed browser checks and unnecessary account work on the health route |
| 2 | Private service authorization | An untrusted caller pretending to be the website, Discord Worker, Email Worker, or Scheduler Worker |
| 3 | Path and method match | An unknown path or unsupported request method reaching feature code |
| 4 | Private route check | A browser session using scheduled jobs, receipt fulfillment, or routes that claim a one-use request number (nonce) |
| 5 | Better Auth session lookup | Protected work running without a current user and session |
| 6 | Suspension check | A suspended account continuing normal API work |
| 7 | General rate limit | One account or client sending too many requests to a group of routes |
| 8 | Public route or account permission | Protected data reaching an anonymous or unverified account |
| 9 | Limits for the requested action | A burst of uploads or event actions passing the broader limit |
| 10 | Feature code | Feature rules receiving a request that has not been checked |
The general limit is 120 requests per minute. Gallery uploads, profile uploads, and event actions each have a separate limit of ten per minute. When a request reaches a limit, the API returns 429 with a Retry-After header. It does not silently drop the request.
Better Auth runs inside the API Worker and stores account data in D1. This includes users, sessions, linked accounts, verification records, two-factor records, and passkeys.
Members can sign in with an email and password. Discord OAuth, Discord’s secure account-link process, connects a Discord identity to an existing account. It cannot create a new account, so Discord does not become a second, unreviewed sign-up method.
When a request has a cookie or authorization header, the API asks Better Auth for its session. A valid session provides both the session and user records. Before feature code runs, the API can mark an expired membership and refresh the account’s current access.
If the API cannot find a valid session, protected routes treat the request as anonymous. User details in a JSON body cannot replace a signed session.
The system has two Discord account flows.
OAuth account link
Better Auth completes Discord OAuth and links that Discord identity to the account. Its account checks enforce the server policy. The website receives a stable discordId for permission checks and updates.
Verification code
A separate check stores a short-lived code, its expiry, the attempt limit, and the related application. To finish, the API checks Turnstile, marks the code as used, and sends a Discord event to update the role and nickname.
Linking proves which Discord account belongs to a website user. Server verification proves that the user completed a specific server-entry check before its code expired or reached its attempt limit.
Turnstile, Cloudflare’s anti-bot check, protects public pages that can create an account or save lasting work. It covers the Better Auth entry path, Discord verification, and photographer requests.
The browser receives a challenge token. Before accepting the action, the API checks that token with the configured secret and allowed website addresses. If the token is missing or invalid, the API stops before it creates an account or saves a request.
Turnstile is only one check. Sessions, the list of public routes, input validation, rate limits, and feature permissions still apply.
Signing in does not give a member permission to do everything.
The website checks the session and role before showing protected dashboard sections. A member must link Discord to use the normal dashboard. Membership and staff rules then control protected pages.
The API checks these permissions again before it reads or changes data. Hiding a button can make the interface clearer, but it is never the permission check.
The API may ask:
After the API decides the membership or verification state, it updates Discord roles to match. The database holds the decision. A Discord role only reflects it.
The router lists every public read. These routes cover public stats, membership information, merchandise, gallery images, profiles, competition results, events, and leadership. A few public actions are also listed, including newsletter changes, verification completion, and photographer requests.
The router does not make every route under a shared prefix public. /api/gallery can be public while /api/gallery?mine=true still requires the current user. Competition results can be public while entry management stays protected. The request method and full path both matter.
New routes start as private. A developer must list a route before it becomes public.
The API returns an error as soon as it knows what went wrong:
400 for malformed or invalid input.401 when a protected route has no valid session.403 when the user is signed in but unverified, suspended, or missing permission.404 when a route or record does not exist.405 when the route exists but the method does not.429 when a rate limit is reached.500 for an unexpected server failure. The response does not include internal stack details.Server logs keep the technical details. The interface receives a safer message that does not reveal a token, database query, or private service address.