The stack we use to ship apps fast
We are a small studio. We build software, then we run it, which means every tool we pick has to earn its keep twice: once while we are writing the app, and again at two in the morning when something breaks and one of us is the person on call. That second cost is the one people forget. A clever bit of infrastructure you have to babysit is not clever, it is a second job.
So the rule we hold to is simple. Pay someone else to run the boring, undifferentiated part - the database host, the build servers, the certificate dance - and spend our own hours on the thing users actually see. Here is what that looks like in practice, piece by piece, and why each one is on the list.
Mobile
Kerbnow and RHUL Mobile are both React Native, built with Expo and routed with Expo Router. We write the app once in TypeScript and it runs on iOS and Android. Expo Router gives us file-based navigation, so the folder structure is the app’s structure and there is no separate map of routes to keep in sync.
Builds and store submission go through EAS Build and EAS Submit. This is the part we are happiest to hand over. Native builds mean Xcode versions, provisioning profiles, signing certificates and the specific misery of App Store and Play Store uploads. EAS runs all of that on their machines to a config we check into the repo, so a release is a command, not an afternoon. When it goes wrong it is usually Apple’s fault, and we would rather debug that against a known-good build pipeline than against our own laptop.
Subscriptions run through RevenueCat. Both apps have a paid tier, and RevenueCat sits between our code and the two stores’ billing systems so we do not have to write receipt validation or reconcile Apple and Google’s different ideas of what a subscription is. We ask it whether a user has access and it tells us. That is a lot of edge cases we never had to write.
Web
The marketing sites - the pages you are reading this on, and the parking-rule guides on the Kerbnow site - are built with Astro and shipped as static HTML. There is no server rendering the page when you visit. Astro builds the whole site to plain files at deploy time, so what reaches your browser is HTML and a little CSS, which is fast and has almost nothing to go wrong.
Those files live on Cloudflare Pages. It serves them from the edge, close to whoever is asking, and it costs us next to nothing because there is no server sat there waiting. For a content site this is the right shape. We are not paying to keep a machine warm for pages that do not change between deploys.
Backend
The app API is a Hono server running on AWS Lambda, sat behind CloudFront. Hono is a small, fast web framework that runs happily inside a Lambda function, so the API only costs us anything while it is actually handling a request. There is no idle server to pay for and nothing to scale by hand when a term starts and every student opens the bus app at once. CloudFront sits in front as the front door and caches what can be cached.
Auth is Better Auth. Sign-in, sessions and the token handling underneath are a category of code you really do not want to get subtly wrong, so we use a library that has thought about it harder than we have time to. It plugs into our own database rather than shipping our users off to a third party, which keeps the account data ours.
Data
Everything of record lives in Neon, which is Postgres run as a serverless service. It is ordinary Postgres, so we are not locked into anything exotic, but Neon handles the hosting and scales the compute down when nobody is querying. We talk to it through Drizzle, a TypeScript ORM that keeps the database schema and our types in one place, so a column change shows up as a type error before it shows up as a bug.
The part of Neon we lean on most is branching. Each app gets an isolated branch for production and a separate one for development, so we can test a migration against a real copy of the data without going anywhere near the live database. Getting that wrong is how small teams lose customer data, and branch isolation means a mistake in dev stays in dev.
Operations
The rest is the connective tissue that keeps the lights on.
Files that users upload, and assets we serve, go in Cloudflare R2. It is S3-compatible object storage with no charge for pulling data back out, which for anything image-heavy is the difference that matters.
Transactional email - the receipts, the sign-in links - is written with react-email, so we build the templates as components in the same language as everything else, and sent through AWS SES. We keep the reputation of those sending domains separate from anything marketing so a promotional wobble never puts a password-reset email in a spam folder.
Product analytics is PostHog. It tells us which screens people actually use and where they drop out, which is how we decide what to build next rather than guessing.
All of it ships through GitHub Actions. Push to the main branch and the pipeline builds, checks and deploys. There is no manual deploy step for either of us to forget or get wrong at the end of a long day.
None of this is exotic. That is the point. The stack is boring on purpose, because boring is what lets two people build an app, launch it and still be there to keep it running.