Skip to content

How the WL side works

The WL pair — a Next.js app and a Laravel API on 178.128.113.107 — serves every customer event. Which event a request belongs to is decided by its Host; see Domain resolution for that half. This page covers what happens afterwards.

The runtime

Apache terminates TLS and does not send X-Forwarded-Proto. That has bitten this codebase before: building a URL from req.url yields the proxy's internal origin, so redirects from middleware went to http://localhost:3000. The public origin is derived from the Host header instead — see publicOrigin() in middleware.ts.

Middleware runs on the Edge runtime, so it uses plain fetch() and cannot import the React-cache helpers in lib/domain.ts. That is why the host cache is duplicated there rather than shared.

Rendering a page

If resolution produced nothing, the layout catches DomainNotConnectedError and renders the "event not connected" page rather than a 500.

Pages are data

Most of what a participant sees is a block tree built in the admin page builder and stored per page. The frontend has a renderer per block type — currently around 84 of them — and draws whatever the tree contains.

Consequences worth internalising before changing anything here:

  • A new block type needs a renderer on both sides. The admin's catalogue and the frontend's registry are separate lists, and they drift.
  • Container types must be wired into both recursions. A new container missing from either ContainerChildren or ColumnsChildren makes its nested contents vanish from the tree view.
  • Blocks using hooks need "use client". Omitting it fails at build, not at review.
  • CSS injected through dangerouslySetInnerHTML must not contain a backtick — not even inside a comment. It terminates the template literal and breaks the build with an error nowhere near the cause.

Tokens

Copy is written with tokens that resolve from real configuration — {{event.name}}, {{share_text}}, {{total_raised}}.

Unresolved tokens render empty, not loudly

There is no error. You get a sentence with a hole in it, on a live event. Tokens must resolve from actual configuration rather than hardcoded fallbacks, and blocks should be gated on data presence so a block with nothing behind it hides instead of rendering an empty shell.

Custom HTML is client-only

The custom_html block is sanitised with DOMPurify client-side only — there is no server rendering of raw HTML. Its CSS is scoped under cb_<blockId>, and scripts are stripped.

Anything that needs JavaScript has to be a native block. That is precisely why blocks like early_bird_slider exist rather than being pasted-in HTML.

Caching

CacheTTLNotes
Host → event id60sMiddleware, in memory
Event config, pages, meta60slib/serverCache, per event
/resolve-event response60sCache-Control header

A hard reload sends Cache-Control: no-cache, which bypasses the server TTL cache. A normal reload does not — so an editor checking their change should hard-reload rather than conclude nothing saved.

The API

Laravel, at wl-api.togoparts.com, mostly public read endpoints plus the authenticated participant surface.

Two rate-limiting notes, both learned the hard way:

  • /resolve-event uses the public-read limiter, not the default api one. At 60/minute a single active visitor exhausted it, and the 429s surfaced as the not-connected page on live events.
  • Client error reports are tightly limited per IP; a real browser sends at most one per crash.

Participant authentication layers over the legacy Togoparts user table, verified read-only. Signup provisions a tgp_userid. See Legacy app and TGP.

One deployment, every event

The thing to keep in mind whenever you change this codebase:

There is no gradual rollout

One process serves every live event. A change to tga-v3-wl-web reaches all of them simultaneously. Anything conditional on configuration must default safely for events that never set it — including events that were configured a year ago and are still running.

Organiser guide and developer documentation for the TogoActive platform.