The website of AMYBO, a non-profit open source protein fermentation community: sustainable protein for all.
Built with Astro and Starlight, hosted on Cloudflare Pages. Event registration uses Events&I (beta), andeye's open source registration system on Pages Functions and D1; a copy lives in eventsandeye/.
- Design and structure: Amy Andrews, whose 2025 Astro redesign (originally myndrws/amybo) is the base of this repository, with its full history.
- Text: the site's own pages are written by Martin Currie with Claude; the pages marked
source: hugoare carried over verbatim from the previous Hugo/Docsy site (amy-bo/pages, private until it is archived), whose contributors are credited in LICENSE-CONTENT.md. - Homepage photo: Ozark Drones on Unsplash, edited.
Every page's frontmatter records where its text came from (source: amy | hugo | new). REVIEW.md lists every page with its source and confirms each old Hugo URL still works.
Requires Node.js 22 or later (.nvmrc). A project-local Node 22 is also installed as a dev dependency, so npm run scripts use it even where the system Node is older.
npm install
npm run dev # http://localhost:4321
npm run build # static site into dist/ (needs PUBLIC_TURNSTILE_SITE_KEY)
npm run build:test # same, with Cloudflare's Turnstile test key, for local testing
npm run check # astro check + type-check of the functions and cron worker
npm run review # regenerate REVIEW.md (after a build)
npm run test:a11y # axe-core accessibility scan of dist/ (after a build)
npm run test:e2e # full registration flow against a local D1 database (after build:test)
npm run test:forum # the build-time forum feed (src/lib/forum.mjs): live path with a fixture, fallback paths
node scripts/og.mjs # regenerate the Open Graph images after changing the logo or event titlePages live in src/content/docs/; the file path is the URL. Images go next to the page that uses them, or in src/assets/photos/ (AMYBO's own photos, with their sources listed there). See the contribution guidelines.
src/content/docs/ pages: Amy's section pages, projects/ (electroPioreactor, CARMA Hub, PioFlo, other hardware,
past projects), background/, collaborate/, events/, privacy, 404
src/content/talks/ talks for event pages (one markdown file per talk)
src/components/ Amy's Hero and Header overrides, footer, click-to-play YouTube embed, talks list, forum feed
src/lib/forum.mjs fetches forum.amybo.org/latest.json at build time; FORUM_FEED=off skips it, FORUM_FIXTURE=<file> stands in for the forum
src/assets/photos/ AMYBO photos from amy-bo/electroPioreactor (CC BY-SA 4.0), resized by Astro at build time
scripts/ og.mjs (Open Graph images), review.mjs (REVIEW.md), a11y.mjs (axe-core scan)
public/ robots.txt, og.png and og-event.jpg, _redirects
eventsandeye/ Events&I registration system (AGPL-3.0): server code, admin page, cron worker, schema, tests, Astro components
functions/ one-line Cloudflare Pages Functions wrappers that route to Events&I
seed/ the 13 November 2026 event, its sessions and first joining instructions
workers/cron/ wrangler config for the Events&I cron worker
public/_redirects every old Hugo /docs/ URL, redirected to its new home under /projects/, /background/ or /collaborate/
migration/hugo-map.json every Hugo file and where it went
The 13 November 2026 get-together page is src/content/docs/events/2026-11-13-london.mdx.
Adding talks. Copy src/content/talks/_example.md to a new file, fill in the title, speaker, affiliation, bio and optionally a headshot (put the image in src/content/talks/headshots/), and set published: true. Until at least one talk is published, the page shows "Programme to be announced".
How registration works is described in the Events&I README: double opt-in, holds, waiting lists that only move when an admin clicks Promote, self-service changes, calendar invitations and host lists. For this event:
- Places: 20 in person (remote unlimited), and 10 on each lab tour. The main registration deadline is 6 November, 12 noon; each tour can be booked or changed until it starts, while places remain. Change any of these on the admin page.
- Sessions: 10:30 and 11:15 lab tours (optional, booked separately), 12:00 and 14:00 talks (in person and on Google Meet), 17:00 pub. Add the two Meet links, hosts and final times on the admin page under Sessions; people whose calendar entries change get an update automatically, and nobody else is emailed.
- Calendars: in-person attendees get one entry for their day, remote attendees one per online talk session, each with reminders and add-to-calendar links.
- Hosts: a session with a host email gets the attendee list whenever it changes (names, plus emails of people who opted in to share them).
The automatic emails are: confirm your email; a reminder with joining instructions for anyone who registers twice; joining instructions with calendar invitations on confirmation or promotion; calendar updates only when someone's entries change; cancellation confirmation; notifications to the organisers' mailbox (NOTIFY_EMAIL in wrangler.toml); host lists. Editing a page or saving new joining instructions never emails anyone.
Admin page: amybo.org/admin/rsvps, behind Cloudflare Access. It shows counts and every registration, and lets you:
- change the in-person maximum, registration deadline and assumed travel time
- edit sessions: times, location, Google Meet links, hosts, capacities and tour booking deadlines
- promote people from waiting lists, or remove spam
- download a CSV, or copy a BCC list for sending from Gmail (always paste into BCC, never To or CC)
- save a new version of the joining instructions, which new registrants then receive
- email all confirmed registrants, or a filtered group (in person or remote, a tour, people on an older joining-instructions version), now or at a scheduled time, and cancel a scheduled email
- download the sent log as markdown: every joining-instructions version, who has which version, the sessions, and every message sent
Updating joining instructions without spamming anyone. Download the sent log and ask Claude to draft the next version plus a short "what's changed" message. Save the new version, then send the "what's changed" message to people on an older version, ticking counts as joining instructions version N. Everyone ends up with everything, and nobody gets the same information twice.
Data retention. A scheduled job deletes the event's registrations, their email records and host lists 30 days after the event ends. See the privacy notice.
Link-in-bio pages for AMYBO volunteers and supporters, and AMYBO's own at amy.bo/links. Each page is a plain-text twisty list with a purely graphical map of the same links beside it (above it on phones). The map is laid out once on the server and drawn as static SVG; links on it work without any script, and only opening a group (or tapping the map on a phone) loads public/link-assets/graph.js, which grows the group's items out of it with CSS transitions. Hovering an item in either lights up its counterpart, in CSS alone. Volunteers get a diary with highlights; supporters a "How I support" note.
- Code:
src/links/(model, renderer, icons, API, editor), routes infunctions/[page]/(/~name),functions/links/(/links),functions/api/links/andfunctions/admin/links/, the editor atsrc/pages/links/edit/.public/_routes.jsonlimits Functions to the paths that need them; keep it in step withfunctions/. - Data: D1 tables
lp_*andlink_events(db/links/). Clicks and views store only the page, the link and the time. - People: by invitation from
/admin/links/(behind Cloudflare Access). They sign in with an emailed single-use link; no passwords. - Images: uploads go to the R2 bucket
amybo-links(EU). The deploy binds it asLINKS_BUCKETonly if it exists, so uploads stay off until it is created. - amy.bo: the
amybo-redirectsWorker inworkers/amy-bo/keeps the short links and serves/~nameand/linksfrom the Pages project under the amy.bo address. The deploy workflow deploys it once the repository variableDEPLOY_AMYBO_WORKERistrue(the API token then also needs Workers Routes edit on the amy.bo zone). Until then, edit the Worker in the Cloudflare dashboard by pastingworkers/amy-bo/index.js. - Tests:
npm run build:test && npm run test:links(pages, clicks, invitations, sign-in, editing, diary, security checks and accessibility), andnode --test workers/amy-bo/index.test.mjs.
.github/workflows/deploy.yml deploys everything from GitHub Actions on every push to launch-2026 or main, so nothing has to run on a laptop. It has two jobs:
- build has no secrets at all: it installs, builds the site and hands it on as an artifact.
- deploy runs in the
productionGitHub Environment and installs without running package scripts before it touches the Cloudflare token. It creates the D1 database in the EU jurisdiction if it is missing (amybo-rsvp-eu), applies the migrations and the insert-only seed, sets the secrets, deploys the site with its Functions and deploys the cron Worker.
Only the Pages project's production branch touches the live database and the cron Worker, and only it deploys the registration Functions. Other branches get a preview of the static pages alone, with no Functions and no database binding, so preview code can never reach live registrations. The first push to main makes main the production branch, so merging the launch PR hands production over automatically.
One-off set-up:
- Cloudflare API token (My Profile → API Tokens → Create Token → Custom): Account: Cloudflare Pages: Edit, Account: D1: Edit, Account: Workers Scripts: Edit, for this account only. Copy it and the Account ID.
- Turnstile (dashboard → Turnstile → Add widget): hostnames
amybo.org,www.amybo.organdamybo-4p1.pages.dev(the project's own pages.dev address;amybo.pages.devbelongs to someone else), managed mode. - Resend: add the domain
amybo.orgin the EU region, add the DNS records it shows (proxy off), verify, create an API key with sending access only. - GitHub (repository → Settings → Secrets and variables → Actions). Secrets:
CLOUDFLARE_API_TOKEN,CLOUDFLARE_ACCOUNT_ID,TOKEN_SECRET(openssl rand -hex 32; never paste it anywhere else),RESEND_API_KEY,TURNSTILE_SECRET_KEY. Variables:PUBLIC_TURNSTILE_SITE_KEY;SITE_URL(the address emails link to:https://amybo-4p1.pages.devuntil amybo.org is attached, thenhttps://amybo.org);ACCESS_TEAM_DOMAINandACCESS_AUDonce step 6 below is done. - Environment (Settings → Environments →
production): add yourself as a required reviewer and restrict deployment branches tolaunch-2026andmain. Every deploy then waits for an approval from a two-factor-authenticated GitHub account. - Cloudflare Access for the admin page, with two-factor sign-in: see step 6 of the manual set-up below. The admin page refuses any sign-in whose token does not report a second factor.
These accounts can reach every registrant's details, so each needs two-factor authentication:
| Login | Why | Where to turn it on |
|---|---|---|
| Cloudflare (every member of the account) | D1 database, Pages secrets, Access policy | My Profile → Authentication → Two-factor; Manage Account → Members → require 2FA |
| GitHub (amy-bo organisation) | pushes deploy to the live database | Organisation settings → Authentication security → Require two-factor authentication |
| Resend | delivery logs list every recipient | Settings → Account → Two-factor authentication |
| Google account for hello@amybo.org | receives organiser notifications and BCC exports | Google Account → Security → 2-Step Verification |
| The admin page itself | every registration | Cloudflare Access policy with MFA and a session of 12 hours or less; enforced in code as well (second factor, ADMIN_EMAILS, ACCESS_MAX_AGE_HOURS) |
You need a Cloudflare account with the amybo.org zone (already there, since DNS is on Cloudflare), and admin rights on the amy-bo GitHub organisation.
-
Create the database.
npx wrangler login npx wrangler d1 create amybo-rsvp-eu --jurisdiction eu
Put the printed
database_idinto bothwrangler.tomlandworkers/cron/wrangler.toml, commit, then create the tables and the 13 November event:npx wrangler d1 migrations apply amybo-rsvp-eu --remote npx wrangler d1 execute amybo-rsvp-eu --remote --file seed/2026-11-13-london.sql
-
Connect GitHub. In the Cloudflare dashboard: Workers & Pages → Create → Pages → Connect to Git. When GitHub asks, install the Cloudflare Pages app on the amy-bo organisation and give it access to only the
websiterepository. Chooseamy-bo/website, production branchmain, framework preset Astro, build commandnpm run build, output directorydist. Add the build variableNODE_VERSION=22. Pages reads the D1 binding and variables fromwrangler.toml. On this route every preview would also get the Functions and the live database binding, so set Settings → Builds → Branch control → Preview branches: None, or use the GitHub Actions route above, which deploys previews as static pages only. -
Turnstile. Dashboard → Turnstile → Add widget for
amybo.org(andamybo-4p1.pages.dev, the project's own pages.dev address), managed mode. Add the site key as the Pages build variablePUBLIC_TURNSTILE_SITE_KEY(the build fails without it, so a test key can never reach the live site), and the secret key as the secretTURNSTILE_SECRET_KEY. -
Resend. In Resend, add the domain
amybo.org. Resend shows a few DNS records (an MX and a TXT record on asendsubdomain, and a DKIM TXT record). Add them in Cloudflare DNS with the proxy off (DNS only), then click Verify in Resend. These records sit on subdomains, so the existing Google mail for the domain keeps working. Create an API key with sending access only. -
Secrets. In the Pages project → Settings → Variables and secrets, for Production (and Preview if you use previews):
Secret Value TOKEN_SECREToutput of openssl rand -hex 32; keep it stable, because changing it breaks every link already emailedRESEND_API_KEYthe Resend key TURNSTILE_SECRET_KEYthe Turnstile secret key Never set
DEV_MODEon Cloudflare. -
Cloudflare Access for the admin page, with two-factor sign-in. Zero Trust → Access → Applications → Add → Self-hosted. Add these destinations:
amybo.org/admin/*,amybo.org/api/admin/*,www.amybo.org/admin/*,www.amybo.org/api/admin/*,amybo-4p1.pages.dev/admin/*,amybo-4p1.pages.dev/api/admin/*,*.amybo-4p1.pages.dev/admin/*and*.amybo-4p1.pages.dev/api/admin/*. Add a policy Allow for the admins' email addresses, and turn on Require multi-factor authentication for the policy (Access's own MFA: an authenticator app or security key), or use an identity provider that enforces MFA. The email one-time PIN alone is not a second factor. With Cloudflare's per-application MFA (the application's Authentication → MFA → Customize MFA settings → Authenticator application; users enrol at<team>.cloudflareaccess.com/AddMfaDevice, which needs the App Launcher enabled for them), Access asks for the authenticator code but its tokens still report onlyonetimepin, so setACCESS_MFA = "application"inwrangler.tomland keep that application setting on. Otherwise the admin page checks the token's methods itself and names them when it refuses; other method names can be accepted withACCESS_MFA_METHODS. Set the application's session duration to 12 hours or less: the admin page refuses a sign-in token older thanACCESS_MAX_AGE_HOURS(default 12), and its refusal links to/cdn-cgi/access/logoutto sign in again. The code also admits only the addresses inADMIN_EMAILSinwrangler.toml(nowhello@amybo.org), whatever the Access policy allows, so add any further admin there as well as in the policy. Save, then copy the application's Audience (AUD) tag intoACCESS_AUDinwrangler.toml, and your team domain (Zero Trust → Settings → Custom pages, e.g.amybo.cloudflareaccess.com) intoACCESS_TEAM_DOMAIN. Commit and push. The functions refuse admin requests without a valid Access token even if the Access policy is misconfigured. -
Cron Worker. Deploy the scheduled-jobs Worker and give it the same secrets:
npx wrangler deploy --config workers/cron/wrangler.toml npx wrangler secret put TOKEN_SECRET --config workers/cron/wrangler.toml npx wrangler secret put RESEND_API_KEY --config workers/cron/wrangler.toml
-
Test on the preview URL (
https://amybo-4p1.pages.dev): register with your own email, confirm, change and cancel; open/admin/rsvps/and check it asks you to log in.
Local development of the functions: copy .dev.vars.example to .dev.vars, fill in TOKEN_SECRET, then npm run build && npm run db:migrate:local && npm run pages:dev and open http://localhost:8788. Emails, including calendar attachments, are printed to the console and stored in a local outbox instead of being sent.
- Steps 1 to 8 above done, and the preview site checked page by page against the live site (REVIEW.md lists every page).
- Event page facts confirmed: times, room and entrance, in-person maximum, tour capacities and registration deadline (set on the admin page).
- Joining instructions version 1 checked on the admin page, including the Google Meet link and room details (or a note that they will follow).
- Resend domain shows Verified; a test registration email arrives and is not in spam.
- Gate, not a tick-box: calendar invitations checked with real sends to a Gmail and an Outlook address. The invitation shows as an invitation (check the raw message has
Content-Type: text/calendar; method=REQUEST), adds to the calendar, and a session time change produces an update rather than a duplicate. If Outlook only shows a file, tell andeye before opening registration. - Session hosts, the two Google Meet links and final times entered on the admin page.
- In Pages → Custom domains, add
amybo.organdwww.amybo.org. Cloudflare replaces the existing DNS records that point at Netlify. Wait for the certificate to show Active. - Check https://amybo.org, a few old URLs (for example
/docs/overview/,/docs/pioflo/pioflo-v0.01/,/about/), the event page, registration end to end, and that/admin/rsvps/needs a login. - In Netlify, remove the custom domain from the old site, then delete or disable the site so it stops building.
- Archive the
amy-bo/pagesrepository with a README note pointing toamy-bo/website. - Update links that point at the old repository (forum, YouTube descriptions, GitHub organisation profile).
Rollback: in Cloudflare DNS, point amybo.org back at the Netlify site (the Netlify site keeps working until it is deleted), so do not delete it until the new site has run cleanly for a while.
Done work is logged in CHANGELOG.md; what remains is in TODO.md.
Website code MIT (LICENSE); Events&I in eventsandeye/ AGPL-3.0; page text CC BY 4.0; AMYBO photos and images CC BY-SA 4.0. Details in LICENSE-CONTENT.md.