Hive, a Classroom Curation Platform
A content-curation platform for a real client, migrated from JSX to TypeScript while it stayed live and kept shipping features.
Client-owned codebase, so there is no public repository. The running application is public, linked below.
What’s hard about it
The codebase was plain JSX with no type coverage, already deployed and in use, and the feature roadmap could not stop for a rewrite. The work was migrating it to TypeScript incrementally, module by module, keeping the build green and the app shipping the whole way, rather than doing the big-bang rewrite that usually strands a project half-converted.
What it is
A content-curation platform built for a real client: curators sign in, pull items into collections, tag and order them, and publish the result to a public-facing view. React front end, AdonisJS API, PostgreSQL underneath, deployed and used in production.
The interesting engineering was not the CRUD. It was that the application was already live when the largest change, full TypeScript adoption, had to happen.
The migration, concretely
The starting point was a JSX codebase with no type coverage and an active feature roadmap. The approach:
- Turn on
allowJsandcheckJs: false. Both languages compile. Nothing breaks. - Convert leaf-first. Utility modules and types have no internal dependents, so converting them can't cascade. Each one lands as its own small, reviewable commit.
- Model the domain before the components. Once
Item,Collection,Tag, andCuratorwere real types, converting the components that consumed them became mechanical, and the compiler started finding actual bugs rather than restating what I already knew. - Ratchet, never loosen. Once a module compiled under strict settings, it stayed there.
The payoff was the class of bug that stopped happening: the API returned nullable fields that the UI rendered unconditionally, and TypeScript surfaced every one of those sites at compile time instead of leaving them for a curator to find.
What production taught me
Real users on a real deadline changed how I work more than any of the technical content did:
- A deploy is not a finish line. The first week after launch produced more requirements than the whole design phase, not because the design was wrong, but because people only discover what they need once they can touch it.
- Client feedback is usually a symptom, not a diagnosis. "The list feels slow" was a rendering problem on one screen and a query problem on another. Both arrived phrased identically.
- Boring is a feature. Nothing about this stack is novel, and that was the correct call: the novelty budget went into shipping and operating it, which is what the client was actually paying for.
Design decisions
- 01
Incremental TypeScript migration, file-by-file, with `allowJs` on
RejectedA big-bang rewrite on a long-lived branch
A rewrite branch would have diverged from a codebase that was still shipping features weekly, and every day of divergence made the merge worse. Keeping `allowJs: true` meant typed and untyped modules coexisted, so every commit was deployable and the migration could pause whenever a feature took priority.
- 02
AdonisJS over a bare Express setup
RejectedExpress with hand-assembled middleware, validation, and ORM
A solo developer on a client deadline cannot also maintain a bespoke framework. Adonis brought validation, migrations, and an ORM as one coherent piece with one set of conventions. The cost is a smaller ecosystem than Express, a real trade I would make again at this team size, and would reconsider on a team of five.
- 03
PostgreSQL with explicit migrations from day one
RejectedA document store, or an ORM with auto-syncing schema
The content model was relational: items, collections, tags, and curators with real foreign keys between them. Auto-sync schema tools are pleasant until the first production data migration, at which point the absence of a reviewable migration history becomes the problem.
What I’d do differently
- Turn on `strict` mode at the very start with the whole codebase in `allowJs`, rather than enabling strictness per-module. Ratcheting strictness later meant revisiting files I had already converted once.
- Write the seed script in week one. I tested against hand-made data far longer than I should have, which hid at least one ordering bug that only appeared with realistic volume.
- Instrument earlier. I had no request-latency visibility until late, so performance conversations with the client were qualitative when they should have been numeric.