bakebook · field guide
How bakebook actually works.
A recipe development and storage app: develop a recipe over time, cook from it, log the bake, ask an AI assistant, and share it. Built as a web app and shipped to the App Store. This page covers what it does, the functions that do it, where the data lives, what could go wrong, and where to change any of it.
Living doc · rebuilt 2026-08-20 · kept current as the architecture changes
01 The three layers
bakebook is one web app, wrapped to run natively, talking to a cloud backend.
Plain HTML, CSS & JavaScript
No framework. The pages a baker sees, in bakebook/.
wrapped by ↓
Capacitor + Xcode
Packages the web app into a native iOS shell and submits it to the App Store. Also wraps for Android.
talks to ↓
Firebase + Claude
Accounts, database, files and server code. Claude (Sonnet) powers butter, reached only through a server function.
02 What you can do
Each capability, with the function that makes it possible (spelled out in section 03).
Develop a recipe over time
Change amounts and steps without losing earlier tries. Each iteration is kept.
setLocalRecipes → pushToCloud
Store & organize
Sort into categories, rename them, drag a recipe between them.
convergeCats · metaDoc
Cook from it (“make-it”)
A hands-free step-by-step mode; ingredients matched to the step that uses them.
mapStepIngredients
Log a bake
A dated note per bake, merged safely across devices.
unionLogs
Ask butter
AI help, always routed through the server gate.
exports.claude
Share a recipe
A public link; “add to my bakebook” copies it in.
cloudSafe → shares/{id}
Delete, with undo
Remove a recipe and put it back before it's gone for good.
tombstones · bakebookForgetDeletes
Work offline, sync later
Edit with no connection; changes upload when you're back.
markPending · startListener
03 The functions that do the work
The real functions in the code, what each one does, and which file it lives in.
The sync engineruns in the browser · bakebook/bakebook-store.js
getLocalRecipes()
setLocalRecipes()
Read and write the working copy in localStorage. This is the copy every page actually edits.
markPending()
Flags any recipe whose local copy differs from what was last pushed, so a real unsynced edit is never dropped in a merge.
schedulePush()
pushToCloud()
Debounced by 700ms, then uploads changed recipes to Firestore. bakebookFlush() forces it to run right now.
startListener()
applyCloud()
A real-time listener: when another device changes something, it pulls that change down and applies it.
mergeSets()
The heart of it. Converges local + cloud into one agreed truth: keep the local copy only when it's a genuine unsynced edit, otherwise take the cloud's; the newest updatedAt wins.
getTombstones()
setTombstones()
Remember deletions so a recipe you deleted doesn't come back from the cloud on the next sync.
uploadRecipePhotos()
Send photos to Cloud Storage and store the resulting URL on the recipe (the image itself never sits in the database).
bakebookForgetDeletes()
Undo support: forget a tombstone so a just-deleted recipe can be restored.
The serverruns on Firebase · functions/index.js
exports.claude
The butter proxy. Picks the model, sets the prompt and reply length, meters usage, and screens for abuse. The only path to Claude.
isOffensive()
The moderation screen: a cheap fast model checks each message for hate or sexual content before the real model answers.
butterQuotaAllows()
bumpUsage()
Enforce and count the daily limits (free vs premium), so no one can run up an unbounded bill.
recordStrike()
One strike warns; a second bans the account. Banned users can't reach butter at all.
exports.mapStepIngredients
Matches each ingredient to the step that uses it, which powers make-it mode.
exports.deleteAccount
cancelDeletion
purgeDeletedAccounts
Soft-delete with a grace period: mark the account, let it be undone, and a scheduled job hard-deletes only after the window passes.
exports.revenuecatWebhook
Receives subscription changes from RevenueCat and sets premium on the account (server-side only).
04 Why a web app, not Flutter
The first version was a FlutterFlow project with no usable export. Rather than fight a black box, bakebook was rebuilt from scratch as a plain web app: quicker to learn, nothing hidden behind a framework, and easy to wrap for iOS with Capacitor.
05 The stack at a glance
| Part | What runs it | Why |
| Frontend | HTML · CSS · JavaScript | Plain and legible; the app is the vehicle for learning. |
| Native wrapper | Capacitor + Xcode | Ship the web app to the App Store without rewriting it. |
| Accounts | Firebase Auth | Sign-in; ties every recipe to a person. |
| Database | Firestore | Recipes, logs and saved chats, synced across devices. |
| Files | Cloud Storage | Recipe photos (the binary files). |
| Server logic | Cloud Functions | The gate in front of Claude, and the only writer of plan/ban state. |
| AI assistant | Claude Sonnet 5 | butter, reached only through the proxy. |
| Subscriptions | RevenueCat | bakebook+ membership + Apple billing. |
06 The recipe, as data
Every recipe is one document, stored under the signed-in baker's account.
users/{uid}/recipes/{recipeId}
titlethe recipe name
ingredients[]rows of amount + item, editable as a table
steps[]the method, one step at a time (drives make-it mode)
categorieshow the baker organizes it
versions[]earlier iterations, kept so nothing is lost as the recipe develops
logs[]dated bake notes, merged by id across devices
photoa URL into Cloud Storage (the image lives there, not here)
updatedAtthe timestamp the merge uses to pick the newest edit
07 Where your data lives, and why
| Data | Where it lives | Why there |
| Working copy recipes, categories | localStorage a cache, on the device | So the app opens instantly and works offline. This is a fast mirror of what's in the cloud, not the only copy. If an app update clears it, everything reloads from Firestore — losing it is a non-event by design (the one edge is an edit still mid-sync; see trade-offs). |
| Recipes, logs, saved chats | Firestore users/{uid}/… | The durable backup, tied to your account, so it syncs across devices and survives a reinstall. |
| Recipe photos | Cloud Storage | Big image files don't belong in a document database; the recipe stores only a link. |
| Plan & ban state | users/{uid} server-write-only | Readable by the browser, never writable by it. Only Cloud Functions set it, so no one can self-grant premium or lift a ban. |
| A shared recipe | shares/{id} public read | Anyone with the link can view; only you can publish or change it. A snapshot, never your private fields. |
08 Trade-offs: can anything be lost?
The device copy is a cache; the cloud is the source of truth. So losing the device copy loses nothing on its own, and the honest answer is “almost never, with two edges.” Here's each case.
You switch to a new device
The new device signs in and pulls everything from the cloud (startListener → mergeSets), so your recipes are all there. The one lag: a change made offline on the old device only arrives after that device gets online and syncs.
covered
You update or reinstall the app
An update can wipe localStorage. Your recipes come back from the cloud automatically (the merge + an auto-restore path). This is exactly what happened once in production, and everything returned.
covered
An edit hadn't finished syncing when the wipe hit
There's a 700ms window (and any time you're offline) where a fresh edit is only on the device. If localStorage is wiped in that window, that one edit can be lost. markPending shrinks the risk by re-arming unsynced edits, and bakebookFlush() forces an immediate push before risky moments.
edge · mitigated
Two devices edit the same recipe offline
When both come online, the merge is whole-recipe most-recent-edit-wins (by updatedAt). The older of the two edits is overwritten — there's no field-by-field merge. Rare in practice, but a real limitation to know.
edge · by design
The rule of thumb
The cloud is the durable backup; the device is the working copy. Deletes are handled by tombstones so nothing resurrects. The only residual risk is an unsynced edit at the instant of a wipe, or two offline edits to the same recipe.
09 butter → Claude
The assistant never talks to Claude directly. Every message runs through exports.claude.
the app
baker sends a message
→
exports.claude
model · prompt · caps · moderation
→
→
the app
reply appears in butter
10 Where & how to edit (all of it)
Which file to open for each kind of change, and how to push it live.
To change…
Open
Then deploy with
A screen's look or behaviour
home, recipe, butter, logbook, share
bakebook/index.html · recipe.html · butter.html · logbook.html · share.html
firebase deploy --only hosting
+ npx cap copy for the app
How recipes save, sync or merge
bakebook/bakebook-store.js
firebase deploy --only hosting
+ npx cap copy
butter's model, prompt, caps, prices
the numbers at the top of the file
functions/index.js
firebase deploy --only functions
Who can read or write what
firestore.rules
firebase deploy --only firestore:rules
The iOS app itself (native shell)
npx cap copy ios → archive in Xcode
The pattern
The web app deploys to Firebase Hosting; to get the same change into the installed iOS/Android app you also run
npx cap copy (and rebuild in Xcode for a store release). The server and the rules deploy on their own.
11 The file map
bakebook/ the web app
index.html home & recipe library
recipe.html a single recipe + make-it mode
butter.html the AI baking assistant
logbook.html bake logs
share.html public recipe viewer (share links)
bakebook-store.js the sync engine (§03)
functions/index.js the Cloud Functions (§03)
firestore.rules who can read/write what
ios/ · android/ Capacitor native projects
12 Glossary
- localStorage
- Storage inside the browser/app on the device. bakebook's fast working copy.
- Firestore
- Firebase's cloud database; the durable backup.
- Merge / converge
- Reconciling the device copy and the cloud copy into one agreed truth.
- Tombstone
- A record that something was deleted, so it doesn't come back on sync.
- Cloud Function
- Code that runs on the server, not the phone. bakebook's gate to Claude.
- Debounce
- Wait a beat (700ms) after the last edit before syncing, so rapid edits push once.