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, updated 2026-10-04 (make-it notes and the bake-log pop-up, password sign-ups confirm their email so a Google sign-in can't wipe the password, full-screen butter sends its own saves, a delete in the first moment after opening stays deleted, a reconnect is always noticed, opening offline keeps the whole book, the logbook saves only bakes, saves send only the parts that changed, and a recipe says when it changed on another device, the event log, recipe import now limited by the server, the recipe data model corrected, Android + Play billing, bakebook.co, the unit sheet, the amount wheel, the learning ingredient library, time/serves pickers, the back gesture, the save hook fix so iPhone saves sync on their own) · 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
A variation is its own recipe linked to the original, and each bake is logged with what changed. Editing the recipe itself saves over it: there is no history of earlier edits yet.
parentId · logs[] · 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. A note can go on the step on screen or on the recipe. Leaving after a real bake (more than half the steps and more than 10 minutes) offers a quick pop-up to log it.
mapStepIngredients
Log a bake
A dated note per bake, merged safely across devices. A deleted bake stays deleted on every device. A bake logged from make-it with "later" shows a red dot on the log book button until its results are added.
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
Open, edit and delete with no connection; the whole book stays on screen, and changes upload when you're back.
markPending · startListener
Set an amount on a wheel
Flick, scroll, double-tap or hold to type. Each unit has its own grid (cups in quarters and thirds, spoons in eighths, grams 1 then 5); changing the unit converts the number.
bbAmountWheel · WHEEL_STEPS · convertAmount
Pick a unit from a sheet
A bottom sheet: the five units you use most, then weight, volume, count. Old spellings ("cup", "grams") are folded onto the one list.
bbUnitPicker · bbNormalizeUnit
An ingredient library that learns
Built from your recipes: names merge across case, plurals, accents and typos; amounts and units you used last come back; three suggestions as you type.
bakebook-ingredients.js · bbIngredientMatches
Time and serves
Tap the underlined word: a scrolling sheet (5-minute notches to 100 hours; serves from 0) or type it. Only "done" writes the pick.
bbTimeField · bbServesField
Android back that never quits
Back undoes one step (closes a sheet, a form, the recipe); on the home list it backgrounds the app. A native safety net catches a build with the plugin off.
bakebook-back.js · MainActivity.java
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.
Storage.prototype.setItem
(the save hook)
Every page saves by writing to localStorage. The store wraps that one write method, so any save anywhere schedules a cloud push without the page asking. Every page that saves recipes loads the store, including full-screen butter (since 30 Sep 2026; butter inside the sheet hands its saves to the page around it instead). Hooked on the shared blueprint (Storage.prototype) since 12 Sep 2026: hooking localStorage itself silently did nothing on iPhone, so no iPhone save synced on its own before then.
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. Since 30 Sep 2026 an existing recipe sends only the parts that changed (name, photos, a component, and so on), so an edit to a different part on another device survives. New bakes are added to the cloud's list rather than replacing it. A brand-new recipe is still sent whole. 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, and tells the recipe page it came from another device (each device has its own id, bakebook.deviceId). The page then shows "updated on another device", or while editing, "this recipe just changed on another device" and a keep mine / load the other version choice on save.
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. For a recipe with unsynced edits, only the parts this device changed keep the local version (the list is remembered across page loads in bakebook.changedParts); every other part takes the cloud's. Bakes from both copies are always kept, minus any on either copy's deleted-bakes list. An answer that comes only from the phone's own cache (opened offline) never removes a recipe; only the real server can (since 30 Sep 2026; before that, opening offline could show an empty book). A recipe counts as seen in the cloud only once the server has accepted it, so a new recipe whose upload was cut off (weak signal, page left) is never mistaken for one deleted on another device. After reconnecting, the page always notices the server's answer, even when nothing changed (the listener hears includeMetadataChanges).
getTombstones()
setTombstones()
Remember deletions so a recipe you deleted doesn't come back from the cloud on the next sync. A tombstone is only forgotten once the server confirms the delete, so a delete made offline survives until you are back online. A delete made before sign-in has finished (the first moment after a page opens) writes its tombstone straight away (noteEarlyDeletes), so it survives the page moving on.
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.
buildImportBody()
takeImportQuota()
Recipe import (photo or link) goes through the same proxy. The app sends only the photos or the link; the server writes the instructions, picks the cheaper model and a capped length, adds only the page-fetching tool, and allows 30 import calls a day, 100 for bakebook+. Phones on older builds still send their old request; the server reads only the photos or the link out of it. Lives in functions/import-guard.js. Added 28 Sep 2026; before that, import passed the browser's request through.
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. Email+password sign-ups get a confirmation email, and a home reminder stays until they confirm, because Google deletes the password of an unconfirmed email the first time its owner signs in with Google (proved 4 Oct 2026). The email text and link page are Firebase's own until Firebase Support unlocks template edits; confirm.html is ready for when they do. |
| 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 on iPhone, Google Play billing on Android (both wired, the server flag set by RevenueCat's webhook). |
| Domain | bakebook.co | The real address (registrar Vercel, DNS → Firebase Hosting). The old bakebook-gz9v92.web.app still works; both must be on Firebase Auth's allowed-domain list for sign-in. |
| Android | Capacitor 8 + Android Studio | Same web app in an Android shell; on Google Play closed testing. npx cap sync, never just copy, before a build. |
06 The recipe, as data
Every recipe is one document, stored under the signed-in baker's account.
users/{uid}/recipes/{recipeId}
namethe recipe name
components[]the parts of a recipe (crust, filling), each with its own ingredients, steps and step notes
ingredients[]rows of amount + unit + item + a note, editable as a table (a flat copy of all the parts)
steps[]the method, one step at a time (drives make-it mode)
notesrecipe-wide notes, shown below the method; private, not sent in a share link (since 4 Oct 2026)
categoryhow the baker organizes it
parentIdon a variation: the recipe it came from. A variation is a whole recipe of its own. (There is no stored history of edits to a recipe; a save replaces it.)
sourceon a recipe added from a share link: which share, who shared it, and when (recorded since 28 Sep 2026)
logs[]dated bake notes, with what changed and photos, merged by id across devices. A bake logged from make-it also has a time, and resultsPending until its results are added
deletedLogs[]ids of bakes deleted from this recipe, so no device can put one back in a merge (since 28 Sep 2026)
photos[]URLs into Cloud Storage (the images live there, not here)
updatedAtthe device's timestamp for the last edit (older phone builds still compare it)
serverUpdatedAtthe server's time for the last save, so a wrong phone clock can't fake "newer" (recorded since 30 Sep 2026; the merge does not use it yet, on the backlog)
lastDevicewhich device saved last, so a page can tell its own save from another device's (since 30 Sep 2026)
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 users/{uid}/…, owner only | Big image files don't belong in a document database; the recipe stores only a link. storage.rules only accepts images under 5 MB (since 28 Sep 2026). |
| 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. |
| The event log what people do, never what they wrote | users/{uid}/events add-only | One small entry per action: recipe made (and how), opened, bake logged, Make It started, butter asked, butter change accepted, recipe shared. bakebook-events.js queues each one in localStorage first, so an entry survives the page changing straight after. The rules let the browser add entries but never read, change or delete them. Read with tools/event-log-check.mjs. Deleted with the account (since 28 Sep 2026). |
| 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
You signed up with a password, then tapped "Sign in with Google"
If your email was never confirmed, Google replaces your password with Google sign-in. Your recipes are safe (same account), but the password stops working. Confirming the email (the home reminder) prevents it; a confirmed email keeps both ways to sign in.
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
Since 30 Sep 2026 each save sends only the parts it changed, so edits to different parts (the name here, a photo there) both survive. Two edits to the same part (the same component's ingredients) still collide: the later save wins that part. If you are editing when the other change arrives, you are told and choose "keep mine" or "load the other version". Known gap: a change that lands while this device's own automatic save is in flight is not announced. Phones on store builds from before 30 Sep still save whole recipes until they update.
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 sync for the app
How recipes save, sync or merge
bakebook/bakebook-store.js
firebase deploy --only hosting
+ npx cap sync
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
What photo files are accepted
storage.rules
firebase deploy --only storage
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 sync (never just
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)
confirm.html the "your email is confirmed" page (waits on Firebase Support)
bakebook-store.js the sync engine (§03)
bakebook-units.js the unit list, the unit sheet, unit spelling
bakebook-ingredients.js the learning ingredient library
bakebook-meta.js time + serves fields and their scrolling sheet
bakebook-back.js the Android back gesture, one step at a time
bakebook-billing.js RevenueCat: the paywall, Apple + Google keys
bakebook-butter-sync.js saved butter chats across devices
bakebook-events.js the event log: what people do, never what they wrote (§07)
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.