Skip to content
Club system guide

Requests, accounts, and access

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

How a request reaches the database

The website sends the browser request to the API. The API checks it, runs the right handler, and sends the response back.

Request path

  1. Browser Calls /api/v1/... with its cookies
  2. Website bridge Removes internal headers that a browser could fake
  3. Service binding Adds the internal token and proxy details
  4. API checks Checks the route, session, suspension, and rate limit
  5. Request handler Checks the input and applies the business rules
  6. D1 / R2 Reads or saves the data
  7. Response Returns the status and data to the browser
The browser uses /api/v1 for normal API routes. Better Auth keeps /api/auth, so the website bridge does not rewrite those routes.

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:

  1. Removes connection-only headers from the browser request.
  2. Removes private headers that a browser could fake.
  3. Adds trusted request details and the website’s private service token.
  4. Uses the 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.

Why /api/v1 and /api/auth use different paths

Section titled “Why /api/v1 and /api/auth use different paths”

The 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.

OrderCheckWhat it prevents
1Browser permission check (CORS preflight) and health routeFailed browser checks and unnecessary account work on the health route
2Private service authorizationAn untrusted caller pretending to be the website, Discord Worker, Email Worker, or Scheduler Worker
3Path and method matchAn unknown path or unsupported request method reaching feature code
4Private route checkA browser session using scheduled jobs, receipt fulfillment, or routes that claim a one-use request number (nonce)
5Better Auth session lookupProtected work running without a current user and session
6Suspension checkA suspended account continuing normal API work
7General rate limitOne account or client sending too many requests to a group of routes
8Public route or account permissionProtected data reaching an anonymous or unverified account
9Limits for the requested actionA burst of uploads or event actions passing the broader limit
10Feature codeFeature 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.

Discord linking and server verification do different jobs

Section titled “Discord linking and server verification do different jobs”

The system has two Discord account flows.

OAuth account link

Connect a website account to Discord

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

Complete Discord server verification

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:

  • Is the account suspended?
  • Is Discord linked?
  • Is the membership active and at the required tier?
  • Is the user an officer or staff member for an admin route?
  • Does the user own the record being edited?
  • Can the request move from its current status to the next one?

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.

Expanded photograph