Back to all projectsCTR Sports: Championship Portal and CMSSeason 2026Live

INCRC 2026 · One Nation, One Championship

A championship,as a product.

ctrsports.in is the source of truth for a national racing championship: the newsroom, the calendar and its countdown, results and standings for seven classes, drivers, teams, tickets and a fan zone. The CMS that keeps it right lives on its own hostname in the same deployment, with roles, a standings engine and an audit trail.

  • Site + CMS, one deployment
  • 9 roles, 29 permissions
  • Standings computed on publish
  • 7 race classes

Server ctr.arkhins.comFull-stack

Role
Design, full-stack, CMS
Stack
Next.js 15 · Drizzle · Neon · Tailwind v4
Shape
npm workspaces: site, db, ui, email
Status
Live for the 2026 season
Live feed
  • Latest
  • Schedule
  • Results
  • Standings
  • Drivers
  • Teams
  • Tickets
  • Video
  • Fan zone
  • One deployment, two hostnames
  • Recompute on publish

Career stats

  • Stat 01

    58

    Tables in the Drizzle schema

  • Stat 02

    9

    Admin roles

  • Stat 03

    7

    Race classes, one weekend

  • Stat 04

    106

    Pages across site and CMS

Yor (Spy x Family)

01Overview

Built to be the record.

Mission briefing

A championship portal is not a brochure. It updates every session of every race weekend, its standings must be right, its calendar must be unambiguous, and its ticket path has to work for someone standing at a circuit gate on a phone. The design borrows from broadcast timing graphics so it reads like a championship, not a startup.

The primary navigation is the championship itself: Latest, Schedule, Results, Standings, Drivers, Teams, Tickets. A ticker across every page shows the current round, the session and a countdown, so the site always knows where the season is. Seven classes race on shared weekends, so results and standings are per category, and the model was generalised into championships, seasons, rounds and sessions so a future series is just another row.

The CMS is the other half of the same build. Nine roles cover the newsroom, the race editors, team and driver managers, media, pages and the fan zone; every server action re-checks its permission and writes an audit row. Publishing a session's classification recomputes the standings snapshot from the stored points, deterministically, and the site's cached reads are invalidated by tag in the same process.

Yor (Spy x Family)
  • A newsroom that leads

    A magazine Latest page: a breaking lead, secondary features, venue guides and class explainers rather than a hero and a scroll.

  • Live season state

    The ticker reads the next round and session from the same data the schedule page renders, with a countdown.

  • Standings engine

    Points systems live per championship season; publishing results recomputes overall, team and sub-classification tables.

  • Roles and audit

    Nine roles, 29 permissions, every mutation checked on the server and logged with who, what and when.

  • Word import

    Drop a .docx into the article editor: mammoth converts it, sharp resizes every embedded image into the media library, sanitised HTML lands in TipTap.

  • Fan zone

    Accounts, predictions, polls, favourites, RSVPs and a double-opt-in newsletter, with web push and email reminders on a cron.

02Two hostnames

Front of house, race control.

One Next.js app, one process, one database. The middleware reads the Host header: the admin hostname is rewritten onto the CMS routes and the public hostname refuses them, so the CMS is unreachable through the site's domain and its cookies never cross over.

P1

ctrsports.in

The portal

Dark-first, in the manner of a timing screen: warm off-white plates on carbon, a single signal yellow, Exo 2 for type and a monospaced face for every number.

Yor (Spy x Family)
  • Cached reads under tags, invalidated on every CMS write
  • Per-class results, standings and driver entries
  • Fan accounts with their own session cookie
  • PWA shell, web push, sitemap and OG from SITE_URL
P2

admin.ctrsports.in

The CMS

Everything on the site has a screen: articles, race weekends, results entry, standings, teams and cars, drivers, media, pages, polls, newsletter, members, admins, settings and the audit log.

Yor (Spy x Family)
  • Server actions re-check permissions on every call
  • revalidateTag straight into the site's cache, no HTTP bridge
  • A members' portal for team staff at /m
  • Responses carry X-Robots-Tag: noindex
Shared by bothone Next.js buildpackages/db · Drizzle schemapackages/ui · tokens and chamferspackages/email · Brevo templatesNeon Postgres, pooled and directNeon object storageADMIN_URL and SITE_URL onlyvercel.json crons

03Roles

Who may touch what.

Yor (Spy x Family)
PosRoleScopeManages
1Super AdmineverythingAdmin users, site settings and every module
2Managereverything but accountsAll modules except admins and settings
3News EditornewsroomArticles, categories, tags and the media picker
4Race EditorracingRace weekends, circuits, results entry, standings recalc
5Team ManagerentriesTeams, season entries and cars
6Driver ManagerentriesDrivers and their season entries
7Media ManagerassetsMedia library, videos and galleries
8Page EditorcontentCMS pages, blocks and sponsors
9Fan Zone EditoraudiencePolls and predictions, fan list, newsletter

04Under the hood

Three things that must not be wrong.

Results, standings and what the fan sees next. Each is a short chain, and each ends in the same place: the public page is right the moment the editor presses publish.

Categories

Results to standings

5 settings · 7 modules
  1. 01

    Pick the session

    A round has sessions per class: practice, qualifying, Race 1, Race 2. Each carries its own grid and its own points scheme.

  2. 02

    Enter the classification

    Positions, times, laps, status and fastest lap per entry, with the signed official declaration PDF attached to the session.

  3. 03

    Publish

    The action re-checks the race-editor permission, writes the results, and writes an audit row.

  4. 04

    Recompute the snapshot

    computeStandings reads the season's points system and rebuilds overall, team and sub-classification tables from session_results.points, deterministically.

  5. 05

    Invalidate by tag

    The standings, results and round pages drop their cached reads in the same process; the ticker moves on to the next session.

Pieces

  • championshipSeasons.pointsSystemenabled
  • raceSessions.sequenceenabled
  • session_resultsenabled
  • computeStandings()enabled
  • standingsType: overall · team · rookieenabled
  • revalidateTagenabled
  • declarationMediaIdenabled
Yor (Spy x Family)

05Key screens

From the lead story to the last row.

Yor (Spy x Family)
  1. The front page

    A ticker with the current round, session and countdown, then a magazine: one lead, a grid of features, venue guides and explainers, the next round, the standings at a glance and the partners' marks in white.

    • Round ticker on every page
    • Lead story with class badges
    • Partner logos as white marks, no plates
  2. Standings

    Drivers' and teams' tables per class, with points, wins and podiums, recomputed the moment a session is published. Position numbers and points sit in the monospaced timing face.

    • Per-class tabs
    • Overall, team and sub-classification tables
    • Same data the CMS standings screen shows
  3. A race weekend

    Every session of every class across the weekend, with the circuit, its turns and direction, the countdown to the next session, and the results once they are in.

    • Sessions grouped by day and class
    • Circuit facts from the circuits module
    • RSVP for signed-in fans
  4. Results entry

    The CMS screen a race editor lives in on a Sunday: one session, every entry, positions, times and status, the declaration PDF, and a publish that recomputes the standings.

    • Permission re-checked on the server
    • Audit row per publish
    • Standings snapshot rebuilt from stored points
  5. The article editor

    TipTap with tables, links and images from the media library; import a .docx and keep editing. Category, tags, hero image and scheduling on the side.

    • Import .docx
    • Media picker with WebP variants
    • Preview links built from SITE_URL
  6. The audit log

    Every write, by whom, on what, when. Roles are enforced in the action itself, not only in the navigation, so the log is complete by construction.

    • Nine roles, many per admin
    • Denied screen for out-of-scope routes
    • Settings edit the theme at runtime

06At the gate

On a phone, at the circuit.

Tickets, the schedule and the standings on a phone, and the CMS too: a race editor can enter a classification from the pit wall.

  • Home on a phone
  • Tickets on a phone
  • Standings on a phone
  • A round on a phone
  • Results entry on a phone

07Stack and rules

What keeps it on schedule.

Model

  • championships → seasons → rounds → sessions, with sequence for Race 1 and 2
  • Points systems as JSON per season; standings types per season
  • Race categories per championship; entries carry a classification
  • Standings snapshots, never live sums
  • Media by key, URLs built from the current bucket

Boundaries

  • Admin, fan and member cookies have distinct names and secrets
  • /admin/* is a 404 on the public host
  • Server actions check permissions, then write the audit row
  • Client-side image compression keeps uploads under the function limit
  • No domain in the source: SITE_URL and ADMIN_URL only

Stack

  • Next.js 15
  • React 19
  • TypeScript
  • Tailwind CSS v4
  • Drizzle ORM
  • Neon Postgres
  • @neondatabase/serverless
  • Neon object storage
  • sharp
  • mammoth
  • TipTap
  • TanStack Table
  • zod
  • bcryptjs
  • web-push
  • Brevo
  • date-fns
  • npm workspaces
  • Vercel crons

Operations

  • 19 Drizzle migrations against the direct connection
  • Seed is idempotent: racing data rebuilt, content kept
  • db:copy and storage:copy move a whole deployment
  • Fetch-data pulls the latest results into JSON snapshots
  • GitHub Actions runs the cron jobs off-platform too
Yor (Spy x Family)

Event complete

Want a championship that runs itself?

  • Tables in the Drizzle schema+58
  • Admin roles+9
  • Race classes, one weekend+7
  • Pages across site and CMS+106