From 854aa99c4aeba5d78bf1f6b66d5357bc40b27316 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 23:27:42 +0000 Subject: [PATCH 01/21] docs: add domain glossary with account scope decision Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- CONTEXT.md | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 CONTEXT.md diff --git a/CONTEXT.md b/CONTEXT.md new file mode 100644 index 0000000..5d8a19b --- /dev/null +++ b/CONTEXT.md @@ -0,0 +1,29 @@ +# Local Catch — domain glossary + +Terms as this project uses them. Use these words in code, issues and docs. + +## Yields and costs + +- **Conversion** — going from one state of a fish to another, written + `From → To` (e.g. `Round → Skinless Fillet`). +- **Yield** — the percentage of weight kept by a conversion, 0 < yield ≤ 100. + Always a percent in the UI and API, never a fraction. +- **Reference yields** — the bundled yields from the MAB-37 publication + (`app/src/data/fish_data_v3.js`). Read-only, ship with the app, work offline. +- **Custom yield** — a yield a person records from their own processing. + Private to them unless shared. +- **Saved calculation** — a cost calculation a person chose to keep. + +## People and sharing + +- **Account** — optional. It exists for two reasons only: + 1. keeping a person's custom yields and saved calculations across devices; + 2. sharing custom yields to the community dataset. + The calculator never requires an account. +- **Shared yield** — a custom yield its owner has chosen to make public. +- **Community dataset** — all shared yields, readable by anyone, attributed by + display name or "Anonymous". Never attributed by email. +- **Display name** — the only public identity. Set by the person; optional. + +_Avoid:_ "contributor profile" (dropped — no bio/organization pages), +"username" for anything public. From 2b82eb62708db1f315e76a7c190d3149b0140a61 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 23:29:34 +0000 Subject: [PATCH 02/21] =?UTF-8?q?docs:=20ADR=200001=20=E2=80=94=20Firebase?= =?UTF-8?q?=20Auth=20and=20Firestore,=20no=20own=20server?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0001-firebase-auth-and-firestore.md | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 docs/adr/0001-firebase-auth-and-firestore.md diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md new file mode 100644 index 0000000..3dac570 --- /dev/null +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -0,0 +1,52 @@ +# 1. Firebase Auth and Firestore, with no server of our own + +Date: 2026-09-28 +Status: Accepted + +## Context + +Local Catch should be a very simple app. Today it runs two backends +(Express + SQLite for local dev, Vercel functions + Neon in production), a +shared handler layer, a custom offline store and sync engine, Firebase sign-in +and a legacy username/password path. Every API change has to be made twice. + +An **Account** has only two jobs (see `CONTEXT.md`): keep a person's custom +yields and saved calculations across devices, and share custom yields to the +community dataset. The live site has about two accounts, so moving them costs +almost nothing. + +## Decision + +Use Firebase Auth for sign-in and Cloud Firestore for data. The browser talks to +Firestore directly; Firestore security rules enforce who can read and write: + +- a person reads and writes only their own custom yields and saved + calculations; +- anyone can read shared yields; +- no document ever holds a public email. + +Firestore's offline cache is the offline store and sync. + +## Consequences + +- Delete `api/`, `server/`, `shared/`, the SQLite database, the Neon database, + the legacy JWT login and register endpoints, and the custom sync layer + (`localRepository.js`, `syncCoordinator.js` and helpers). +- The "make every API change in both backends" rule goes away. +- Excel/CSV import parses in the browser. +- The community dataset is also published as a CSV/JSON file so other tools + can use it without Firestore. +- Contributor profile pages and publishing saved calculations publicly are + dropped; neither is one of an account's two jobs. +- The app bundles the Firebase SDK, which is larger than today's REST calls. +- Local development and CI use the Firebase emulators. + +## Alternatives considered + +- **Supabase** (Postgres + auth + row-level security): about as simple, but + free projects pause after a week idle, and it would change the sign-in + system a third time. +- **Keep Neon, delete the Express/SQLite backend**: smallest change, but we'd + still own API code, token verification and a custom sync engine. +- **Firebase SQL Connect** (proved in `dataconnect/`): needs a paid Cloud SQL + instance; more than this app needs. From 9fe630eea200528a9ca13a26340aaa482f462032 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 28 Sep 2026 23:30:37 +0000 Subject: [PATCH 03/21] =?UTF-8?q?docs:=20ADR=200002=20=E2=80=94=20guests?= =?UTF-8?q?=20use=20Firebase=20anonymous=20sign-in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- CONTEXT.md | 3 ++ docs/adr/0002-guests-use-anonymous-sign-in.md | 36 +++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 docs/adr/0002-guests-use-anonymous-sign-in.md diff --git a/CONTEXT.md b/CONTEXT.md index 5d8a19b..ed42cc4 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -20,6 +20,9 @@ Terms as this project uses them. Use these words in code, issues and docs. 1. keeping a person's custom yields and saved calculations across devices; 2. sharing custom yields to the community dataset. The calculator never requires an account. +- **Guest** — someone using the app without signing in. The first time a guest + saves something, they get an anonymous Firebase account; signing in later + keeps the same data. Clearing browser data loses a guest's data. - **Shared yield** — a custom yield its owner has chosen to make public. - **Community dataset** — all shared yields, readable by anyone, attributed by display name or "Anonymous". Never attributed by email. diff --git a/docs/adr/0002-guests-use-anonymous-sign-in.md b/docs/adr/0002-guests-use-anonymous-sign-in.md new file mode 100644 index 0000000..be4307f --- /dev/null +++ b/docs/adr/0002-guests-use-anonymous-sign-in.md @@ -0,0 +1,36 @@ +# 2. Guests use Firebase anonymous sign-in + +Date: 2026-09-28 +Status: Accepted + +## Context + +The calculator works without an account, and people should be able to save +custom yields and calculations before deciding to sign up. Today the app keeps +guest data in the browser and runs its own code to copy it into the account on +sign-in (`guestAdoption.js`, `legacyMigration.js`). + +## Decision + +The first time a guest saves something, sign them in anonymously with Firebase +Auth. Their data lives in Firestore under that anonymous user, with the same +security rules as any account. When they sign in with Google or email, link the +new credential to the anonymous user so the data stays with them. + +Nothing is saved until the guest's first save, so people who only use the +calculator never get an account. + +## Consequences + +- Delete the guest-adoption and legacy-migration code. +- Clearing browser data loses a guest's data, as it does today. +- Unused anonymous accounts accumulate in Firebase and may need occasional + cleanup. +- Sharing to the community dataset requires a non-anonymous account, so every + shared yield has a real owner. + +## Alternatives considered + +- **Sign in to save anything**: simplest, but loses "save first, sign up later". +- **Keep browser-only guest storage and copy on sign-in**: today's approach; + custom code we'd have to maintain. From 47e09e4e4d5626aae9ee6b2f7bf9c52bc444ced7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 04:31:10 +0000 Subject: [PATCH 04/21] docs: match glossary and ADRs to the grill-with-docs formats Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- CONTEXT.md | 91 ++++++++++++------- docs/adr/0001-firebase-auth-and-firestore.md | 76 ++++++---------- docs/adr/0002-guests-use-anonymous-sign-in.md | 43 +++------ 3 files changed, 102 insertions(+), 108 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index ed42cc4..1229601 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -1,32 +1,59 @@ -# Local Catch — domain glossary - -Terms as this project uses them. Use these words in code, issues and docs. - -## Yields and costs - -- **Conversion** — going from one state of a fish to another, written - `From → To` (e.g. `Round → Skinless Fillet`). -- **Yield** — the percentage of weight kept by a conversion, 0 < yield ≤ 100. - Always a percent in the UI and API, never a fraction. -- **Reference yields** — the bundled yields from the MAB-37 publication - (`app/src/data/fish_data_v3.js`). Read-only, ship with the app, work offline. -- **Custom yield** — a yield a person records from their own processing. - Private to them unless shared. -- **Saved calculation** — a cost calculation a person chose to keep. - -## People and sharing - -- **Account** — optional. It exists for two reasons only: - 1. keeping a person's custom yields and saved calculations across devices; - 2. sharing custom yields to the community dataset. - The calculator never requires an account. -- **Guest** — someone using the app without signing in. The first time a guest - saves something, they get an anonymous Firebase account; signing in later - keeps the same data. Clearing browser data loses a guest's data. -- **Shared yield** — a custom yield its owner has chosen to make public. -- **Community dataset** — all shared yields, readable by anyone, attributed by - display name or "Anonymous". Never attributed by email. -- **Display name** — the only public identity. Set by the person; optional. - -_Avoid:_ "contributor profile" (dropped — no bio/organization pages), -"username" for anything public. +# Local Catch + +A yield and cost calculator for fishermen and fishmongers. It turns what a fish +costs whole into what each finished product really costs, and lets people keep +and share the yields they measure themselves. + +## Language + +### Yields and costs + +**Conversion**: +Going from one form of a fish to another, written `From → To` (e.g. +`Round → Skinless Fillet`). +_Avoid_: cut, transformation + +**Yield**: +The share of weight kept by a conversion, as a percentage above 0 and up to 100. +_Avoid_: recovery rate, fraction (0.42) + +**Reference yield**: +A published yield from the MAB-37 research that ships with the app. +_Avoid_: default yield, static data + +**Custom yield**: +A yield a person measured from their own processing. Private unless shared. +_Avoid_: user data, my data + +**Saved calculation**: +A cost or weight calculation a person chose to keep. +_Avoid_: calc, saved calc + +### People and sharing + +**Account**: +An optional sign-in that exists for two reasons only: keeping a person's custom +yields and saved calculations across devices, and sharing custom yields. +_Avoid_: user, profile + +**Guest**: +Someone using the app without signing in. A guest can save, but loses what they +saved if they clear their browser data, and cannot share. +_Avoid_: anonymous user + +**Shared yield**: +A custom yield its owner chose to make public in the community dataset. +_Avoid_: published yield, contribution + +**Community dataset**: +All shared yields, readable by anyone, attributed only by display name or as +"Anonymous". +_Avoid_: community pool, public data + +**Display name**: +The only public identity a person has. Optional, chosen by them, never their +email. +_Avoid_: username, contributor name + +_Dropped concepts_: contributor profile (bio/organization pages) and publishing +saved calculations publicly. Neither is one of an account's two jobs. diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index 3dac570..43b667c 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -1,52 +1,36 @@ -# 1. Firebase Auth and Firestore, with no server of our own - -Date: 2026-09-28 -Status: Accepted - -## Context - -Local Catch should be a very simple app. Today it runs two backends -(Express + SQLite for local dev, Vercel functions + Neon in production), a -shared handler layer, a custom offline store and sync engine, Firebase sign-in -and a legacy username/password path. Every API change has to be made twice. - -An **Account** has only two jobs (see `CONTEXT.md`): keep a person's custom -yields and saved calculations across devices, and share custom yields to the -community dataset. The live site has about two accounts, so moving them costs -almost nothing. - -## Decision - -Use Firebase Auth for sign-in and Cloud Firestore for data. The browser talks to -Firestore directly; Firestore security rules enforce who can read and write: - -- a person reads and writes only their own custom yields and saved - calculations; -- anyone can read shared yields; -- no document ever holds a public email. - -Firestore's offline cache is the offline store and sync. +--- +status: accepted +--- + +# Firebase Auth and Firestore, with no server of our own + +The app ran two backends (Express + SQLite locally, Vercel functions + Neon in +production), a shared handler layer, a custom offline sync engine and two +sign-in systems, for about two accounts whose only jobs are keeping custom +yields and saved calculations across devices and sharing custom yields. We +chose Firebase Auth plus Cloud Firestore, reached directly from the browser: +security rules decide who reads and writes what, and Firestore's offline cache +replaces our sync engine. Firebase Auth was already in use, so no one signs up +again. + +## Considered Options + +- **Supabase** (Postgres, auth, row-level security): about as simple, but free + projects pause after a week idle, and it would change the sign-in system a + third time. +- **Keep Neon and delete the Express/SQLite backend** (the direction in + `AUDIT_REPORT.md` item 2.1): smallest change, but we'd still own API code, + token verification and a sync engine. +- **Firebase SQL Connect** (proved in `dataconnect/`): needs a paid Cloud SQL + instance; more than this app needs. ## Consequences -- Delete `api/`, `server/`, `shared/`, the SQLite database, the Neon database, - the legacy JWT login and register endpoints, and the custom sync layer - (`localRepository.js`, `syncCoordinator.js` and helpers). -- The "make every API change in both backends" rule goes away. +- `api/`, `server/`, `shared/`, SQLite, Neon, the legacy password login and the + custom sync layer go away, and so does the rule to change both backends. - Excel/CSV import parses in the browser. -- The community dataset is also published as a CSV/JSON file so other tools +- The community dataset is also published as a CSV/JSON file, so other tools can use it without Firestore. -- Contributor profile pages and publishing saved calculations publicly are - dropped; neither is one of an account's two jobs. -- The app bundles the Firebase SDK, which is larger than today's REST calls. +- The Firebase SDK makes the app download larger than today's REST calls. - Local development and CI use the Firebase emulators. - -## Alternatives considered - -- **Supabase** (Postgres + auth + row-level security): about as simple, but - free projects pause after a week idle, and it would change the sign-in - system a third time. -- **Keep Neon, delete the Express/SQLite backend**: smallest change, but we'd - still own API code, token verification and a custom sync engine. -- **Firebase SQL Connect** (proved in `dataconnect/`): needs a paid Cloud SQL - instance; more than this app needs. +- Supersedes `docs/AUTH_MIGRATION_ROADMAP.md` (Better Auth + Cloudflare). diff --git a/docs/adr/0002-guests-use-anonymous-sign-in.md b/docs/adr/0002-guests-use-anonymous-sign-in.md index be4307f..1b4e22d 100644 --- a/docs/adr/0002-guests-use-anonymous-sign-in.md +++ b/docs/adr/0002-guests-use-anonymous-sign-in.md @@ -1,36 +1,19 @@ -# 2. Guests use Firebase anonymous sign-in +--- +status: accepted +--- -Date: 2026-09-28 -Status: Accepted +# Guests use Firebase anonymous sign-in -## Context - -The calculator works without an account, and people should be able to save -custom yields and calculations before deciding to sign up. Today the app keeps -guest data in the browser and runs its own code to copy it into the account on -sign-in (`guestAdoption.js`, `legacyMigration.js`). - -## Decision - -The first time a guest saves something, sign them in anonymously with Firebase -Auth. Their data lives in Firestore under that anonymous user, with the same -security rules as any account. When they sign in with Google or email, link the -new credential to the anonymous user so the data stays with them. - -Nothing is saved until the guest's first save, so people who only use the +People should be able to save before deciding to sign up. Instead of keeping +guest data in the browser and copying it into an account with our own code, a +guest's first save signs them in anonymously; their data lives in Firestore +under the same rules as any account, and signing in with Google or email later +links to that same user so nothing is copied. People who only use the calculator never get an account. ## Consequences -- Delete the guest-adoption and legacy-migration code. -- Clearing browser data loses a guest's data, as it does today. -- Unused anonymous accounts accumulate in Firebase and may need occasional - cleanup. -- Sharing to the community dataset requires a non-anonymous account, so every - shared yield has a real owner. - -## Alternatives considered - -- **Sign in to save anything**: simplest, but loses "save first, sign up later". -- **Keep browser-only guest storage and copy on sign-in**: today's approach; - custom code we'd have to maintain. +- A guest who clears browser data loses what they saved, as today. +- Unused anonymous accounts pile up and may need occasional cleanup. +- Only a non-anonymous account can share, so every shared yield has a real + owner. From 05dadd6d03f87d354517f2b2f7fd1f82f29668c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 12:52:10 +0000 Subject: [PATCH 05/21] =?UTF-8?q?docs:=20ADR=200001=20=E2=80=94=20host=20t?= =?UTF-8?q?he=20static=20app=20on=20Firebase=20Hosting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0001-firebase-auth-and-firestore.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index 43b667c..b4e138f 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -2,7 +2,7 @@ status: accepted --- -# Firebase Auth and Firestore, with no server of our own +# Firebase Auth, Firestore and Firebase Hosting, with no server of our own The app ran two backends (Express + SQLite locally, Vercel functions + Neon in production), a shared handler layer, a custom offline sync engine and two @@ -10,8 +10,9 @@ sign-in systems, for about two accounts whose only jobs are keeping custom yields and saved calculations across devices and sharing custom yields. We chose Firebase Auth plus Cloud Firestore, reached directly from the browser: security rules decide who reads and writes what, and Firestore's offline cache -replaces our sync engine. Firebase Auth was already in use, so no one signs up -again. +replaces our sync engine. The static app is served from Firebase Hosting, so +the whole app lives with one provider. Firebase Auth was already in use, so no +one signs up again. ## Considered Options @@ -33,4 +34,7 @@ again. can use it without Firestore. - The Firebase SDK makes the app download larger than today's REST calls. - Local development and CI use the Firebase emulators. +- The address moves from `*.vercel.app` to `*.web.app`. The Vercel project stays + only as a redirect for a while, so installed copies of the app and bookmarks + keep working. PR previews come from Firebase preview channels. - Supersedes `docs/AUTH_MIGRATION_ROADMAP.md` (Better Auth + Cloudflare). From 58284ee84f55ed0e82487ca469ec12a52bf83b21 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 13:01:41 +0000 Subject: [PATCH 06/21] docs: submitted yields go public only after review Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- CONTEXT.md | 23 ++++++++++++------- docs/adr/0001-firebase-auth-and-firestore.md | 2 +- docs/adr/0002-guests-use-anonymous-sign-in.md | 4 ++-- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 1229601..7e6c7cf 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -22,31 +22,38 @@ A published yield from the MAB-37 research that ships with the app. _Avoid_: default yield, static data **Custom yield**: -A yield a person measured from their own processing. Private unless shared. +A yield a person measured from their own processing. Private unless approved +into the community dataset. _Avoid_: user data, my data **Saved calculation**: A cost or weight calculation a person chose to keep. _Avoid_: calc, saved calc -### People and sharing +### People and the community dataset **Account**: An optional sign-in that exists for two reasons only: keeping a person's custom -yields and saved calculations across devices, and sharing custom yields. +yields and saved calculations across devices, and submitting custom yields to +the community dataset. _Avoid_: user, profile **Guest**: Someone using the app without signing in. A guest can save, but loses what they -saved if they clear their browser data, and cannot share. +saved if they clear their browser data, and cannot submit yields. _Avoid_: anonymous user -**Shared yield**: -A custom yield its owner chose to make public in the community dataset. -_Avoid_: published yield, contribution +**Submitted yield**: +A custom yield its owner has asked to add to the community dataset. It stays +private until a reviewer approves it. +_Avoid_: shared yield, published yield, contribution + +**Reviewer**: +The person who approves or rejects submitted yields. +_Avoid_: admin, moderator **Community dataset**: -All shared yields, readable by anyone, attributed only by display name or as +All approved yields, readable by anyone, attributed only by display name or as "Anonymous". _Avoid_: community pool, public data diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index b4e138f..1ac6391 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -7,7 +7,7 @@ status: accepted The app ran two backends (Express + SQLite locally, Vercel functions + Neon in production), a shared handler layer, a custom offline sync engine and two sign-in systems, for about two accounts whose only jobs are keeping custom -yields and saved calculations across devices and sharing custom yields. We +yields and saved calculations across devices and submitting custom yields. We chose Firebase Auth plus Cloud Firestore, reached directly from the browser: security rules decide who reads and writes what, and Firestore's offline cache replaces our sync engine. The static app is served from Firebase Hosting, so diff --git a/docs/adr/0002-guests-use-anonymous-sign-in.md b/docs/adr/0002-guests-use-anonymous-sign-in.md index 1b4e22d..6e050f9 100644 --- a/docs/adr/0002-guests-use-anonymous-sign-in.md +++ b/docs/adr/0002-guests-use-anonymous-sign-in.md @@ -15,5 +15,5 @@ calculator never get an account. - A guest who clears browser data loses what they saved, as today. - Unused anonymous accounts pile up and may need occasional cleanup. -- Only a non-anonymous account can share, so every shared yield has a real - owner. +- Only a non-anonymous account can submit yields to the community dataset, so + every submitted yield has a real owner. From 628d952573b610aa68891369e7a528905f6983bb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 13:03:42 +0000 Subject: [PATCH 07/21] docs: editing an approved yield resubmits it for review Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- CONTEXT.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CONTEXT.md b/CONTEXT.md index 7e6c7cf..25f9631 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -45,7 +45,8 @@ _Avoid_: anonymous user **Submitted yield**: A custom yield its owner has asked to add to the community dataset. It stays -private until a reviewer approves it. +private until a reviewer approves it. Editing an approved yield takes it out of +the dataset and submits it again. _Avoid_: shared yield, published yield, contribution **Reviewer**: From 7fe0fad3f90f5bb7f2bbc5ef2f8cd3e67d1570e4 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 13:52:43 +0000 Subject: [PATCH 08/21] =?UTF-8?q?docs:=20ADR=200003=20=E2=80=94=20five=20p?= =?UTF-8?q?ages;=20inventory=20is=20a=20separate=20tool?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0003-five-pages-inventory-is-separate.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 docs/adr/0003-five-pages-inventory-is-separate.md diff --git a/docs/adr/0003-five-pages-inventory-is-separate.md b/docs/adr/0003-five-pages-inventory-is-separate.md new file mode 100644 index 0000000..3c2f0a6 --- /dev/null +++ b/docs/adr/0003-five-pages-inventory-is-separate.md @@ -0,0 +1,13 @@ +--- +status: accepted +--- + +# Five pages; inventory is a separate tool + +The app is only the calculator and what an account needs around it: +Calculator, My data (custom yields, saved calculations, import, submit), +Community (the community dataset and where reference yields come from), About, +and Review (reviewers only). Sign-in is a panel, not a page. Contributor +profiles, the roadmap page and the "Inventory — coming soon" placeholder are +removed. Inventory belongs in a separate tool in the harvester suite, not in +this app, so it should not be added back here. From 8d25faf576c4cdd17edd91022c668082c5203061 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:32:21 +0000 Subject: [PATCH 09/21] docs: rejected yields return to private with an optional review note Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- CONTEXT.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index 25f9631..6ddfcf6 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -45,10 +45,16 @@ _Avoid_: anonymous user **Submitted yield**: A custom yield its owner has asked to add to the community dataset. It stays -private until a reviewer approves it. Editing an approved yield takes it out of -the dataset and submits it again. +private until a reviewer approves it. A rejected yield goes back to private, +where its owner can fix it and submit it again. Editing an approved yield takes +it out of the dataset and submits it again. _Avoid_: shared yield, published yield, contribution +**Review note**: +An optional message from the reviewer to a yield's owner saying why it was +rejected. Only the owner sees it. +_Avoid_: rejection reason, feedback + **Reviewer**: The person who approves or rejects submitted yields. _Avoid_: admin, moderator From 09dcc65293904f6378505c478909991a4c2ca14f Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:33:00 +0000 Subject: [PATCH 10/21] =?UTF-8?q?docs:=20ADR=200004=20=E2=80=94=20communit?= =?UTF-8?q?y=20dataset=20is=20CC=20BY=204.0,=20download=20now?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- CONTEXT.md | 4 ++-- docs/adr/0004-community-dataset-cc-by.md | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 docs/adr/0004-community-dataset-cc-by.md diff --git a/CONTEXT.md b/CONTEXT.md index 6ddfcf6..fce8a57 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -60,8 +60,8 @@ The person who approves or rejects submitted yields. _Avoid_: admin, moderator **Community dataset**: -All approved yields, readable by anyone, attributed only by display name or as -"Anonymous". +All approved yields, readable and downloadable by anyone under CC BY 4.0, +attributed only by display name or as "Anonymous". _Avoid_: community pool, public data **Display name**: diff --git a/docs/adr/0004-community-dataset-cc-by.md b/docs/adr/0004-community-dataset-cc-by.md new file mode 100644 index 0000000..49758ea --- /dev/null +++ b/docs/adr/0004-community-dataset-cc-by.md @@ -0,0 +1,19 @@ +--- +status: accepted +--- + +# Community dataset is CC BY 4.0; download now, versioned releases later + +Submitting a yield means its owner agrees that, once approved, it is released +under CC BY 4.0 with attribution by display name or "Anonymous"; the submit +dialog says so. The code stays MIT. A licence on contributed data cannot be +narrowed after the fact, so this is set before any submissions under the new +flow. For now the Community page offers a CSV/JSON download built from the +approved yields. Scheduled, versioned releases (a GitHub Action exporting a +numbered file) will be added once there are enough approved yields to be worth +citing. + +## Consequences + +- Yields shared before this consent existed are not in a licensed release until + their owners submit them again under the new terms. From 7e6f8e951dd658d17bc3b8a7d05c5a722fd7efd6 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:33:12 +0000 Subject: [PATCH 11/21] =?UTF-8?q?docs:=20ADR=200004=20=E2=80=94=20legacy?= =?UTF-8?q?=20shared=20yields=20need=20owner=20consent=20to=20be=20approve?= =?UTF-8?q?d?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0004-community-dataset-cc-by.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/adr/0004-community-dataset-cc-by.md b/docs/adr/0004-community-dataset-cc-by.md index 49758ea..9394731 100644 --- a/docs/adr/0004-community-dataset-cc-by.md +++ b/docs/adr/0004-community-dataset-cc-by.md @@ -15,5 +15,7 @@ citing. ## Consequences -- Yields shared before this consent existed are not in a licensed release until - their owners submit them again under the new terms. +- Yields shared before this consent existed enter the community dataset only + with their owner's agreement to CC BY 4.0: the one-time copy from Neon marks + them approved only for owners who have agreed, and leaves the rest private + for their owners to submit again. From c5a797d88c264dad02ad27b31acfd756862ccefd Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:42:16 +0000 Subject: [PATCH 12/21] docs: cover existing-account sign-in, reviewer rights and the address move Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- CONTEXT.md | 5 +++-- docs/adr/0001-firebase-auth-and-firestore.md | 11 ++++++++--- docs/adr/0002-guests-use-anonymous-sign-in.md | 6 ++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/CONTEXT.md b/CONTEXT.md index fce8a57..c35fdca 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -35,7 +35,7 @@ _Avoid_: calc, saved calc **Account**: An optional sign-in that exists for two reasons only: keeping a person's custom yields and saved calculations across devices, and submitting custom yields to -the community dataset. +the community dataset. A reviewer's account can also review submitted yields. _Avoid_: user, profile **Guest**: @@ -56,7 +56,8 @@ rejected. Only the owner sees it. _Avoid_: rejection reason, feedback **Reviewer**: -The person who approves or rejects submitted yields. +An account that has been granted the right to approve or reject submitted +yields. Being signed in is not enough. _Avoid_: admin, moderator **Community dataset**: diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index 1ac6391..0023280 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -34,7 +34,12 @@ one signs up again. can use it without Firestore. - The Firebase SDK makes the app download larger than today's REST calls. - Local development and CI use the Firebase emulators. -- The address moves from `*.vercel.app` to `*.web.app`. The Vercel project stays - only as a redirect for a while, so installed copies of the app and bookmarks - keep working. PR previews come from Firebase preview channels. +- The address moves from `*.vercel.app` to `*.web.app`. Browser storage belongs + to an address, so a redirect cannot carry a guest's saved data or unsynced + edits across. Before the redirect is switched on, the last version on the old + address asks guests to sign in so their data reaches their account, and + finishes syncing signed-in users; only then is Neon copied and Vercel reduced + to a redirect. Bookmarks follow the redirect; installed copies of the app + need to be installed again from the new address. PR previews come from + Firebase preview channels. - Supersedes `docs/AUTH_MIGRATION_ROADMAP.md` (Better Auth + Cloudflare). diff --git a/docs/adr/0002-guests-use-anonymous-sign-in.md b/docs/adr/0002-guests-use-anonymous-sign-in.md index 6e050f9..91ef874 100644 --- a/docs/adr/0002-guests-use-anonymous-sign-in.md +++ b/docs/adr/0002-guests-use-anonymous-sign-in.md @@ -13,6 +13,12 @@ calculator never get an account. ## Consequences +- If the guest signs in with a Google account or email that already belongs to + an account, Firebase cannot link it to the anonymous user. In that case the + app reads the guest's custom yields and saved calculations while still signed + in as the guest, signs in to the existing account, and writes them there. + This copy is the one piece of guest-transfer code we keep. + - A guest who clears browser data loses what they saved, as today. - Unused anonymous accounts pile up and may need occasional cleanup. - Only a non-anonymous account can submit yields to the community dataset, so From 0f98d9c1846b8de4e44c82860c4aa3af3db97395 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:42:44 +0000 Subject: [PATCH 13/21] docs: guest-data handoff, display-name-only attribution Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0002-guests-use-anonymous-sign-in.md | 7 +++++++ docs/adr/0004-community-dataset-cc-by.md | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/adr/0002-guests-use-anonymous-sign-in.md b/docs/adr/0002-guests-use-anonymous-sign-in.md index 91ef874..eb06cf7 100644 --- a/docs/adr/0002-guests-use-anonymous-sign-in.md +++ b/docs/adr/0002-guests-use-anonymous-sign-in.md @@ -13,6 +13,13 @@ calculator never get an account. ## Consequences +- Guest data saved by the current app (in the browser, not Firestore) is not + read by the new app. It moves over the existing way before the switch: the + current app asks guests to sign in, its adoption code moves their records + into the account, and they reach Firestore with the one-time copy from Neon + (see ADR 0001). The old guest storage and adoption code are removed only + after that. + - If the guest signs in with a Google account or email that already belongs to an account, Firebase cannot link it to the anonymous user. In that case the app reads the guest's custom yields and saved calculations while still signed diff --git a/docs/adr/0004-community-dataset-cc-by.md b/docs/adr/0004-community-dataset-cc-by.md index 9394731..4662609 100644 --- a/docs/adr/0004-community-dataset-cc-by.md +++ b/docs/adr/0004-community-dataset-cc-by.md @@ -6,7 +6,8 @@ status: accepted Submitting a yield means its owner agrees that, once approved, it is released under CC BY 4.0 with attribution by display name or "Anonymous"; the submit -dialog says so. The code stays MIT. A licence on contributed data cannot be +dialog says so. No other identifying field (such as the organization from the +old contributor profiles) is published or exported. The code stays MIT. A license on contributed data cannot be narrowed after the fact, so this is set before any submissions under the new flow. For now the Community page offers a CSV/JSON download built from the approved yields. Scheduled, versioned releases (a GitHub Action exporting a From 27d36a8bc3bf04db0b122b103ac16e932a26c221 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:47:57 +0000 Subject: [PATCH 14/21] docs: freeze writes on the old API before the Neon copy Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0001-firebase-auth-and-firestore.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index 0023280..eff404c 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -38,8 +38,9 @@ one signs up again. to an address, so a redirect cannot carry a guest's saved data or unsynced edits across. Before the redirect is switched on, the last version on the old address asks guests to sign in so their data reaches their account, and - finishes syncing signed-in users; only then is Neon copied and Vercel reduced - to a redirect. Bookmarks follow the redirect; installed copies of the app + finishes syncing signed-in users. Then the old API stops accepting writes + (it answers them with an error), so nothing changes in Neon while it is + copied. Only after the copy has been checked is Vercel reduced to a redirect. Bookmarks follow the redirect; installed copies of the app need to be installed again from the new address. PR previews come from Firebase preview channels. - Supersedes `docs/AUTH_MIGRATION_ROADMAP.md` (Better Auth + Cloudflare). From 9e5b663b5271b2856ca005e22c1e7f8736528f7a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:48:08 +0000 Subject: [PATCH 15/21] docs: rewrap ADR 0001 cutover paragraph Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0001-firebase-auth-and-firestore.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index eff404c..a3da558 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -40,7 +40,8 @@ one signs up again. address asks guests to sign in so their data reaches their account, and finishes syncing signed-in users. Then the old API stops accepting writes (it answers them with an error), so nothing changes in Neon while it is - copied. Only after the copy has been checked is Vercel reduced to a redirect. Bookmarks follow the redirect; installed copies of the app - need to be installed again from the new address. PR previews come from - Firebase preview channels. + copied. Only after the copy has been checked is Vercel reduced to a + redirect. Bookmarks follow the redirect; installed copies of the app need to + be installed again from the new address. PR previews come from Firebase + preview channels. - Supersedes `docs/AUTH_MIGRATION_ROADMAP.md` (Better Auth + Cloudflare). From c22070a528cfcad5dcaccfe32e3ed14a89baf7e8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 15:54:31 +0000 Subject: [PATCH 16/21] docs: cover offline guests, public fields and last-write-wins Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0001-firebase-auth-and-firestore.md | 13 +++++++++++-- docs/adr/0002-guests-use-anonymous-sign-in.md | 8 ++++++-- docs/adr/0004-community-dataset-cc-by.md | 5 +++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index a3da558..34132b3 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -30,8 +30,17 @@ one signs up again. - `api/`, `server/`, `shared/`, SQLite, Neon, the legacy password login and the custom sync layer go away, and so does the rule to change both backends. - Excel/CSV import parses in the browser. -- The community dataset is also published as a CSV/JSON file, so other tools - can use it without Firestore. +- Security rules cannot hide single fields of a readable document, so approved + yields are copied into a separate public collection holding only the public + fields (species, conversion, yield, source, display name). The owner's user + id, submission details and review note stay in the owner's private document. + The reviewer's approval writes both in one batch. +- Our sync engine's revision check goes away: when the same record is edited + on two devices while one is offline, the later write wins. We accept this + because the accounts belong to one person; the conflict screen is dropped. +- The community dataset can be downloaded as CSV/JSON from the Community page. + A stable file address for other tools comes with the versioned releases in + ADR 0004. - The Firebase SDK makes the app download larger than today's REST calls. - Local development and CI use the Firebase emulators. - The address moves from `*.vercel.app` to `*.web.app`. Browser storage belongs diff --git a/docs/adr/0002-guests-use-anonymous-sign-in.md b/docs/adr/0002-guests-use-anonymous-sign-in.md index eb06cf7..54d73f6 100644 --- a/docs/adr/0002-guests-use-anonymous-sign-in.md +++ b/docs/adr/0002-guests-use-anonymous-sign-in.md @@ -9,7 +9,9 @@ guest data in the browser and copying it into an account with our own code, a guest's first save signs them in anonymously; their data lives in Firestore under the same rules as any account, and signing in with Google or email later links to that same user so nothing is copied. People who only use the -calculator never get an account. +calculator never get an account. Anonymous sign-in needs the network, so a save +made offline before the guest has a user is kept in the browser and written to +Firestore once sign-in succeeds. ## Consequences @@ -27,6 +29,8 @@ calculator never get an account. This copy is the one piece of guest-transfer code we keep. - A guest who clears browser data loses what they saved, as today. -- Unused anonymous accounts pile up and may need occasional cleanup. +- Unused anonymous accounts pile up and may need occasional cleanup. Deleting + an Auth user does not delete its Firestore documents, so cleanup removes the + user's custom yields and saved calculations along with the user. - Only a non-anonymous account can submit yields to the community dataset, so every submitted yield has a real owner. diff --git a/docs/adr/0004-community-dataset-cc-by.md b/docs/adr/0004-community-dataset-cc-by.md index 4662609..93dfe23 100644 --- a/docs/adr/0004-community-dataset-cc-by.md +++ b/docs/adr/0004-community-dataset-cc-by.md @@ -11,8 +11,9 @@ old contributor profiles) is published or exported. The code stays MIT. A licens narrowed after the fact, so this is set before any submissions under the new flow. For now the Community page offers a CSV/JSON download built from the approved yields. Scheduled, versioned releases (a GitHub Action exporting a -numbered file) will be added once there are enough approved yields to be worth -citing. +numbered file at a stable address other tools can fetch) will be added once +there are enough approved yields to be worth citing. Until then the dataset has +no fixed download address. ## Consequences From bf5cd82791f2d4ae5cc3a563dac9906be0d3550e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 16:01:33 +0000 Subject: [PATCH 17/21] docs: cover legacy logins, starting form and public copy removal Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0001-firebase-auth-and-firestore.md | 38 ++++++++++++-------- docs/adr/0004-community-dataset-cc-by.md | 3 +- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index 34132b3..e14550f 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -10,9 +10,9 @@ sign-in systems, for about two accounts whose only jobs are keeping custom yields and saved calculations across devices and submitting custom yields. We chose Firebase Auth plus Cloud Firestore, reached directly from the browser: security rules decide who reads and writes what, and Firestore's offline cache -replaces our sync engine. The static app is served from Firebase Hosting, so -the whole app lives with one provider. Firebase Auth was already in use, so no -one signs up again. +replaces our sync engine. The static app is served from Firebase Hosting, so the +whole app lives with one provider. Firebase Auth was already in use, so no one +signs up again. ## Considered Options @@ -34,12 +34,14 @@ one signs up again. yields are copied into a separate public collection holding only the public fields (species, conversion, yield, source, display name). The owner's user id, submission details and review note stay in the owner's private document. - The reviewer's approval writes both in one batch. -- Our sync engine's revision check goes away: when the same record is edited - on two devices while one is offline, the later write wins. We accept this - because the accounts belong to one person; the conflict screen is dropped. -- The community dataset can be downloaded as CSV/JSON from the Community page. - A stable file address for other tools comes with the versioned releases in + The reviewer's approval writes both in one batch. Editing or deleting an + approved yield deletes its public copy in the same batch, and the rules let an + owner delete the public copy of their own yield. +- Our sync engine's revision check goes away: when the same record is edited on + two devices while one is offline, the later write wins. We accept this because + the accounts belong to one person; the conflict screen is dropped. +- The community dataset can be downloaded as CSV/JSON from the Community page. A + stable file address for other tools comes with the versioned releases in ADR 0004. - The Firebase SDK makes the app download larger than today's REST calls. - Local development and CI use the Firebase emulators. @@ -47,10 +49,16 @@ one signs up again. to an address, so a redirect cannot carry a guest's saved data or unsynced edits across. Before the redirect is switched on, the last version on the old address asks guests to sign in so their data reaches their account, and - finishes syncing signed-in users. Then the old API stops accepting writes - (it answers them with an error), so nothing changes in Neon while it is - copied. Only after the copy has been checked is Vercel reduced to a - redirect. Bookmarks follow the redirect; installed copies of the app need to - be installed again from the new address. PR previews come from Firebase - preview channels. + finishes syncing signed-in users. Then the old API stops accepting writes (it + answers them with an error), so nothing changes in Neon while it is copied. + The copy matches Neon accounts to Firebase users by their Firebase user id; an + account with only the legacy password login must first sign in on the old + address with Google or email using the same verified email, which links it, + and the copy stops rather than skip an account that holds data but has no + Firebase user. Neon custom yields record only the finished product, not the + starting form, so the copy leaves the starting form blank for the owner to + fill in. Only after the copy has been checked is Vercel reduced to a redirect. + Bookmarks follow the redirect; installed copies of the app need to be + installed again from the new address. PR previews come from Firebase preview + channels. - Supersedes `docs/AUTH_MIGRATION_ROADMAP.md` (Better Auth + Cloudflare). diff --git a/docs/adr/0004-community-dataset-cc-by.md b/docs/adr/0004-community-dataset-cc-by.md index 93dfe23..19d3939 100644 --- a/docs/adr/0004-community-dataset-cc-by.md +++ b/docs/adr/0004-community-dataset-cc-by.md @@ -20,4 +20,5 @@ no fixed download address. - Yields shared before this consent existed enter the community dataset only with their owner's agreement to CC BY 4.0: the one-time copy from Neon marks them approved only for owners who have agreed, and leaves the rest private - for their owners to submit again. + for their owners to submit again. A copied yield whose starting form is + still blank stays private until its owner fills it in and submits it. From a59f12b0404207ececf9515e763b364d16fb1c12 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 16:08:33 +0000 Subject: [PATCH 18/21] docs: map legacy accounts explicitly and keep the offline cache Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0001-firebase-auth-and-firestore.md | 27 ++++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index e14550f..36f3f90 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -9,10 +9,10 @@ production), a shared handler layer, a custom offline sync engine and two sign-in systems, for about two accounts whose only jobs are keeping custom yields and saved calculations across devices and submitting custom yields. We chose Firebase Auth plus Cloud Firestore, reached directly from the browser: -security rules decide who reads and writes what, and Firestore's offline cache -replaces our sync engine. The static app is served from Firebase Hosting, so the -whole app lives with one provider. Firebase Auth was already in use, so no one -signs up again. +security rules decide who reads and writes what, and Firestore's persistent +offline cache (so offline edits survive a reload) replaces our sync engine. The +static app is served from Firebase Hosting, so the whole app lives with one +provider. Firebase Auth was already in use, so no one signs up again. ## Considered Options @@ -51,14 +51,13 @@ signs up again. address asks guests to sign in so their data reaches their account, and finishes syncing signed-in users. Then the old API stops accepting writes (it answers them with an error), so nothing changes in Neon while it is copied. - The copy matches Neon accounts to Firebase users by their Firebase user id; an - account with only the legacy password login must first sign in on the old - address with Google or email using the same verified email, which links it, - and the copy stops rather than skip an account that holds data but has no - Firebase user. Neon custom yields record only the finished product, not the - starting form, so the copy leaves the starting form blank for the owner to - fill in. Only after the copy has been checked is Vercel reduced to a redirect. - Bookmarks follow the redirect; installed copies of the app need to be - installed again from the new address. PR previews come from Firebase preview - channels. + The copy uses a written list that maps each Neon account holding data to its + Firebase user id, checked by hand (there are about two accounts); it stops if + an account with data is missing from the list, appears twice, or two accounts + map to one Firebase user. Neon custom yields record only the finished product, + not the starting form, so the copy leaves the starting form blank for the + owner to fill in. Only after the copy has been checked is Vercel reduced to a + redirect. Bookmarks follow the redirect; installed copies of the app need to + be installed again from the new address. PR previews come from Firebase + preview channels. - Supersedes `docs/AUTH_MIGRATION_ROADMAP.md` (Better Auth + Cloudflare). From 685f2080898f2ca11dccf774549847a4427c029b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 16:14:42 +0000 Subject: [PATCH 19/21] docs: read-only old client, anonymous write limits, CSV formulas Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0001-firebase-auth-and-firestore.md | 24 ++++++++++-------- docs/adr/0002-guests-use-anonymous-sign-in.md | 25 +++++++++++-------- docs/adr/0004-community-dataset-cc-by.md | 22 ++++++++-------- 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index 36f3f90..57cfbd7 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -49,15 +49,17 @@ provider. Firebase Auth was already in use, so no one signs up again. to an address, so a redirect cannot carry a guest's saved data or unsynced edits across. Before the redirect is switched on, the last version on the old address asks guests to sign in so their data reaches their account, and - finishes syncing signed-in users. Then the old API stops accepting writes (it - answers them with an error), so nothing changes in Neon while it is copied. - The copy uses a written list that maps each Neon account holding data to its - Firebase user id, checked by hand (there are about two accounts); it stops if - an account with data is missing from the list, appears twice, or two accounts - map to one Firebase user. Neon custom yields record only the finished product, - not the starting form, so the copy leaves the starting form blank for the - owner to fill in. Only after the copy has been checked is Vercel reduced to a - redirect. Bookmarks follow the redirect; installed copies of the app need to - be installed again from the new address. PR previews come from Firebase - preview channels. + finishes syncing signed-in users. A later release makes the old app read-only + too: it stops accepting new edits, still pushes what is pending, and shows a + banner about the move. Once pending changes have drained, the old API stops + accepting writes (it answers them with an error), so nothing changes in Neon + while it is copied. The copy uses a written list that maps each Neon account + holding data to its Firebase user id, checked by hand (there are about two + accounts); it stops if an account with data is missing from the list, appears + twice, or two accounts map to one Firebase user. Neon custom yields record + only the finished product, not the starting form, so the copy leaves the + starting form blank for the owner to fill in. Only after the copy has been + checked is Vercel reduced to a redirect. Bookmarks follow the redirect; + installed copies of the app need to be installed again from the new address. + PR previews come from Firebase preview channels. - Supersedes `docs/AUTH_MIGRATION_ROADMAP.md` (Better Auth + Cloudflare). diff --git a/docs/adr/0002-guests-use-anonymous-sign-in.md b/docs/adr/0002-guests-use-anonymous-sign-in.md index 54d73f6..717a446 100644 --- a/docs/adr/0002-guests-use-anonymous-sign-in.md +++ b/docs/adr/0002-guests-use-anonymous-sign-in.md @@ -8,29 +8,32 @@ People should be able to save before deciding to sign up. Instead of keeping guest data in the browser and copying it into an account with our own code, a guest's first save signs them in anonymously; their data lives in Firestore under the same rules as any account, and signing in with Google or email later -links to that same user so nothing is copied. People who only use the -calculator never get an account. Anonymous sign-in needs the network, so a save -made offline before the guest has a user is kept in the browser and written to +links to that same user so nothing is copied. People who only use the calculator +never get an account. Anonymous sign-in needs the network, so a save made +offline before the guest has a user is kept in the browser and written to Firestore once sign-in succeeds. ## Consequences - Guest data saved by the current app (in the browser, not Firestore) is not read by the new app. It moves over the existing way before the switch: the - current app asks guests to sign in, its adoption code moves their records - into the account, and they reach Firestore with the one-time copy from Neon - (see ADR 0001). The old guest storage and adoption code are removed only - after that. + current app asks guests to sign in, its adoption code moves their records into + the account, and they reach Firestore with the one-time copy from Neon (see + ADR 0001). The old guest storage and adoption code are removed only after + that. - If the guest signs in with a Google account or email that already belongs to an account, Firebase cannot link it to the anonymous user. In that case the app reads the guest's custom yields and saved calculations while still signed - in as the guest, signs in to the existing account, and writes them there. - This copy is the one piece of guest-transfer code we keep. + in as the guest, signs in to the existing account, and writes them there. This + copy is the one piece of guest-transfer code we keep. - A guest who clears browser data loses what they saved, as today. -- Unused anonymous accounts pile up and may need occasional cleanup. Deleting - an Auth user does not delete its Firestore documents, so cleanup removes the +- Unused anonymous accounts pile up and may need occasional cleanup. Deleting an + Auth user does not delete its Firestore documents, so cleanup removes the user's custom yields and saved calculations along with the user. +- Anonymous users can write to Firestore without a verified account, so the app + uses App Check, and the rules cap the size and number of each user's records. + A budget alert on the Firebase project flags unusual use. - Only a non-anonymous account can submit yields to the community dataset, so every submitted yield has a real owner. diff --git a/docs/adr/0004-community-dataset-cc-by.md b/docs/adr/0004-community-dataset-cc-by.md index 19d3939..d864ff3 100644 --- a/docs/adr/0004-community-dataset-cc-by.md +++ b/docs/adr/0004-community-dataset-cc-by.md @@ -7,18 +7,20 @@ status: accepted Submitting a yield means its owner agrees that, once approved, it is released under CC BY 4.0 with attribution by display name or "Anonymous"; the submit dialog says so. No other identifying field (such as the organization from the -old contributor profiles) is published or exported. The code stays MIT. A license on contributed data cannot be -narrowed after the fact, so this is set before any submissions under the new -flow. For now the Community page offers a CSV/JSON download built from the -approved yields. Scheduled, versioned releases (a GitHub Action exporting a -numbered file at a stable address other tools can fetch) will be added once -there are enough approved yields to be worth citing. Until then the dataset has -no fixed download address. +old contributor profiles) is published or exported. The code stays MIT. A +license on contributed data cannot be narrowed after the fact, so this is set +before any submissions under the new flow. For now the Community page offers a +CSV/JSON download built from the approved yields. Every CSV export, including +the later scheduled one, neutralizes cells that start with `=`, `+`, `-` or `@` +so a spreadsheet does not run them as formulas. Scheduled, versioned releases (a +GitHub Action exporting a numbered file at a stable address other tools can +fetch) will be added once there are enough approved yields to be worth citing. +Until then the dataset has no fixed download address. ## Consequences - Yields shared before this consent existed enter the community dataset only with their owner's agreement to CC BY 4.0: the one-time copy from Neon marks - them approved only for owners who have agreed, and leaves the rest private - for their owners to submit again. A copied yield whose starting form is - still blank stays private until its owner fills it in and submits it. + them approved only for owners who have agreed, and leaves the rest private for + their owners to submit again. A copied yield whose starting form is still + blank stays private until its owner fills it in and submits it. From 051dfc89f53dedd4d03e49cd439145cf552fddcf Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 16:21:23 +0000 Subject: [PATCH 20/21] docs: offline handoff file, sign-out cache, private source, quotas Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0001-firebase-auth-and-firestore.md | 38 +++++++++++-------- docs/adr/0002-guests-use-anonymous-sign-in.md | 6 ++- 2 files changed, 27 insertions(+), 17 deletions(-) diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index 57cfbd7..f39dc48 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -32,11 +32,14 @@ provider. Firebase Auth was already in use, so no one signs up again. - Excel/CSV import parses in the browser. - Security rules cannot hide single fields of a readable document, so approved yields are copied into a separate public collection holding only the public - fields (species, conversion, yield, source, display name). The owner's user - id, submission details and review note stay in the owner's private document. - The reviewer's approval writes both in one batch. Editing or deleting an - approved yield deletes its public copy in the same batch, and the rules let an - owner delete the public copy of their own yield. + fields (species, conversion, yield, display name). The owner's user id, the + free-text source note, submission details and review note stay in the owner's + private document. The reviewer's approval writes both in one batch. Editing or + deleting an approved yield deletes its public copy in the same batch, and the + rules let an owner delete the public copy of their own yield. +- Firestore's persistent cache keeps private data in the browser after sign-out, + so signing out first sends pending writes (or, if offline, asks whether to + wait or discard them) and then clears the local cache. - Our sync engine's revision check goes away: when the same record is edited on two devices while one is offline, the later write wins. We accept this because the accounts belong to one person; the conflict screen is dropped. @@ -51,15 +54,18 @@ provider. Firebase Auth was already in use, so no one signs up again. address asks guests to sign in so their data reaches their account, and finishes syncing signed-in users. A later release makes the old app read-only too: it stops accepting new edits, still pushes what is pending, and shows a - banner about the move. Once pending changes have drained, the old API stops - accepting writes (it answers them with an error), so nothing changes in Neon - while it is copied. The copy uses a written list that maps each Neon account - holding data to its Firebase user id, checked by hand (there are about two - accounts); it stops if an account with data is missing from the list, appears - twice, or two accounts map to one Firebase user. Neon custom yields record - only the finished product, not the starting form, so the copy leaves the - starting form blank for the owner to fill in. Only after the copy has been - checked is Vercel reduced to a redirect. Bookmarks follow the redirect; - installed copies of the app need to be installed again from the new address. - PR previews come from Firebase preview channels. + banner about the move. An old copy that was offline through all this can't be + seen from the server, so the read-only app also lets anyone save their unsent + changes as a file, which the new app's import accepts. After a waiting period + for devices to come online, the old API stops accepting writes (it answers + them with an error), so nothing changes in Neon while it is copied. The copy + uses a written list that maps each Neon account holding data to its Firebase + user id, checked by hand (there are about two accounts); it stops if an + account with data is missing from the list, appears twice, or two accounts map + to one Firebase user. Neon custom yields record only the finished product, not + the starting form, so the copy leaves the starting form blank for the owner to + fill in. Only after the copy has been checked is Vercel reduced to a redirect. + Bookmarks follow the redirect; installed copies of the app need to be + installed again from the new address. PR previews come from Firebase preview + channels. - Supersedes `docs/AUTH_MIGRATION_ROADMAP.md` (Better Auth + Cloudflare). diff --git a/docs/adr/0002-guests-use-anonymous-sign-in.md b/docs/adr/0002-guests-use-anonymous-sign-in.md index 717a446..daf77ea 100644 --- a/docs/adr/0002-guests-use-anonymous-sign-in.md +++ b/docs/adr/0002-guests-use-anonymous-sign-in.md @@ -34,6 +34,10 @@ Firestore once sign-in succeeds. user's custom yields and saved calculations along with the user. - Anonymous users can write to Firestore without a verified account, so the app uses App Check, and the rules cap the size and number of each user's records. - A budget alert on the Firebase project flags unusual use. + Those caps are per user, so the project-wide limits are App Check, Firebase + Auth's limit on new anonymous sign-ups from one IP address (kept low), and the + free plan's hard quotas, which stop writes instead of billing. A bot could + still exhaust the free quota and block saves until it resets; we accept that + for an app of this size and revisit it if it happens. - Only a non-anonymous account can submit yields to the community dataset, so every submitted yield has a real owner. From 111f20fb2f0bbd48e5e141852fb52698162ed197 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 29 Sep 2026 16:29:20 +0000 Subject: [PATCH 21/21] docs: import columns, display name updates, zero yields, old address Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01PnPh61Aps5neY87hh1pu9T --- docs/adr/0001-firebase-auth-and-firestore.md | 24 +++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/adr/0001-firebase-auth-and-firestore.md b/docs/adr/0001-firebase-auth-and-firestore.md index f39dc48..c4ddf2c 100644 --- a/docs/adr/0001-firebase-auth-and-firestore.md +++ b/docs/adr/0001-firebase-auth-and-firestore.md @@ -29,14 +29,18 @@ provider. Firebase Auth was already in use, so no one signs up again. - `api/`, `server/`, `shared/`, SQLite, Neon, the legacy password login and the custom sync layer go away, and so does the rule to change both backends. -- Excel/CSV import parses in the browser. +- Excel/CSV import parses in the browser. The import and export format gains + separate starting-form and finished-product columns; a row without a starting + form imports with it blank, like the Neon copy. - Security rules cannot hide single fields of a readable document, so approved yields are copied into a separate public collection holding only the public fields (species, conversion, yield, display name). The owner's user id, the free-text source note, submission details and review note stay in the owner's - private document. The reviewer's approval writes both in one batch. Editing or - deleting an approved yield deletes its public copy in the same batch, and the - rules let an owner delete the public copy of their own yield. + private document. The reviewer's approval writes both in one batch. Changing + the display name rewrites the name on all of that owner's public copies in one + batch. Editing or deleting an approved yield deletes its public copy in the + same batch, and the rules let an owner delete the public copy of their own + yield. - Firestore's persistent cache keeps private data in the browser after sign-out, so signing out first sends pending writes (or, if offline, asks whether to wait or discard them) and then clears the local cache. @@ -64,8 +68,12 @@ provider. Firebase Auth was already in use, so no one signs up again. account with data is missing from the list, appears twice, or two accounts map to one Firebase user. Neon custom yields record only the finished product, not the starting form, so the copy leaves the starting form blank for the owner to - fill in. Only after the copy has been checked is Vercel reduced to a redirect. - Bookmarks follow the redirect; installed copies of the app need to be - installed again from the new address. PR previews come from Firebase preview - channels. + fill in. The dry run lists any yield that is not above 0 and up to 100; the + owner fixes those before the freeze, and the copy refuses to run while any + remain. After the copy has been checked, the old address keeps serving the + read-only app for six months, so an installed copy that comes back online + still gets the save-to-file option and a link to the new address; only then is + Vercel reduced to a redirect. Bookmarks follow the redirect; installed copies + of the app need to be installed again from the new address. PR previews come + from Firebase preview channels. - Supersedes `docs/AUTH_MIGRATION_ROADMAP.md` (Better Auth + Cloudflare).