The technical specification for ace.co.nz. What it is built with, what it talks to, where its email comes from, and where the code lives. Written so somebody who has never seen this project can pick it up.
The short version
ace.co.nz is a static website. Every page is built into plain HTML ahead of time and served as a file, which is why it is fast and why there is very little that can break at the moment a customer visits.
The pages are built from two sources. Course listings, dates and prices come from Administrate, which stays ACE's system of record. Page wording comes from Sanity, where Tracey edits it. The site is rebuilt every morning and every night so both stay current.
The only parts that run live, at the moment somebody clicks, are the forms, the booking and payment steps, the course advisor and this portal. Everything else is a file.
What it is built with
| Layer | What we use | Why it is that |
|---|---|---|
| The site | Astro 7, a static site builder. Node 22.
package.json, astro.config.mjs |
Builds 419 pages to HTML in about three minutes. No database call and no server work when a customer opens a page. |
| Hosting | Cloudflare Pages, project ace-training, in ACE's own Cloudflare account.
ace-training.pages.dev, ace.co.nz, demo.ace.co.nz |
ACE own the account, so nothing here depends on Rove keeping a login. DNS for the ace.co.nz zone sits in the same account. |
| The live bits | Cloudflare Pages Functions, in functions/api/.
enquiries, bookings, payments, digests |
Small pieces of code that run on request. They are the only server-side code in the website itself. |
| Page wording | Sanity content studio, project kvftx3ne, dataset production.
ace-training-nz.sanity.studio |
So ACE change their own words without a developer. Sanity always beats the wording committed in the code, which is the thing people get wrong first. |
| Course data | Administrate, read over its GraphQL API at build time.
api.getadministrate.com/graphql |
Administrate remains the single source of truth for courses, dates, places and prices. The website never decides any of them. |
| Database | Supabase project "ACE and Project Laneways", reference noonadzhstgxncsdobzj, in ACE's name.
schemas: ace, portal, project_laneways |
Holds what the website learns and Administrate has nowhere to put: unfinished bookings, 404s, course advisor questions, how people heard about ACE, and this portal. |
| This portal | Plain HTML on Cloudflare Pages project ace-portal, signed in against Supabase.
portal.ace.co.nz |
Members only, four people. No framework, so it stays readable to anyone. |
| Builds and jobs | GitHub Actions, five workflows.
.github/workflows/ |
Building and deploying is not done by hand. Every scheduled job is listed on What runs automatically. |
There is no WordPress, no plugin list, no separate CMS licence and no server to patch. The bill for the site itself is Cloudflare, Sanity and Supabase, all on free or low tiers today.
The code, and where it is
Two repositories on GitHub, both under the account JPmack23. Everything to do with the website is in the first one, including this portal.
| Repository | What is in it | Branches that matter |
|---|---|---|
| JPmack23/ ace-website |
The website, the booking and payment code, the Supabase edge functions, this portal, and every migration and checking script.
src/ pages and components · functions/api/ live endpoints · worker-lib/ shared booking, email and Administrate code · supabase/functions/ edge functions · portal/ this portal · migration/ one-off and repeatable checks · .github/workflows/ the scheduled jobs |
master is the site, and the only branch that ships.portal-login is this portal.heard-dashboard is the dashboard work.The practice site is not a branch: the same code is deployed to Cloudflare under the label demo. |
| JPmack23/ ace-studio |
The Sanity studio: the shape of every editable field and the studio Tracey signs into. React and Sanity 3.
deployed with npx sanity deploy |
master |
ace-website: README.md is the architecture, CLAUDE.md is the rules and the traps that have already cost time, and NEXT-SESSION.md is the current state of play. They are kept current deliberately, because this project has been built across many short sessions and the written record is the only thing that carries between them.
The four addresses
| Address | What it is |
|---|---|
| ace.co.nz | The live website. Real customers and real payments. |
| demo.ace.co.nz | The practice copy. The same site, rebuilt for checking before anything reaches customers. It deliberately cannot send email or take a payment, so a booking made here does nothing. |
| portal.ace.co.nz | This portal. Members only. |
| old.ace.co.nz | The previous website, still running on its original server. It is the way back if it is ever needed, and it is hidden from search engines. |
Code only ever reaches any of these through GitHub Actions, which builds the site and hands it to Cloudflare. There is no other path, and nothing deploys from anyone's laptop.
The systems it talks to
Every outside service the website uses, what it is for, and who owns the account. The account owner is the important column: it says who can still get in if the person who set it up is not available.
| Service | What it does here | Account |
|---|---|---|
| Administrate | Courses, dates, places, prices, registrations. Read when the site builds, written to when somebody books. Authenticated with a client id and secret over OAuth.
api.getadministrate.com/graphql · auth.getadministrate.com · ADM_ID, ADM_SECRET, ADM_ACCOUNT_ID, ADM_WRITE_ENABLED |
ACE |
| Sanity | Page wording, hero text, images and the eight Insight articles. Read when the site builds. The dataset is public to read, so no key is needed to display it.
project kvftx3ne, dataset production · SANITY_PROJECT_ID, SANITY_DATASET, SANITY_TOKEN |
Rove, for ACE |
| Stripe | Card payments, and the webhook that turns a successful payment into an Administrate registration.
api.stripe.com · js.stripe.com · STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET |
ACE |
| Supabase | The database, the five edge functions, and sign-in for this portal.
noonadzhstgxncsdobzj.supabase.co |
ACE |
| Resend | Sends every email the website sends. See the next section.
api.resend.com · RESEND_API_KEY |
ACE |
| Anthropic | The Course Advisor. One model call per question, with the whole ACE catalogue in front of it so it can only recommend real courses.
api.anthropic.com · claude-haiku-4-5 · ANTHROPIC_API_KEY, held on the Supabase function and not in the website |
Set up by Rove |
| Cloudflare Turnstile | The anti-spam check on all four forms. Checked on the server, so a form that skips it is refused.
challenges.cloudflare.com · TURNSTILE_SECRET |
ACE |
| Mailchimp | Marketing list. Only people who tick the box are added, and the decision is made on the server so a form cannot subscribe anybody by accident. Starting a booking no longer writes to it.
MAILCHIMP_API_KEY, MAILCHIMP_LIST_ID |
ACE |
| PD Training | Their public schedule is read once a week to find dates Administrate does not have. Read only, and it writes nothing anywhere.
pd-training-dates.yml · migration/pd-training-load.mjs loads them by hand |
ACE |
| Google Tag Manager and GA4 |
Analytics. GTM container GTM-52Z75C5, which loads GA4.
googletagmanager.com |
ACE |
| Microsoft Clarity | Heatmaps and session recordings, used to decide what to change on the busiest pages.
project ybdi1v9jkz |
ACE |
| LiveChat | The chat widget. Licence 10204607.
cdn.livechatinc.com |
ACE |
| GitHub | The code, and the scheduled jobs that build and check the site.
github.com/JPmack23 |
Rove |
public/_headers lists every allowed address, and anything not on the list is blocked before the request leaves the browser. So adding a new service means adding it to that policy too, and the change has to be checked in a real browser, because command-line tests ignore the policy.
Where the emails come from
Every email the website sends goes through Resend on ACE's own account, from the sending domain send.ace.co.nz. Nothing is sent from a Rove address, and no email is sent from the practice site at all.
| Sent from | Who gets it | |
|---|---|---|
| Enquiries, in-house requests, contact forms | ACE website <enquiries@send.ace.co.nz>, replies go to the customer |
info@ace.co.nz |
| New booking, and ACTION NEEDED when a booking cannot hold a place | ACE website <enquiries@send.ace.co.nz> |
bookings@ace.co.nzRove, always copied |
| Booking acknowledgement to the customer | ACE Training <enquiries@send.ace.co.nz>, replies go to bookings@ace.co.nz |
The person who booked, and each attendee |
| Course confirmation to the learner | Administrate, not the website. It sends ACE's own template, from ACE's own address, and it lands in the customer's history in Administrate | The learner |
| Daily and weekly checks: unfinished bookings, pages not found, the Administrate check, PD Training dates | ACE Training <enquiries@send.ace.co.nz> |
See What runs automatically |
| Sign-in codes for this portal | ACE Training <noreply@send.ace.co.nz>, through Supabase using the same Resend account |
The person signing in |
Addresses are settings, not code. Changing who gets what is a settings change on the Cloudflare Pages project followed by a rebuild, because Cloudflare fixes settings into the site at the moment it is built. The settings are ENQUIRY_TO, BOOKINGS_NOTIFY_TO, BOOKING_ALERT_TO, OPS_ALERT_TO and ENQUIRY_FROM.
Email authentication, and why it matters
ACE publish a strict DMARC policy on ace.co.nz: any mail claiming to be from ACE that cannot prove it is genuine gets rejected, not filed in junk. That is the right setting, and it means the website's own email has to prove itself properly or it simply would not arrive.
It does. All three checks pass on real delivered mail:
- SPF passes, from the record on
send.send.ace.co.nz. - DKIM passes, signed as
send.ace.co.nz. - DMARC passes, under
p=reject; sp=rejectwith strict alignment.
Checked 16 September against the message headers of a booking notification actually delivered on 15 September, rather than against the settings screen. The sending mail is handled in Resend's Tokyo region, and the DNS records that authorise it live in the ace.co.nz zone in ACE's own Cloudflare account.
What the database holds
One Supabase project, three schemas. It is ACE's project, holding both of the client's businesses. Nothing in it duplicates Administrate.
| Where | What is in it |
|---|---|
schema ace |
booking_starts, booking_completions and booking_attempts: who started a booking and what happened. heard_about: the answer to "how did you hear about us", kept with no name attached. not_found_hits: addresses visitors could not find. advisor_queries: every Course Advisor question and answer, with no name, no email and no IP address. publishes: every publish of the live site from this portal, and who pressed the button. |
schema portal |
This portal: tasks, files, courses, levels, redirects, clients and members. Documents sit in a private bucket and are opened with links that expire after five minutes. |
schema project_laneways |
The client's other business. Same project, separate schema, deliberately kept apart. |
Every table has row level security switched on, and the portal tables are members only: the key that sits in this page reads nothing on its own. Five edge functions run on the same project: ace-course-advisor, ace-booking-log, ace-dashboard, ace-not-found and ace-publish.
How a change reaches the live site
- Wording is edited in Sanity, then Show me my changes on this portal builds the practice site so it can be checked, and Publish to ace.co.nz builds the real one. Six minutes each.
- Code goes to the
masterbranch on GitHub. A push builds the practice site only. The live site is built by the 9:56pm job, or on demand by pressing Publish. - Every build refuses to deploy a half-built site: fewer than 200 course pages and it stops. That check exists because an Administrate outage once let a nearly empty site reach the web.
- Course dates and prices are refreshed by the build itself, so the live site is never more than a day behind Administrate.
Going back is one DNS change. The old website is still running at old.ace.co.nz on the original server, and pointing ace.co.nz back at it takes under a minute.
If you are the developer picking this up
Clone the repo, read README.md, CLAUDE.md and NEXT-SESSION.md, then:
npm install
npx astro build # needs adm-creds.json: {"id":"...","secret":"..."}
Without Administrate credentials the build stops on a 401, which is expected and still catches mistakes. Add a sanity-creds.json holding the project and dataset and no token, and every non-course page builds locally, because the dataset is public to read.
Run these before any deploy
node migration/verify-build.mjs # refuses a half-built site
node migration/search-check.mjs # 50 search checks
node migration/consent-check.mjs # nobody subscribed by accident
node migration/filter-audit.mjs # every filter on every topic page
node migration/verify-redirects.mjs https://ace.co.nz # all 440 old addresses
node migration/bookable-check.mjs --host https://ace.co.nz
bookable-check is the one that asks the customer's question rather than the data's: does every Book button, on every page, lead to a charge at the advertised price.
Before changing anything to do with bookings
node migration/adm-request-audit.mjs # every request shape against Administrate
node migration/test-booking-refusal.mjs # the refusal path, not just the happy one
The four traps that have cost the most time
- Any
first:in an Administrate query without a cursor loop is a bug. One of them silently hid 169 courses, 45% of the catalogue, for weeks. - Sanity beats the wording in the code. Edit a fallback in a file and the page will not change if Sanity fills that field.
- Cloudflare fixes settings into the site at build time. Changing a setting does nothing at all until the next build, and production and the practice site hold separate copies of every one.
- Check the page a person sees, not the markup. Several faults passed every automated structural check and were obvious in one screenshot.
The full list is in CLAUDE.md and is worth twenty minutes before touching anything.
Keys and passwords
No key is on this page, and no key is in the code. They are held in three places: Cloudflare Pages project settings for the website (14 of them on production, fewer on the practice site, which is why the practice site cannot send email or take a payment), GitHub repository secrets for the scheduled jobs, and Supabase function settings for the edge functions. Anyone with access to ACE's Cloudflare and Supabase accounts can read or replace all of them without Rove.
Written 16 September 2026. Every fact on this page was read off the system that holds it on that day, not copied from an earlier document. If a service, an address or a schedule changes, change it here at the same time.