How it's built

The stack, written for a curious non-engineer.

Engineers will respect it. A curious non-engineer will follow it. The goal of this page is both. I am writing about an app I built without formal training, so I tried to write it the way I would want it written about something I didn't know.

The whole picture, in two sentences

Preg is an Expo React Native app written in TypeScript. The data lives in Firebase — Firestore for the records, Storage for the photos, Auth for the two of us, Cloud Functions for anything that needs a server, and Hosting for the separate Village web app.

The stack, piece by piece

Expo React Native

Cross-platform mobile, one codebase, fast iteration. I picked Expo because it gave me over-the-air updates (I can push a fix to Megan's phone without going through the App Store), because the development experience is excellent, and because the AI coding tools I was using knew it inside and out.

TypeScript

Every file. I learned TypeScript by force, mostly by reading errors and asking the AI what they meant. The strict-mode discipline has caught more bugs than it has cost me — and as a nurse, the analogy I keep coming back to is that types are like the double-check we do before a medication. It feels slow until it stops you from doing the thing you didn't mean to do.

Firebase Firestore

All the records — every check-in, every appointment, every kick session, every name on the Name Board — live in Firestore. It is a hierarchical document store with real-time subscriptions, which means that when Megan adds a question to an appointment on her phone, my phone sees the update in under a second without me writing any sync code.

Firebase Storage

Photos. Ultrasounds. Attachments. Megan's bump photos go here, the bulletin board photos go here, and the EPUB and PDF files for the in-app bookshelf go here.

Firebase Auth

It started as two users — Megan and me — and auth was mostly invisible because we signed in once and never signed out. Then the Village grew accounts: every invited family member now signs in (Google or email) after redeeming a one-time invite code, so the board knows a 🥹 from Grandma is actually from Grandma. Same auth pool, same project, more humans.

Firebase Cloud Functions

The serverless backend for anything that can't run on the phone. The biggest job is Ask The Books — embedding a question, querying Pinecone, building a prompt, calling Gemini, and returning the answer plus citations. There's also a function that generates the Daily Brief every morning and a couple that handle scheduled jobs.

Firebase Hosting

One Firebase project, several hosting sites that never step on each other: the Village's original site, a private ops dashboard, and this site — Preg HQ — each deployed with its own explicit target. The Village also runs here at /village/board now, as a straight copy of the same three files, so one headquarters can hold everything while the original address keeps working for everyone who bookmarked it.

Vertex AI — Gemini 2.5 Flash

The language model behind Ask The Books and the Daily Brief. Flash is the right tier for what I'm doing: fast, cheap, and very good at following instructions for citation format and tone.

Pinecone

The vector index for the six pregnancy textbooks. 3,815 chunks. Each chunk is about 500 tokens with 80 tokens of overlap. The index name is pregnancy-books. Querying is fast enough that Ask The Books answers feel instant.

The phase switcher

The app has three modes — Pregnancy, Labor, Postpartum — and a global switcher at the top that flips between them. Architecturally, the switcher is a single piece of app state. Most screens read the current phase and render the right version of themselves. Some screens (Labor mode's home screen, for example) are entirely separate routes that the navigator only mounts when the phase is set to labor.

The thing I'm proudest of in this design is that the underlying data model is the same across all three phases. An appointment in pregnancy mode and a pediatrician visit in postpartum mode are the same document type — they share the prep/visit/follow-up structure and the attachments. The phase decides what the user sees. The data does not care.

The shared-collection Firestore model

Megan and I are both pointed at the same Firestore documents for the things we want to share — appointments, the Name Board, the Timeline, the in-app chat, the Village posts. Anything that is private to one of us (Megan's Inner Life entries are not visible to me, for example) is namespaced under their own user ID. There is no family entity in the schema. There is just a small list of shared collections at the root, and Megan and I are both authorized to read and write them.

That was the right call for a two-user app — and when the Village grew real family accounts, the model grew with it, exactly the way the previous paragraph predicted it would have to: there's now a family entity with a members subcollection, a familyId on the shared records, and security rules that scope every read and write to family membership. The pleasingly small schema became a pleasingly boring multi-family one, which is the most flattering thing you can say about a schema migration.

The Ask The Books pipeline

Six books. Each is parsed into plain text with chapter and page boundaries preserved. A Python script splits each book into ~500-token chunks at natural paragraph breaks, tags each chunk with the book title, chapter, and page, embeds each chunk with text-embedding-004, and upserts the result into Pinecone. The script is idempotent — re-running it doesn't duplicate.

At query time, a Firebase Cloud Function takes Megan's question, embeds it, queries Pinecone for the top eight most similar chunks, and builds a prompt that includes the chunks and a strict instruction to cite every claim. Gemini 2.5 Flash generates the answer. The function returns the answer along with the sources, formatted for the UI. End-to-end latency is 1.5 to 3 seconds.

The Village

A separate web app under the same project. Vanilla HTML, CSS, and JavaScript — no framework, no build step, three files — because the audience includes grandparents with week-old browser tabs, and boring technology is what keeps week-old tabs alive. Firestore real-time subscriptions for the feed, reactions, and comments. Access began as a single shared family password and grew up into per-member accounts: one-time invite codes, Google or email sign-in, and security rules that scope everything to the family.

The cork-board aesthetic is built entirely in CSS — a small repeating linen texture, a wooden frame, CSS masonry, and pushpin pseudo-elements at small random angles. New posts animate in with a brief gold flash and a soft toast at the bottom.

This site

Preg HQ is an Astro static site — every page is plain HTML with almost no JavaScript, which is why it's fast. The three exceptions are deliberate: the Village board and the ops dashboard are self-contained apps that ride along as static files, and the shop's Buy buttons make one small request to a Cloud Function that opens a Stripe-hosted checkout. When an order completes, Stripe calls a webhook function that sends the order to a print-on-demand service. No cart, no accounts, no card numbers anywhere near this codebase — the boring architecture is the security feature.

The Games Hub

Five games, all async, all backed by Firestore documents. Each game session is its own document. Each move is an update to the document. Both players have real-time subscriptions and animate the most recent move in. Chess uses chess.js for move validation and game state; the rest are written from scratch with the rules in plain TypeScript.

The release cadence

Mostly evenings. I batch up the week's changes and push an over-the-air Expo update on Sunday night, so when Megan opens the app on Monday morning it has whatever I shipped that week. Bug fixes for anything she reports on the day get pushed immediately. The whole loop from Megan finds a bug to the fix is on her phone is usually under an hour.

The things I'd do differently

I'd start with proper Firestore security rules from day one instead of bolting them on later. I'd write more unit tests around the data layer earlier — the kick counter specifically broke twice in ways tests would have caught. I'd commit to TypeScript strict mode from the first commit. I made all three of those mistakes and they cost me weekends I'm not getting back. If you're building anything in this space, take that as a small gift.