Skip to content
All work
Live in production
Ticketing · public institution
2025 — 2026

B.M. Birla Planetarium

Show ticketing with a counter POS

The ticketing system for a public planetarium — online seat booking, a counter POS selling the same shows, discounts, cancellations, holiday scheduling and the reports the finance team runs on it.

Visit live site
bmbirlaplanet.org

Role

Lead developer

Client

B.M. Birla Planetarium · via Kayease

Status

Live in production

Year

2025 — 2026

63

App Router pages

77k

Lines of code

16

Sequelize models

155

Express routes

107

Commits

15

Runtime bugs fixed in audit

The hardest problem

A planetarium show has hard, finite capacity, and two channels sell it at once — the website and the counter POS in the lobby. A visitor buying online and a clerk selling the last row at the same moment must not both succeed. Layer on holiday closures, per-show discounts and cancellation requests, and 'how many seats are actually left' stops being a simple read.

What I did about it

01

Show capacity is modelled on ShowTime, not Show — a booking consumes a specific dated slot, so the counter and the website contend for one row rather than reconciling two counters afterwards.

02

Cancellations are a CancellationRequest with its own lifecycle rather than a flag on the booking, so a refund that is requested, approved and then reversed leaves a readable trail instead of an ambiguous final state.

03

Holidays and a SiteMode switch are first-class tables, so closing the planetarium for a day or putting the site into maintenance is an operator action rather than a deploy.

04

Tickets are generated server-side with PDFKit and reports with ExcelJS, so what the visitor receives and what finance reconciles come from the same source rather than two renderings of it.

What I rejected, and why

The honest part of this project is the audit. A committed AUDIT_REPORT.md tracks the cleanup: 15 backend runtime bugs fixed, the frontend TypeScript error backlog cut from 96 to 47, 18 debug console.logs removed — two of which were leaking auth tokens — and a dead, backend-less /admin/tickets subtree deleted. next build still passes only because ignoreBuildErrors is set; clearing the last 47 errors so that flag can be removed is the open item, and it is written down rather than glossed over.

What I’d change

next build still passes only because typescript.ignoreBuildErrors is set, with 47 errors left in the frontend backlog. That flag hides the next real type error behind 47 old ones, and clearing them is the open item.

Built with

Next.js 15React 18ExpressTypeScriptSequelizeMySQLPDFKitExcelJSHandlebarsJestHelmet

Next case study

QuataTrade

P2P crypto marketplace with escrow