Issue 2025September 2025Live

Formula 4 · Indian Championship · 2025

Chennai Turbo Riders, on the feed.

Cover story · p.5

The team's public site rebuilt as an onboard broadcast: video under scanlines, readouts in the corners, a grid walk of driver posters and a calendar that counts down to the next round. Behind it, on its own hostname, the console that edits every word of it without a deploy.

  • One build, two hosts
  • Content in Postgres
  • Media on object storage
  • Android via Capacitor
Tanjiro (Demon Slayer)
chennaiturboriders.in

A Formula 4 team's public site and the console that edits it, in one deployment

Contents

  1. 4By the numbers2 front doors, one process, 25 versioned migrations
  2. 5OverviewA brochure became a broadcast.
  3. 7Two front doorsSame build, two hostnames.
  4. 9Under the hoodWhat a save sets in motion.
  5. 13Key screensLights out to chequered flag.
  6. 19TracksideIn the grandstand.
  7. 21Stack and rulesWhat holds it on the track.
  8. 23More screensThe rest of the paddock.
  9. 26Back coverWant a site that runs like a race weekend?
Tanjiro (Demon Slayer)
  • Onboard feed
  • Grid walk
  • Round countdown
  • One build · two hosts
  • Content in Postgres
  • Media on object storage
  • Revalidate only what changed
  • Racing yellow on carbon

№By the numbersp.4

  1. 2Front doors, one process
  2. 25Versioned migrations
  3. 7Console modules
  4. 0Deploys to change a headline
Tanjiro (Demon Slayer)

01Overviewp.5

A brochure became a broadcast.

The first version was a set of hand-built pages reading a checked-in JSON file. Changing a headline meant a commit and a deploy, and only somebody with the repository could do it. This rebuild keeps the F4 design language, racing yellow on carbon, and moves every word into Postgres behind a console.

The home page opens on the team's onboard video under scanlines and grain, framed by the readouts a broadcast puts in the corners. The three words of the name wipe in one at a time, the season's numbers count up along the baseline, and the drivers arrive as a grid walk: on a wide screen the section pins itself and the posters travel sideways as the page scrolls.

The console is not a rewrite. Every screen, the media library, the rich-text editor, the role model and the S3 pipeline are ported from the CTR Unified platform, so they arrive already working. What is new is the branding, the single-site shape, and the wiring from those tables into this site's own pages.

Tanjiro (Demon Slayer)

Sticker sheet peel & apply

  • Onboard feed hero

    Video at full strength under scanlines and grain, corner readouts, the name wiped in word by word, numbers counting up.

  • Grid walk

    Driver posters pinned and travelling sideways with the scroll; a snapping row on a phone and under reduced motion.

  • Round countdown

    The calendar and the home page read the same published season, so they can never disagree about the next round.

  • Two front doors

    The Host header decides: the admin hostname gets the console at its own root, every other host gets the site and nothing else.

  • Media library

    Folders on Neon object storage, browser-side WebP conversion, presigned PUTs for video, and a usage scan before any delete.

  • Surgical revalidation

    Saving an article clears the index, the home band, the sitemap and both the new and the old address after a rename.

02Two front doorsp.7

Same build, two hostnames.

One deployment, one process, one database connection. The middleware reads the Host header and mounts the console at the admin hostname's root; the mount folder itself 404s on every host, so the public domain carries no admin surface at all.

The site

chennaiturboriders.in/schedule

Server components reading one memoised query per request, the F4 design kept, every page cached for a minute and cleared the moment its content changes.

  • Home, team, drivers, calendar, news, partners
  • Rajdhani display face, JetBrains Mono readouts
  • Session cookie is host-only and never reaches this domain
  • robots and sitemap built from SITE_URL

The console

admin.chennaiturboriders.in/site/ctr/events

A monochrome tool palette in IBM Plex Sans, so the tool never reads as part of the thing it edits. One bright colour: the yellow of the action that writes.

  • Team & site, drivers, sponsors, circuits, season, articles
  • Owner and member roles with per-module grants
  • Live previews drawn by the site's own render layer
  • Accounts, media and co-admins

Shared by both

  • one Next.js build
  • one process
  • ctr.* schema on Neon
  • MEDIA_PREFIX on one bucket
  • ctr.slugs: one address per thing
  • revalidatePath after every write
  • ADMIN_HOSTS read at build time
  • no domain in the source

03Under the hoodp.9

What a save sets in motion.

Three chains the console runs many times a week: publishing an article, uploading a photograph, and keeping an address alive after a rename.

Part 1

Publish

From the editor to the home page

  1. Write in the rich-text editor

    TipTap with headings, lists, links, images from the media picker, normalised to a bounded document before it is stored.

  2. Set the byline

    Category, author and tags: the two columns this deployment added to the shared article schema so the pages could keep their design.

  3. Preview with the site's own renderer

    The console draws the article with the same shell the public page uses, so what is previewed is what ships.

  4. Save through one repo

    Every table has one server-only repository; a client import of it is a build error, not a leaked connection string.

  5. Revalidate exactly the pages that changed

    The news index, the home page's latest band, the sitemap, and the article's own address, old and new.

Pieces: @tiptap/react · normaliseRichText · articlesRepo · revalidatePath · React cache() · server-only · 60 s page cache
Part 2

Media

From a file to a URL on a page

  1. Convert in the browser

    Photographs and logos become WebP and are capped before they leave the machine, so a few hundred kilobytes arrive, not a camera original.

  2. Sign a PUT for anything big

    Video goes straight to the bucket with a presigned request; the serverless upload route refuses bodies over a few megabytes by design.

  3. File under one prefix

    Every object this site writes sits under MEDIA_PREFIX, which is what keeps the library from listing another site's uploads.

  4. Serve immutable, cached for a year

    Public objects carry max-age=31536000, immutable, straight from Neon object storage or a CDN in front of AWS.

  5. Scan before deleting

    Six queries, however many keys: everything that points at a file is listed in the dialog, because a delete would otherwise surface months later as a missing picture.

Pieces: @aws-sdk/client-s3 · s3-request-presigner · browser WebP · MEDIA_PREFIX · mediaUsage scan · folders and breadcrumbs · storage:copy between buckets
Part 3

Addresses

One table for every URL the site publishes

  1. Claim an address

    PRIMARY KEY (site, entity type, slug): an address belongs to exactly one thing, so two drivers can never hold the same URL.

  2. Keep exactly one live address

    A partial unique index over (entity type, entity id) WHERE is_current means a thing has one current slug and any number of former ones.

  3. Answer to the old one

    A rename keeps the previous slug as a redirect, resolved by one indexed lookup instead of a JSONB scan.

  4. Clear both caches

    After a rename the stale page sits under the address that is now a redirect, so the old path is revalidated along with the new.

Why: ctr.slugs · partial unique index · former slugs as redirects · ⚠ replaced a JSONB array with no unique index · docs/defaults-sync.md

04Key screensp.13

Lights out to chequered flag.

  1. The onboard feed

    The full home page: the video hero, the tagline marquee, the team band, the grid walk, the car's specifications, the calendar with its countdown, the latest news and the partner marquee. Everything on it is a row in the console.

    • Corner readouts: season, live marker, coordinates, founding year
    • Counters roll up along the baseline
    • Everything drifts up and fades as the page scrolls
  2. The grid walk

    On a wide screen the drivers section pins itself and the posters travel sideways as the page is scrolled down, the pit lane seen from the pit wall. The distance is measured, not guessed: the track's real width less the window's, so the last poster stops flush with the edge.

    • Framer Motion scroll transforms
    • A snapping row on phones
    • Honours prefers-reduced-motion
  3. The calendar

    The season's rounds with their circuits, and a next-race panel whose countdown rolls each digit like a departures board. The home page reads the same published season, so the two can never disagree about which round is next.

    • Seasons and rounds edited in the console
    • Circuit maps from the tracks module
    • Departures-board digits
  4. A driver

    Each driver has a poster, a profile and highlights, edited on one console screen and drawn on the drivers page, the home reel and their own address, which survives a rename.

    • Posters converted to WebP in the browser
    • Highlights as a repeater field
    • Former slugs keep old links alive
  5. The console's drivers screen

    The tool palette is deliberately not the site's: warm greys one step apart, hairline separation, IBM Plex Sans, and a single yellow button for the action that writes to the database.

    • Image fields open the media picker
    • Slug field with former addresses
    • Sort order by drag
  6. The media library

    Folders, tiles, an upload zone and a breadcrumb over one prefix of the bucket. Deleting asks the usage scan first and lists every page that would lose a picture.

    • Neon object storage or plain AWS
    • Presigned PUT for video, with a poster frame
    • Copy a whole prefix between buckets with one script

05Tracksidep.19

In the grandstand.

The site on a phone, and the console too. A Capacitor shell wraps the same site for Android.

  • Home on a phone
  • Drivers on a phone
  • Calendar on a phone
  • The team page on a phone
  • The console on a phone

06Stack and rulesp.21

What holds it on the track.

Two palettes that never mix

  • Site: racing yellow on carbon, Rajdhani and JetBrains Mono
  • Console: warm greys, one step apart, IBM Plex Sans
  • Yellow appears in the console only on the button that writes
  • Anything using both would look like neither

Boundaries

  • lib/ is browser-safe; lib/server/ imports server-only
  • The console mount folder is never a URL
  • The session cookie is host-only, no domain attribute
  • ADMIN_HOSTS unset means no host is the admin host
  • No domain is written down anywhere in the source

Operations

  • 25 numbered, checksummed migrations
  • Seed fires only on its own empty table
  • create-admin makes the first owner; a trigger keeps one
  • A second dist dir for verify builds
  • check:source and check:articles guard the content shape

Stack

  • Next.js 15
  • React 18
  • TypeScript
  • Tailwind CSS
  • Framer Motion
  • TipTap
  • Neon Postgres
  • @neondatabase/serverless
  • S3 · presigned PUT
  • Neon object storage
  • sharp · WebP
  • Capacitor · Android
  • React cache()
  • revalidatePath
  • lucide-react

Advertisement · back cover

Want a site that runs like a race weekend?