Cloud Run AI Challenge — Build Log

Building Reflect: a mindful workspace that actually remembers you

A security-first, agentic journaling and gratitude workspace built on Gemini and Firestore in Google AI Studio — and the honest list of everything that broke on the way to production.

● Firebase · Firestore · Cloud Run · Gemini Try the live app View the code

Most AI-generated apps look great in a demo and fall apart in production. This is the story of building one that doesn't — and everything I learned catching the ways it almost didn't.

Reflect isn't a chat wrapper with a journal skin. It's a persistent companion that builds long-term psychological memory over time — combining an asynchronous memory synthesis layer with on-demand thematic insight extraction, daily gratitude tracking, and proactive check-in nudges, so patterns in how you think and feel become visible instead of scattered across dozens of disconnected entries.

The starting constraint

The brief was simple on paper: build an authenticated app, wire up Gemini and Firestore, isolate every user's data, never let a secret touch the client — all inside Google AI Studio, configuring its Custom Instructions to enforce threat modeling and secure coding before a single feature existed.

That constraint shaped everything that came after. Every later addition — gratitude tracking, data export, account deletion — inherited the same UID-scoped isolation and server-side secret handling by default, because the rules were load-bearing from day one, not retrofitted at the end.

Custom Instructions enforced threat modeling, Firestore ABAC isolation, input sanitization, and Secret Manager retrieval before any feature code existed.

What it actually does

Reflection engine
Multi-turn dialogue with sentiment scoring (0–100) and mood breakdown per entry
Gratitude tracker
A focused daily module for three things you're grateful for, with history
Insights
Sentiment trajectory charts and thematic reports that cite the entries behind them
Weekly recap
An independent 7-day digest with narrative summary and forward-looking prompts
Agentic nudges
Background prompts that detect reflection gaps and invite a check-in
Data sovereignty
Full JSON/Markdown export, plus reversible deactivation or permanent account purge
Daily Reflection with streaming dialogue
Fig. 1 — Daily Reflection, the primary journaling surface
Daily Gratitude tracker
Fig. 2 — Daily Gratitude Tracker
Insights with sentiment trajectory
Fig. 3 — Insights & Behavioral Analytics

The part most build logs skip

The features are the easy part to write about. What's more useful is what actually broke — because every one of these was a real moment where "looks done" and "is done" turned out to be different claims.

The response that was never there

Early on, a journal entry would sit in a loading state indefinitely. The network request wasn't failing — it simply never resolved. A background memory-synthesis call was silently blocking the user-facing reply from ever starting. The fix informed the architecture shown below: memory updates run genuinely asynchronously, never gating the conversational response.

The reply that was always the same

Later, every entry — regardless of content — returned identical reflection text. A fail-safe fallback, added to guarantee the UI never showed a blank card, had quietly become the default path instead of the exception. That's part of why the current architecture uses an explicit multi-model fallback chain (gemini-3.1-flash-lite → gemini-3.7-flash → gemini-flash-latest) with visibility into which tier actually responded, rather than a single silent catch-all.

The moment that mattered most

Mid-build, GitHub's secret scanning flagged a live Google API key committed to the public repository. It was rotated within the hour, the repo history was cleaned, and the codebase was re-audited to confirm nothing else was exposed — the GEMINI_API_KEY now lives exclusively in Secret Manager, retrieved server-side only.

We're including this on purpose. A security incident that gets caught, disclosed, and fixed quickly says more about real security practice than a repo that simply never mentions the possibility.

Most of these bugs shared one pattern: the network request succeeded, but the UI didn't reflect what actually happened underneath. A 200 status code is not the same claim as a correct response.

Lesson: "it returned 200" and "it worked" are different claims. Every real bug in this build lived in that gap.

What it's built on

Firebase Authentication handles Google Sign-In with no passwords ever stored. Cloud Firestore holds every user's entries, gratitude logs, memory summary, insights, and nudges — all strictly UID-scoped, with security rules that deny everything outside a user's own path by default. An Express.js API layer on Cloud Run orchestrates every Gemini call through @google/genai, with a three-tier model fallback for resilience, and the API key retrieved from Google Cloud Secret Manager at runtime — never shipped to the client.

Firestore isolation rule

match /users/{userId}/{document=**} {
  allow read, write: if request.auth != null
    && request.auth.uid == userId;
}
match /{document=**} { allow read, write: if false; }

Every collection follows the same pattern: users/{uid}/entries, users/{uid}/gratitude, users/{uid}/profile/summary, users/{uid}/insights, and users/{uid}/nudges — no exceptions, no shared collections, no cross-tenant reads possible.