Back

Scope without collapse

Garaje

The operating system for Philippine car culture: six front ends on one shared backend, built in eight months by one person on the technical side.

Role
Co-founder & CEO, Product and Engineering
Year
2025–present
Org
Garaje LLC
The Garaje consumer homepage, Everything for car people, with the mobile app beside it
6
Front ends on one shared backend
Garaje monorepo, August 2026
53
Backend tables behind all of them
Garaje monorepo, August 2026
621
Test files, weighted to pure business logic
Garaje monorepo, August 2026
~360
Reviewed pull requests in eight months
Garaje monorepo, August 2026
23 Jun 2026
First vendor approved
Medium, “First in”, 23 June 2026

One product, six front doors

Garaje is where car culture in the Philippines happens online. You keep a garage of your vehicles and document builds on them, join clubs, find and run meets, book parking and services from verified shops, and buy and sell cars and parts. Businesses get a workspace to manage listings, bookings, events and a subscription.

The breadth is the product bet, not scope creep. We are not trying to win feature-for-feature against the best events tool or the best marketplace. We are betting that one place holding all of an enthusiast's activity beats five best-in-class tools they have to stitch together themselves. Your vehicle is the same object whether you are showing it at a meet, listing it for sale, or logging a build on it.

That amounted to six front ends over one shared backend of 53 tables. I own design and engineering. My co-founders own business and operations.

How one person holds it

One shared backend library, consumed by every front end including React Native. The conventional alternative, an API layer with each client owning its own data access, would have killed this. Every feature gets built twice and the business rules drift apart between them. With one person on the technical side, that is not a tax, it is a wall.

So ticket pricing, capacity math and validation were written and tested once. Porting events to mobile became purely a UI exercise. The schema generates its own types, so a field I add to a table is type-checked inside a native screen with no hand-maintained contract to fall out of sync.

The release process is the other half. Every flagged surface ships staff-enabled in production and publicly invisible, so I develop against real data and real edge cases while the public sees nothing. The marketplace lived in that state for months; when it launched, the launch was a toggle in an admin console rather than a deploy. More to the point, turning something off at 11pm is also a toggle, not a revert and a build and a deploy.

The cost is real. Install is slow, and the main app is pinned a major version behind the marketing sites because upgrading the largest surface is a project rather than a chore. I would make the same call again.

Legal to show, illegal to book

The naive marketplace listing has one switch: live or not. That model is wrong here, and the reason is specific. To advertise a business location in the Philippines you need a mayor's permit or barangay clearance. To take payment you need a BIR 2303, the tax registration certificate. Different documents, different agencies, obtained at different times.

Collapsing both into one boolean forces a bad choice. Block legitimate businesses until tax registration completes, and the marketplace is empty during exactly the period you are trying to fill it. Let anyone take payments, and you put both the shop and the platform in a bad position.

So bookability became its own computed decision, and it returns a reason rather than a boolean. “Booking unavailable” with no explanation is a dead end for the buyer and a support ticket for us. A free listing says contact the host directly. An unverified one says the host is not accepting online bookings yet. There is always a next action.

The first version checked whether a BIR document existed on the workspace. That is spoofable: a partner uploads the file and tags its own type, so any PDF can claim to be a tax certificate. Taking money on the strength of a self-tagged upload is not acceptable, so the check now requires the document and an admin-verified workspace. The note I left in the code is the entire reasoning: presence alone is spoofable.

A listing can now be discoverable, credible and complete while its owner's registration is still in progress, with an honest explanation where the booking button would be.

The gate

Events are the feature closest to why car culture exists, and the interesting half happens at a folding table with bad signal. An organizer is standing at an entrance with a phone and a queue of cars behind them.

Pre-registered arrivals are the easy case. Walk-ins are where the design work is: no account, not making one in a queue, and often paying a different price than the pre-sale tier. So walk-ins create attendance records with no user attached, priced at a door rate carried on the ticket type. The whole point of early-bird pricing is that the door costs more.

Walk-ins also brought the one genuinely sensitive part of the product. Organizers wanted a plate number and an ID reference for cars entering their venue, which is reasonable safety practice and also personal data collected from people who never signed up for anything. It is readable only by that event's organizers, never rendered on a public surface, and never included in the payload that draws attendee lists for other guests. Check-in verifies the caller against the event instead of trusting an ID passed from the client, because a mutation that accepts who I am as an argument is not authentication, it is a suggestion.

Entry is a single transition, in then out, not a running tally of re-entries. The richer model is easy to build and useless at a real gate, where nobody is reconciling a four-entry timeline for one attendee at 9pm.

What I got wrong

We renamed “partner” to “business” across the product. I audited with repository-wide searches, concluded I had found everything, and shipped. Strings assembled from fragments survived in places no pattern reached, and users saw the old word where I had confidently declared the code clean.

A piece of navigation chrome decided what to render from cached workspace state. That state survives logout, so a signed-out visitor could briefly see a control meant for signed-in partners.

A richer vehicles table replaced an older cars table. I moved the garage experience across and missed that the booking flow's vehicle picker still read the old one, so a user could add a car in their garage and then not find it when booking a service. Parking and EV charging still read the legacy source. That is a tracked divergence rather than a discovered surprise, but it is unfixed, and I would rather write that here than claim the migration was clean.

The worst one had no error at all: every class name in the mobile app silently stopped doing anything. The cause was two copies of React Native in the bundle graph. The styling library patched one, my screens imported the other, and both sides worked exactly as designed while producing nothing.

Those are one mistake wearing four sets of clothes. I trusted a proxy for the thing instead of the thing: a grep result for complete coverage, cached data for being signed in, a migrated feature for a migrated data model, one copy of a package for the only copy. What I do differently now is verify at the layer where the truth actually lives: inspect the built bundle rather than the source, ask the auth system rather than the cache, enumerate the readers of a table rather than the features that motivated it.

Screens

Built with

  • TypeScript
  • Next.js
  • React
  • Expo
  • React Native
  • Convex
  • Clerk
  • Stripe
  • Sanity
  • Mapbox
  • Tailwind CSS
  • NativeWind
  • Nx
  • Vitest
  • Playwright
← All work

More work