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.
Layer 1 · the app
Plain HTML, CSS & JavaScript
No framework. The pages a baker sees, in bakebook/.
wrapped by ↓
Layer 2 · the wrapper
Capacitor + Xcode
Packages the web app into a native iOS shell and submits it to the App Store. Also wraps for Android.
talks to ↓
Layer 3 · the backend
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

PartWhat runs itWhy
FrontendHTML · CSS · JavaScriptPlain and legible; the app is the vehicle for learning.
Native wrapperCapacitor + XcodeShip the web app to the App Store without rewriting it.
AccountsFirebase AuthSign-in; ties every recipe to a person.
DatabaseFirestoreRecipes, logs and saved chats, synced across devices.
FilesCloud StorageRecipe photos (the binary files).
Server logicCloud FunctionsThe gate in front of Claude, and the only writer of plan/ban state.
AI assistantClaude Sonnet 5butter, reached only through the proxy.
SubscriptionsRevenueCatbakebook+ 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

DataWhere it livesWhy 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 chatsFirestore
users/{uid}/…
The durable backup, tied to your account, so it syncs across devices and survives a reinstall.
Recipe photosCloud StorageBig image files don't belong in a document database; the recipe stores only a link.
Plan & ban stateusers/{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 recipeshares/{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 (startListenermergeSets), 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
Claude Sonnet
answers
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)
ios/
Capacitor project
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.