Full design · Business
Event landing
Halyard, a three day ideas and culture festival on the Fremantle wharf: talks, workshops and live sets across four rooms over a Friday evening and a full weekend.
Live preview. Click around, it works.
About this template
Halyard, a three day ideas and culture festival on the Fremantle wharf: talks, workshops and live sets across four rooms over a Friday evening and a full weekend. Visitors browse the programme with filters by day, room and track, build a personal agenda that warns them the moment two chosen sessions overlap in time, and export it as a calendar file. Speaker pages carry a bio, their sessions and a room map. Tickets run a demo checkout labelled as a demo, three tiers with a live running total and a promo code. The venue screen carries a room by room map, getting there and accessibility notes. Organisers get their own side behind a Viewing as switch: a programme and speaker editor that adds, edits and retires sessions with clash detection of its own, a ticket sales dashboard with revenue by tier and a day by day chart, and a check in screen that looks a ticket up by code and marks it used once, never twice.
What the agents are asked
The first message sent when you use this template. You can edit the plan before anything is built.
Adapt Halyard to my own conference or festival. Keep both sides and every working rule: the attendee side (Home with the headline and dates, Programme with search, day, room and track filters, a personal agenda that flags a clash the moment two chosen sessions overlap and exports it as a calendar file, Speakers with bios, sessions and a room map, Tickets with a demo checkout, tiers, a running total and a promo code, Venue with a room by room map and getting there notes) and the organiser side (Sessions and speakers editor with its own clash check, Ticket sales dashboard with revenue by tier and by day, Check in that looks a ticket up by its code and marks it used exactly once). Change the brand, colours, dates, rooms, tracks, sessions, speakers and ticket tiers to mine. Keep the sample data marked Sample and removable in one step, keep every payment and AI result labelled as a demo, and never state a figure for a real government scheme. When I want real accounts and ticket sales, wire in the tables and functions in schema.sql as the README describes, with every secret key kept on the server.
A three day ideas and culture festival in a converted wharf shed in Fremantle, WA. Attendees browse the programme, build a personal agenda that flags the moment two chosen sessions clash, buy a pass through a demo checkout and export their agenda as a calendar file. Organisers get their own side to edit the programme, watch ticket sales and check attendees in at the door.
Built on the NovaBuild template kit (docs/guardrails/TEMPLATE-KIT.md): one CONFIG block, one design token block, and a shared data layer that runs in demo mode with no backend or against a real Supabase project once one is connected, with no code change either way.
Screens
Attendee side:
- Home (
#/): the hero, a preview of the next few sessions, a speaker preview and a venue teaser. - Programme (
#/programme): search, filter by day, room and track, grouped by day. Every session carries a flag to add it to your agenda. - Session (
#/programme/<slug>): full details, its speakers and room. - Speakers (
#/speakers,#/speakers/<slug>): every speaker, and a detail page with their bio and their sessions. - Agenda (
#/agenda): the sessions you have flagged, any clash between them, and an export to a calendar file. - Tickets (
#/tickets): a demo checkout: choose a pass, see the total update live as you change the quantity or add a promo code, then a demo confirmation with each ticket's code. - Venue (
#/venue): the address, each room, getting there and accessibility notes.
Organiser side ("Viewing as: Organiser"):
- Overview (
#/organiser): sessions, speakers, tickets sold and revenue at a glance. - Programme editor (
#/organiser/programme): add, edit and remove sessions and speakers, assign speakers to a session, with its own check for two sessions clashing in the same room. - Sales (
#/organiser/sales): revenue by tier, sales over the last 14 days, recent orders. - Check in (
#/organiser/check-in): look a ticket up by its code and mark it used. A ticket can only be checked in once.
In this demo, "Viewing as: Organiser" is a preview toggle: the kit's demo mode has no real organiser accounts. Wire in is_organiser() (schema.sql) once you connect a real backend, and it scopes to whichever signed in user actually has an organisers row (see "Going live" below).
Signature function: the personal agenda
Adding a session to the agenda runs its flag up a small mast (identity.moment, "the hoist"). The instant a second chosen session overlaps it in time, in any room, both flags dip to half mast and the two cards show why. agendaClashesFor (script.js) recomputes this fresh from the current agenda every time a session is added or removed, so the warning always matches what is actually flagged. Exporting the agenda builds a plain .ics file in buildAgendaIcs: the Agenda screen shows the exact text it contains before offering a real download, so its correctness can be read on the page rather than only discovered after a file lands on disk (TEMPLATES.md, "never trigger a real download while testing").
The agenda itself is not a database table. It lives in the same local, try/catch wrapped preference object as the visitor's theme, because it is private browsing state: building an agenda never needs an account, and nothing about which sessions you are considering ever reaches the server. The trade-off is that an agenda does not follow a visitor between devices; wiring that in would mean an agenda_items table keyed on auth.uid() and a sign in step before Programme, which this build deliberately does not ask a browsing visitor for.
Data model (schema.sql)
rooms, speakers, sessions, session_speakers, ticket_tiers, promo_codes, orders, tickets, organisers. Row level security on every table:
rooms,speakers,sessions,session_speakersandticket_tiersare publicly readable and writable only by an organiser (is_organiser()).promo_codesandorganiserscarry no select grant for anyone: a promo code's discount is only ever read from insideprice_orderandsubmit_order, and the organiser roster is only ever read from insideis_organiser(), so neither can be listed by querying the table directly.ordersandticketsare never written directly. Buying a pass runs throughsubmit_order(anonmay call it: a guest checkout, no account needed), which recomputes the price fromticket_tiersandpromo_codesitself rather than trusting a total the browser sent (GENERATED-APPS.md, "never trust the client"), and locks the tier row (select ... for update) before checking itscapacityagainst how many tickets are already sold, so two buyers racing for the last ticket of a capped tier cannot both succeed. Checking a ticket in runs throughcheck_in_ticket, which makes the update itself the guard (where used_at is null) rather than a separate check before it, so two scanners racing the same code cannot both admit it; the same code called twice never checks a ticket in twice, proved by calling it twice on the same seeded code during this build and, for the race itself, by holding one call open in a transaction while a second call runs concurrently (LESSONS.mdL35's pattern: a database review found the race, the same way it found L35's original upsert). Neither function hard codes a time zone: a timestamp always leaves the database as plain ISO text (incheck_in_ticket's returnedused_at, never formatted in SQL, since the database's own zone on Supabase is UTC, not the event's) and the client renders it withCONFIG.locale.timeZone.
Sample data
Every room, speaker, session, ticket tier, promo code, order and ticket is marked is_sample: true and carries SAMPLE_VERSION (script.js). The festival is always seeded in the future relative to whenever the page loads or the schema is applied, so every seeded ticket's used_at is null: nobody has been checked in yet, because nobody could have been. A returning visitor's own orders and tickets (bought through a real checkout) are never sample rows, so they always survive a version bump; only the sample cast gets replaced.
Going live
- Run
schema.sqlon a Supabase project (rehearsed first withtools/rehearse-template-schema.mjs, never applied directly). - Set
CONFIG.backendinscript.jsto{ mode: 'supabase', url: '<project url>', anonKey: '<anon key>' }. The anon key is public by design; row level security is the actual boundary. Never put aservice_roleorsb_secret_...key here; the kit refuses to start with one and the checker refuses to publish a template that contains one. - Add an organiser by inserting a row into
organisers(user_id, the person's realauth.usersid) once they have signed up. There is no self service "become an organiser" screen, on purpose: ticket revenue and check in are sensitive enough that access should be granted by an admin, not requested by anyone who signs in, unlike some of NovaBuild's other templates that do let a signed in visitor create their own staff profile. - Connect Stripe for the real checkout:
submit_orderis the one place a charge belongs, right after its pricing is computed and before the order is inserted. Today it only ever inserts ademo_paidorder. - Everything the customer would change (brand, contact details, locale, which screens are on) comes from
CONFIGat the top ofscript.js. Colours, fonts, radii and spacing come from thenova:tokensblock at the top ofstyles.css.
What is not built yet
- Real payment capture (see "Going live" above): every order today is
status: 'demo_paid'. - A promo codes admin screen: codes are seeded in
schema.sqland read only from inside the pricing functions; adding or editing one today is a direct database edit, the same as adding an organiser. - An agenda that follows a signed in visitor across devices (see "Signature function" above for the trade-off this build made instead).
Honesty
Halyard and every person, session and order in it are fictional, for demonstration only. This template was not modelled on any specific existing event platform or ticketing product.
Art direction
One shoot, one grade: warm, late afternoon and evening light on a converted wharf shed, real documentary photography of real, varied people in real spaces, tight or asymmetric crops with negative space for a headline, no logos, no readable text or lettering on any flag, banner or screen in frame. The one video (hero-loop.mp4) is a locked off, static camera shot of the same hall as hero.webp, with only the bulb lights, haze and the seated audience's own small movement, so it reads as one continuous photograph rather than a separate piece of footage.