A Cloudflare Worker that lets students authenticate with GitHub and match that account to a student ID. D1 is the source of truth.
- GitHub OAuth with public-profile access only (
read:user) - Signed, HTTP-only sessions; GitHub access tokens are never stored
- One GitHub account per student ID, enforced by D1 unique indexes
- Self-service updates for returning students
- Instructor-only JSON and CSV exports
- Responsive interface based on the visual language of ait4x.org
You need Node 22+, a Cloudflare account, and a GitHub OAuth App.
The quickest path is the guided script. It creates the D1 database, writes .env, builds, applies migrations, stores the four Worker secrets, and prints the values you need for GitHub:
npx wrangler login
./scripts/setup-cloudflare.shIt is safe to re-run; an existing database is reused rather than duplicated. To do the same steps by hand:
-
Install and authenticate:
npm install npx wrangler login
-
Create the database:
npx wrangler d1 create github-student-registry
-
Copy
.env.exampleto.env. Add the D1 ID returned above and setNEXT_PUBLIC_SITE_URLto the final HTTPS address. The build uses these values for the D1 binding and absolute social-preview metadata. -
Build and migrate:
npm run build npm run db:migrate:remote
-
Create a GitHub OAuth App under GitHub → Settings → Developer settings → OAuth Apps. Use the deployed site as the homepage and this callback URL:
https://YOUR-DOMAIN/api/auth/github/callback -
Add the Worker secrets. Generate independent random values for the last two:
npx wrangler secret put GITHUB_CLIENT_ID --config dist/server/wrangler.json npx wrangler secret put GITHUB_CLIENT_SECRET --config dist/server/wrangler.json openssl rand -base64 32 | npx wrangler secret put SESSION_SECRET --config dist/server/wrangler.json openssl rand -base64 32 | npx wrangler secret put ADMIN_TOKEN --config dist/server/wrangler.json
-
Deploy:
npm run deploy
If the first deploy creates the workers.dev address you will keep, update NEXT_PUBLIC_SITE_URL with it and deploy once more.
Only students on the roster can register, and each intake is stored under its own cohort so the registry can be reused next year.
A roster is a file of enrolled student IDs, one per line, holding the last four digits of the PolyU ID and its trailing letter:
5668G
4605G
Put it at db/cohorts/<cohort>.cohort and load it into D1:
npm run roster:seed -- 2026 --remote # production
npm run roster:seed -- 2026 # local development databaseSeeding replaces that cohort's rows, so removing a line removes the entry. A duplicate or malformed line aborts the run before anything is written.
db/cohorts/*.cohort is git-ignored on purpose: the IDs are student data, and
this repository is public. A published roster would also let anyone claim an ID
before its owner registers. Keep the file locally and re-seed when it changes.
For next year: add db/cohorts/2027.cohort, seed it, then set
CURRENT_COHORT in lib/cohort.ts to 2027 and deploy. Existing rows keep
their own cohort and are left alone.
Copy .dev.vars.example to .dev.vars and add development OAuth credentials and random secrets. Set the OAuth callback to http://localhost:3000/api/auth/github/callback.
npm run devFor a local production-style Worker preview:
npm run build
npm run db:migrate:local
npm run roster:seed -- 2026
npm startNever commit .env, .dev.vars, db/cohorts/*.cohort, OAuth secrets, or admin tokens.
The Deploy to Cloudflare workflow runs on every push to main and can also be started manually. It checks the code, builds the Worker, applies pending D1 migrations, and deploys.
Open the Workers & Pages dashboard and copy Account ID from the right-hand sidebar. It is a 32-character hex string. The account ID is an identifier, not a credential, but there is no reason to publish it.
From the terminal, npx wrangler whoami prints the same value.
-
Go to My Profile → API Tokens and choose Create Token.
-
Scroll to Custom token and choose Get started. The
Edit Cloudflare Workerstemplate does not include D1, so a custom token is the correct choice here. -
Give it a recognisable name, for example
sd5913-registry-deploy. -
Add exactly these permissions:
Type Resource Access Account Workers Scripts Edit Account D1 Edit -
Under Account Resources, select Include and then the single account you deploy to. Leave Zone Resources unset.
-
Optionally restrict TTL to the length of the semester.
-
Continue to summary, then Create Token.
-
Copy the token now. Cloudflare shows it exactly once.
The token can deploy Workers and write to every D1 database on that account, so treat it as a credential: paste it straight into GitHub and do not store it in a file, a chat message, or the repository.
Create a GitHub environment named production under Settings → Environments → New environment, then add:
| Type | Name | Value |
|---|---|---|
| Environment secret | CLOUDFLARE_API_TOKEN |
The token created above |
| Environment secret | CLOUDFLARE_ACCOUNT_ID |
Your 32-character account ID |
| Environment variable | CLOUDFLARE_D1_DATABASE_ID |
The ID returned by wrangler d1 create |
| Environment variable | NEXT_PUBLIC_SITE_URL |
The final HTTPS URL, without a trailing slash |
Secrets go under Environment secrets and variables under Environment variables; the workflow reads them as secrets.* and vars.* respectively, so a value added in the wrong place fails the Verify configuration step.
The GitHub OAuth credentials and SESSION_SECRET / ADMIN_TOKEN remain Cloudflare Worker secrets. Add them once using the commands in the first-time setup section; the deployment workflow does not copy application secrets through GitHub.
Roll the API token at API Tokens with Roll and update the CLOUDFLARE_API_TOKEN secret, or Delete it to cut off deployment access immediately. Rotate a Worker secret by running wrangler secret put again with the new value; the change takes effect on the next request without a redeploy.
/admin lists the current cohort's registrations, who on the roster has not
registered yet, and a CSV download. From there you can release a registration
(freeing an ID whose owner is locked out because someone else claimed it) or
correct a student ID in place. An edited ID is checked against the roster and
refused if another account already holds it.
Access is granted by the ADMIN_LOGINS Worker secret, a comma-separated list
of GitHub logins:
echo "your-github-login" | npx wrangler secret put ADMIN_LOGINS --config dist/server/wrangler.jsonAdding a teaching assistant is another wrangler secret put; it takes effect on
the next request with no redeploy. Anyone signed in who is not on the list gets
a 404, so the page is not advertised to students. If the secret is unset, nobody
has access. Note that the list matches GitHub logins, so renaming your GitHub
account means updating the secret.
The same data is available without signing in, for scripts. Keep ADMIN_TOKEN private and download a spreadsheet-ready CSV with:
curl -fsS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
"https://YOUR-DOMAIN/api/admin/registrations?format=csv" \
-o sd5913-github-students.csvRemove ?format=csv for JSON. The export includes student ID, GitHub login and numeric ID, profile URL, name, and timestamps.
- Short-lived OAuth state cookie prevents login CSRF.
- Writes require a signed session and same-origin request.
- Session cookies are
Secure,HttpOnly, andSameSite=Lax. - No email, organization, or repository permissions are requested.
- D1 enforces one-to-one matches under concurrent writes.
- Instructor exports are uncached and require a bearer token.
npm run lint
npx tsc --noEmit
npm run build