Skip to content

KindredPics — active brief

Read first. Source of truth for "what is KP right now." Reference, don't duplicate. Cross-refs: projects/kindredpics.yaml (config), C:\Users\devin\kindredpics-site\CLAUDE.md (legacy project memory — describes a mix of LIVE and PLANNED state without distinguishing them; treat with caution), memory pointers below, decisions/.

Canonical rule: CLAUDE.md is legacy project memory; active/kindredpics.md is current operating truth where explicitly marked. This brief separates LIVE / IN-PROGRESS / TARGET so you don't conflate shipped behavior with intent.

LIVE state (shipped to prod 2026-05-23)

Option B workspace rebuild merged to main. Commit e4b9edc (rebuild) + 3dec457 (merge) + 79d3e54 (Playwright refresh). Branch fix/rebuild is merged.

  • Live URL: https://www.kindredpics.com — Cloudflare Pages project kindredpics, production branch production. Note: CF Pages default subdomain stays nanny-pics.pages.dev (CF locks it to creation-name; can't rename). Custom domains: nanny-pics.pages.dev (locked default), www.kindredpics.com (production). nanny.stampready.app REMOVED 2026-05-28.
  • CF hardening applied 2026-05-28 (audit at deliverables/OPS-cloudflare_audit_v1.0_2026-05-28.md): DMARC p=nonep=quarantine on kindredpics.com; always_use_https ON; min_tls_version 1.2; Bot Fight Mode + Block AI bots + Crawler Protection enabled.
  • Davidson tenant + all prior data: wiped. Fresh empty schema applied to new D1. Davidson re-signs up.
  • Infra renamed: D1 nanny-picskindredpics (id 3735c88a-0b7a-49e5-a4b2-f24cd9621519). R2 nanny-pics-photoskindredpics-photos. Old nanny-pics D1 + R2 DELETED 2026-05-26 per Devin (rollback window closed 5/24).
  • Auth: OTP primary, password fallback. Signup is OTP-only (/signup two-step form). Signin is dual-path: OTP primary, password fallback via toggle. Endpoints /api/auth/request-otp + /api/auth/verify-otp (shipped 2026-05-25 ec935be, migration 027). /forgot-password + /reset-password remain for password-path users. Magic-link is DEAD — endpoints return 410. Mobile-rn LoginScreen aligned.
  • Family code: SHIPPED as KP-XXXX-XXXX (8-char, 4-4 hyphenation, Crockford-flavored alphabet — no 0/1/I/L/O). Migrated from 12-char today (commit dd45aad). Used as workspace-invite mechanism/api/tenants/join adds an authenticated user to an additional workspace via code.
  • Retention: 30 days from finalize (P0.2 done). Was 90 days at sign. Purge clock starts at upload completion, not reservation.
  • Auto-Rekognition: REMOVED from upload path (P0.3 partial). processPhoto() runs with skip_rekog=1 shim to preserve derivative generation. Face indexing only fires when manually triggered from admin queue. Paid scan-request UI is Phase 3, not yet built.
  • Metadata sanitize: truthful state (P0.1 done). photos.metadata_sanitize_status is pending | gps_removed | unsupported_format | failed. No more always-zero gps_stripped.
  • Tests: 108/114 Playwright passing against prod (6 skipped, KP_SMOKE_TOKEN-gated).
  • Launch-readiness verified 2026-05-29 — autonomous Playwright + cron-trigger harness drove signup→upload→reminder-emails (15d/7d/3d/1d)→purge→export end-to-end. All four trust-chain touchpoints work in prod. Davidson tenant untouched. Two prod-only bugs found + fixed mid-test (stale Resend key on KP Pages; purge_reminders_sent CHECK constraint missed 15d). Same-day cosmetic follow-up shipped: CRC-32 spec compliance in ZipStream, purged-photo stub in export, path-comment fixes, daily reap cron for OTP + login_attempts (45 8 * * *), compat_date bump. Deliverable: FounderOS/deliverables/OPS-kp_launch_readiness_v1.0_2026-05-29.md.

IN-PROGRESS / NOT YET SHIPPED (Codex's gap doc, Phases 2-5)

Per deliverables/SPEC-rebuild_v2.0_2026-05-23.md + deliverables/TECH-gap_remediation_handoff_v1.0_2026-05-23.md:

  • P0.4 batch/workspace model — no upload_batches table yet; photos still belong directly to a tenant, not to a named batch.
  • P0.5 trust tiers + uploader-first approval — current roles still owner/admin/member. Manual tags still auto-insert as approved. Uploader-first approval model is Devin's intent but NOT in code.
  • Paid scan-request flow (Phase 3) — scan_requests table not built; cost-estimate modal not built; Paddle webhook → scan state not wired.
  • Export as handoff package (Phase 4) — VERIFIED 2026-05-29. Photo-bytes ZIP shipped at GET /api/users/export-zip (streams R2 originals + manifest.json; live test: 7839 b ZIP, 6 photos + manifest). Note: comment in source says /api/users/me/export.zip but Pages routing strips the /me segment; actual path is /api/users/export-zip. Cosmetic gap: manifest.json has a bad CRC on streaming ZIPs (data extracts fine, strict ZIP validators may complain).
  • Purge reminders (Phase 4) — VERIFIED 2026-05-29. Email pipeline shipped at POST /api/photos/purge-reminders, idempotency table purge_reminders_sent (migrations 030 + 032 in remote D1) + cron 15 8 * * *. Marks coded + tested live: 15d / 7d / 3d / 1d (4 marks). End-to-end Playwright + cron-trigger harness landed all 4 emails in Devin's inbox + verified idempotency. See FounderOS/deliverables/OPS-kp_launch_readiness_v1.0_2026-05-29.md.
  • Marketing copy still pre-rebuild. docs/positioning.md and docs/icp.md (both dated 2026-05-18) describe the permanent "kitchen-table archive" framing. Pricing copy on /pricing still says "kept forever" + 90-day originals. These must be rewritten to match the 30-day workspace reality before any growth push.

TARGET intent (Devin's 5 truths, 2026-05-23)

What KP is becoming (some shipped, some not — see LIVE / IN-PROGRESS):

  1. Temporary 30-day collaborative tagging/export workspace. Shipped via 30-day retention; positioning copy aligned 2026-05-29 (docs/positioning.md, docs/icp.md, src/pricing.html, src/index.html hero all reflect 30-day workspace reality).
  2. No unpaid face processing. Auto-Rekog removed (shipped); paid scan-request UI is Phase 3 — until then, face indexing happens only via admin manual trigger.
  3. Rekognition only behind consent + paywall. Same as #2; paywall enforcement waits on Phase 3.
  4. Uploader-first approval model. Not yet in code (Phase 3 / P0.5).
  5. Auth = OTP primary + password fallback + shareable family code KP-XXXX-XXXX workspace-invite. Shipped (OTP ec935be 2026-05-25; family code 8-char Crockford-flavored, 4-4).

Driver for the 30-day timeline: BIPA-class biometric-privacy compliance. Not a product preference. Don't propose extending retention without counsel sign-off.

Repo

  • Root: C:\Users\devin\kindredpics-site
  • Origin: https://github.com/StampReady/kindredpics.git
  • Branches: main (working — option-B rebuild now lives here), production (Cloudflare Pages deploy target)
  • Deploy is NOT push-to-deploy. After git push: bash scripts/deploy.sh OR wrangler pages deploy src --project-name=kindredpics --branch=production --commit-dirty=true
  • Last commit on main: 79d3e54 [TEST] Refresh Playwright suites for email+password auth + workspace family-code

Stack (current)

  • Frontend: vanilla HTML/CSS/JS on Cloudflare Pages, with /app.html as a single-file React 18 + Babel-Standalone SPA (not pure vanilla in app shell)
  • Backend: Cloudflare Workers + R2 + D1
  • Images: Cloudflare Images via sibling Worker service binding (RESIZEkp-image-resize)
  • Mobile: Expo SDK 54 / RN 0.81 / React 19 at mobile-rn/; bottom-tab nav, native-stack for Photo/Person/Upload; shares SPA data-shape via src/data/store.ts
  • Analytics: PostHog (displaced Humblytics 2026-05-22)
  • Cron: kp-cron Cloudflare Worker (cron-worker/) — */5 * * * * drain, Sun 23:00 UTC weekly digest

Files to read first

  • kindredpics-site/CLAUDE.mdlegacy project memory; conflates LIVE + PLANNED — disambiguate against this brief
  • kindredpics-site/deliverables/SPEC-rebuild_v2.0_2026-05-23.md — option B rebuild SPEC + open questions Q1-Q9
  • kindredpics-site/deliverables/TECH-gap_remediation_handoff_v1.0_2026-05-23.md — Codex's P0-P2 gap analysis; canonical for what's NOT yet built
  • kindredpics-site/docs/positioning.md + docs/icp.md — marketing positioning (PRE-rebuild; needs rewrite)
  • kindredpics-site/functions/_middleware.js — public-route allowlist + magic-link 410 shims + security headers
  • kindredpics-site/functions/api/photos/sign.js + finalize.js — upload path (30-day retention live here)
  • kindredpics-site/functions/utils/family-code.js — KP-XXXX-XXXX generator + normalizer
  • kindredpics-site/image-worker/src/index.js — sibling Worker for CF Images resize
  • kindredpics-site/db/schema_v2.sql — 35-table consolidated schema (current live)

Memory pointers (load on demand)

  • project_kp_product_truth_2026-05-23.md — 5 truths (TARGET, partial-live)
  • project_session_2026-05-18_kp_marketing_launch.md — positioning + marketing surface launch (PRE-rebuild)
  • project_session_2026-05-16_to_17.md — face-tag overhaul, /name-people.html wizard
  • reference_kindredpics_deploy.md — deploy gotcha
  • reference_safari_r2_signed_headers.md, reference_csp_blocks_presigned_uploads.md, reference_cf_images_binding_api.md, reference_pages_cant_bind_images.md — upload-path gotchas

Current priority

  1. Nanny-pics → kp-app migration (Devin-action). Pages project rebuild SOP emailed 2026-06-03. ~30s downtime when swapping custom domain. Blocks the "all nanny-pics references removed from CF" goal. 0a. Auto-close upload_batches (Bug C residual). SPA-side /api/batches/:id/close call from upload-queue.js on batch drain, OR idle-cron sweep that closes batches with no pending photos + last upload >10 min ago. Currently cosmetic ("in progress" caption sticks) but future users will hit the same confusion. 0b. UI E2E Playwright specs for the Lindsey class of bugs. tests/e2e/library-render.spec.ts + tests/e2e/tree-add.spec.ts. Would've caught both Lindsey bugs; API-only probes (v1.4 audit) missed them.
  2. Phase 2 backbone SHIPPED 2026-05-27 (commits 0150ae4 + afc53a2). upload_batches table + photos.upload_batch_id + POST /api/batches/create + POST /api/batches/:id/close + sign.js auto-batch (find-or-create on 30-min idle window per uploader). Migration 029 applied to remote D1. Verified end-to-end via UI: 3 photos via "Add photos" modal → all in same upload_batch_id, status='upload_complete' after explicit close, purge_due_at = upload_completed_at + 30 days. Still ahead in Phase 2: batch-aware Import Review UI; user-facing "name your batch / choose visibility" picker (current copy auto-names "YYYY-MM-DD upload"); idle-cron sweep for never-closed batches.
  3. Phase 3: trust tiers + uploader-first approval (P0.5) then paid scan-request flow (P0.3 full).
  4. Family-code migration: generator + endpoint already 8-char; Davidson tenant_id=1 manually rotated to KP-B385-YKRS 2026-05-23 via wrangler d1 execute. If any new tenants land with 12-char codes (shouldn't), repeat. Better: SQL one-shot to sweep all LENGTH(family_code) != 8 rows.
  5. AI / Rekognition watermark in app.html FingerprintP (line ~2280) still renders the new KP script as a 260px decorative bg. Consider whether the script-as-watermark works visually or if it should be replaced with a different motif.

Done this session (2026-06-07 — Lindsey OTP email fix)

  • Root cause: 2026-06-03 PM migration was supposed to push 19 env vars to new kp Pages project. Only 12 landed. EMAIL_FROM and EMAIL_REPLY_TO were missing. functions/utils/email.js:11 silently returns {ok:true,dev:true} when EMAIL_FROM is absent — UI shows "code sent," no actual Resend call. Lindsey clicked "Send code" 3× today (D1 email_otp_codes rows 3-5 at 14:30-14:33 UTC), no email arrived.
  • Fix shipped: PATCH to /accounts/{aid}/pages/projects/kp added [email protected] + [email protected] as secret_text. Env var count 12 → 14. Fresh deploy via bash scripts/deploy.sh landed at https://bc161122.kp-ce8.pages.dev (production). environment=None verify warning is the known CF API false-positive.
  • Wrangler auth fix: wrangler pages deploy hit auth 10000 when shell env not exported. set -a; source ~/.claude/.env; set +a before bash scripts/deploy.sh resolved it (CLOUDFLARE_API_TOKEN now visible to wrangler subprocess).
  • Remaining 5 env vars audited + pushed same session. Cross-referenced every env.X reference in functions/ against prod. 5 actually-broken vars pushed (AWS_ACCESS_KEY_ID, AWS_REGION, R2_ACCESS_KEY_ID, R2_ACCOUNT_ID, STRIPE_KP_PRICE_ID=price_1Tct6H0MzwWYk80n2nLhalWJ). Env var count 14 → 19, matching the original 19 the 2026-06-03 migration promised. Redeploy 7b6994f3 landed. Dormant impacts unblocked: Rekognition signing (cron-drain face-detect), R2 presigned URLs, paid-scan Stripe Checkout (was returning 503 "payment not configured"). Skip list confirmed false-alarms: KP_BASE_URL (cron-worker only), STRIPE_KP_WEBHOOK_SECRET (live mode unused), CSAM_*/PHOTODNA_*/TAKEDOWN_EMAIL (CSAM_DRIVER defaults to noop), PADDLE_* (dead code, Stripe replaced).
  • Email pipeline VERIFIED working — Lindsey OTP row 6 (id 6, created 14:44:32 UTC, consumed 14:44:51) shows a real 19-second receive→enter cycle. Session minted.
  • NEW BUG SURFACED IN SAME SESSION — R2 cred dead. Lindsey attempted 32-photo upload at 14:53:08 UTC. sign() succeeded for all 32 (D1 has rows 38-71 across batches 6/⅞/9 — race-condition 4-batch split is a separate auto-batch bug). Every R2 PUT 401'd Unauthorized. Verified with direct presigned PUT test using local ~/.claude/.env R2 creds → R2 returns <Code>Unauthorized</Code>. Old uploads (ids 1-37) intact in R2; CORS rules fine; bucket fine. R2 token scoped to old nanny-pics-photos bucket name; bucket rename 2026-05-23 invalidated write access. Lindsey's "checkmark → /signin redirect" was a side-effect: SPA app shell re-checked session after the 32 PUT errors and middleware bounced.
  • Devin action emailed (gmail msg 19ea32e66ceb2b44): mint new R2 token via dashboard, scoped to kindredpics-photos, paste back. Then push to Pages + redeploy. Canonical stays PENDING-R2-CRED until done.
  • R2 cred rotation COMPLETE. Devin minted new R2 token kp-pages-write-2026-06-07 (KeyID 6448d6e4...) via dashboard, scoped to kindredpics-photos Object Read & Write. Saved to ~/.claude/.env via Edit (NOT sed -i). Pushed to kp Pages prod via PATCH. Discovered SMOKE_TOKEN on prod didn't match local KP_SMOKE_TOKEN — synced. Redeployed (a4b1097b then c3ab1b36).
  • End-to-end SPA smoke test PASSED 2026-06-07 15:08 UTC: smoke-signin 200 → sign 200 (photo_id 72, batch 10) → R2 PUT 200 (639 bytes) → finalize 201 (row flipped processed/visible). Fixture cleaned up. Devin notified via gmail 19ea358dbd567ed0 (reply in thread 19ea32e66ceb2b44).
  • Orphan cleanup (deferred): 32 photo rows (ids 38-71) + 4 upload_batches (ids 6-9) stuck in 'upload'/'uploading' state. Hold until Lindsey retries successfully, then sweep both stale batches + any new failure in one shot.

Done this session (2026-06-03 PM — nanny-pics CF project rebuild COMPLETE)

  • New CF Pages project kp (id 8cc6d3a8-2f6b-44e5-9c98-bdc42619d489) created via API. CF auto-suffixed default subdomain to kp-ce8.pages.dev because kp.pages.dev was already taken in the global namespace. Acceptable since *.pages.dev is never user-facing — users see www.kindredpics.com.
  • All 19 production env vars pushed via single PATCH call. 11 reused from local ~/.claude/.env; 4 freshly generated (SIGNING_KEY, INTERNAL_CRON_TOKEN, DIGEST_CRON_TOKEN, PASSWORD_SALT); 2 re-typed plain strings ([email protected], [email protected]); 2 random placeholders (PASSWORD_HASH, ADMIN_PASSWORD_HASH — Devin to overwrite with real values via CF dashboard when needed; legacy /photos.html family-pw gate now broken, admin password access broken until set).
  • D1 + R2 + RESIZE service binding all migrated to new project, production env only. Preview env intentionally left unbound to prevent any future "nanny-pics-photos / bef2b393..." style stale references.
  • Custom domain cutover: added www.kindredpics.com to new kp project → CF auto-removed it from old kindredpics project → DNS CNAME explicitly updated via DNS-write token from nanny-pics.pages.devkp-ce8.pages.dev (CF didn't auto-update the CNAME, so manual PATCH was required). ~3 min real downtime between custom-domain swap and CNAME update; longer than the 30s I'd estimated.
  • Old kindredpics project DELETED via API after cutover verified. Only kp remains. Zero nanny-pics references in CF account.
  • Live smoke verified: https://www.kindredpics.com/app serves the new project (200 OK), SPA loads 34 photos, 3 people, smoke-signin works with fresh SIGNING_KEY. All existing kp_session cookies invalidated by the SIGNING_KEY rotation — Devin + Lindsey need to OTP-sign-in again.
  • Lindsey emailed (gmail-personal msg 19e8f055b98f45db) with re-sign-in heads-up. Sent on Devin's behalf — flagged for him.
  • scripts/deploy.sh updated: --project-name=kindredpics--project-name=kp (line 9 + line 24 verify_deploy invocation). Uncommitted — will land in commit.
  • Audit of CF API tokens completed earlier same day: 5 dead tokens deleted by Devin (kp-direct-upload, R2 Account Token, nanny-pics-sync, old-butterfly-846a, stampready-backups-write). 2 kept: kindredpics-gh-deploy (now serves as CLOUDFLARE_API_TOKEN, the Pages/D1/R2/Workers token) and Claude DNS (the DNS-write token used for the CNAME swap, stored as CLOUDFLARE_API_TOKEN_DNS). The kindredpics-gh-deploy token name still says "kindredpics" — separate cleanup if Devin wants to rename it.

Done this session (2026-06-03 AM — Lindsey-reported bugs fixed in prod + UI smoke green)

  • Lindsey's 2026-06-02 19:07 CT email surfaced 3 real bugs, all now FIXED in prod (commit f7e6f00, deploys a81dbd8d + d869440c).
  • Bug A (invisible uploads): 19 photos uploaded 2026-05-30 had staging=1 in D1 (origin: stale SW or cached client; couldn't pin to a current code producer). manifest.js filtered them out by default → invisible to LibraryView. Fixed: D1 recovery (UPDATE photos SET staging=0 for her 19 rows), sign.js forces staging=0 regardless of client input, manifest.js drops the staging=0 filter entirely, sw.js bumped v9 → v10 to bust iOS Safari caches.
  • Bug B (tree add silent 422): Modal opened, submit returned 422 with "apply failed: NOT NULL constraint failed". Two missing tenant_id values: persons.tenant_id AND change_log.tenant_id (4 INSERT sites). Fixed: tree-suggest.js resolves tenant via getTenantContext and passes to applyTreeEdit. tree-edit.js accepts tenant_id, injects into all persons + relationships + change_log INSERTs, scopes all lookups by tenant (closes a latent cross-tenant person-merge leak). admin/queue.js injects suggestion.tenant_id when approving queued tree edits.
  • Bug C (today shows "in progress"): All 5 of her upload_batches were stuck status='uploading' — SPA never calls /api/batches/:id/close after a batch drains. The "Recent batches" rail caption stays "in progress" forever. Recovery only: D1 batch update closed her 5 batches. Still open as cosmetic: SPA-side auto-close on drain + idle-cron sweep are both unimplemented. Photos still render in the main grid; caption is the only artifact.
  • Smoke testing: live chrome-devtools-mcp drove www.kindredpics.com as [email protected] (Davidson tenant). Verified across 3 viewports (375 mobile / 768 tablet / 1280 desktop) — no horizontal overflow, all h1s render, 34 photos load in LibraryView, all 15 visible nav buttons clickable. End-to-end tree add-relative: clicked Tree → "Add a relative" → filled modal → submit → modal closed → KP_DATA refreshed → new person rendered with id=14, tenant_id=1, status='approved'. Cleanup applied (deleted 3 test rows + 3 change_log entries). Console: 1 cosmetic React fetchPriority prop-casing warning, zero functional errors.
  • Playwright spec NOT added this session. chrome-devtools-mcp covered the same surface area for the bug verification. Recommendation: add tests/e2e/library-render.spec.ts (upload → reload → assert N imgs in grid) + tests/e2e/tree-add.spec.ts (open modal → submit → assert person count increment) as permanent regression coverage — both bugs would've been caught and the missing UI E2E coverage is what let Lindsey hit them. ~30 min to add.
  • Nanny-pics migration prep done, awaiting Devin's hands: CF API rejects nanny-pics.pages.dev removal with error 8000021 — it's the project's permanent locked default subdomain, not a removable custom-domain attachment. Only fix = full Pages project rebuild + DNS re-point + secrets re-migration. Full SOP emailed 2026-06-03 (gmail msg 19e8dca324f67a33). Snapshot of current CF config at C:\tmp\kp-mig\cf-project.json (19 env vars, D1 'kindredpics', R2 'kindredpics-photos', service binding RESIZE → kp-image-resize, custom domain www.kindredpics.com). Suggested new project name: kp-app (CF holds the freed name for 24h).
  • Stale canonical note corrected: "all 3 saved CLOUDFLARE_API_TOKEN values 9109-invalid" is no longer true — the .env token deployed cleanly twice today.

Done previous session (2026-05-31 evening — pricing decision shipped: Option A min-batch gate ≥50)

  • Pricing structural decision RESOLVED. Devin picked Option A (minimum-batch UI gate ≥50 paid photos) over pack pricing or prepaid credits. Driver: simplest mental model + preserves penny-per-photo unit pricing.
  • Server gate shipped in functions/api/photos/scan-request.js — new MIN_PAID_PHOTOS = 50 constant. Gate fires after promo lookup, before Stripe Checkout creation: if paidNow > 0 && paidNow < 50 && !preAppliedCouponId, DELETE the orphan scan_requests row and return 400 {error: "min_batch_under_50", paid_count, free_tier_count, free_remaining, hint}. KPFAMILY/preapplied-coupon path bypasses the gate (zeroes total). Gate is on the paid portion only — free-tier users with quota remaining are never blocked.
  • SPA updates in src/app.html: BIPA notice now reads "After that, $0.01 per photo — paid scans run in batches of 50+ photos at a time." kpStartScanFlow handles the new 400 with an actionable alert pointing to KPFAMILY DM fallback.
  • Pricing.html intentionally NOT updated — the public page hides all paid pricing ("free during early access") on purpose. The 50-photo line lives at the in-app friction point only. Revisit when paid plans get an announced public surface.
  • Cosmetic gap (not blocking): the gold CTA still reads "Find people automatically · 100 free" even for users past their free quota. Plumbing free_remaining into the React component is invasive vs the actual friction value; tracking as Phase 3 polish. Currently zero real users are past free quota so this is moot.
  • Not yet deployed. Code-complete + syntax-checked; bash scripts/deploy.sh not run yet. Devin's call on when to push (Lindsey's smoke test is still mid-flight).

Done this session (2026-05-31 PM — Stripe gateway E2E + friends/family coupon)

  • Stripe gateway tested end-to-end in prod (test mode). Full path verified: scan-request → Stripe Checkout → user pays → checkout.session.completed webhook → atomic D1 batch flips scan_request pending → scanning + photo visible → scanning + rekog_paid_at stamped + rekog_status queued. Cron-drain takes it from there (downstream, not gateway-test scope).
  • Real defect surfaced: Stripe enforces a $0.50 USD minimum at Checkout Session creation, BEFORE any user-typed promo code applies. KP's penny-per-photo pricing means every batch <50 photos was 502'ing in prod with amount_too_small (6 logged entries before fix). The Phase 3 paid flow was structurally broken without an unhit code path; the v1.4 audit's "free path 200" probe never exercised this.
  • Short-term workaround shipped (commit 90896f1, deploy e67a1f80): /api/photos/scan-request now accepts optional promo_code in request body. If present, server resolves it via Stripe Promotion Codes API + pinned Stripe-Version: 2023-10-16 (account-default response shape was incompatible), pre-applies the underlying coupon via discounts[0][coupon]=.... A 100%-off coupon zeroes total — Stripe accepts $0 sessions — and unblocks small batches.
  • Friends/Family coupon live in Stripe test mode: coupon KP-FAMILY-100 (100% off, once, max 50 redemptions), promo code KPFAMILY (max 50 redemptions, 1 redeemed via E2E test). Devin can DM KPFAMILY to friends for early sharing.
  • Pricing structural decision STILL OPEN (not launch-blocking with workaround in place): the \(0.50 minimum collides with penny pricing. Three options to pick before first paying public user: minimum-batch UI gate (≥50 photos before paid CTA enables), pack pricing (\)0.50 for 50-photo bundle), or prepaid credits.
  • All Stripe test fixtures cleaned up: user 8 (Stripe Test), photo 22, 9 scan_requests rows, 7 error_log entries (all amount_too_small), 1 tenant_member row. D1 back to clean baseline (2 users, 1 tenant, 19 photos, 0 face_tags, 0 scan_requests, 0 error_log). Stripe-side product/price/coupon/webhook all kept.
  • Wife smoke-test watcher stopped at Devin's request 2026-05-31 ~14:47 CT. She uploaded 19 photos at 2026-05-30 17:55 CT and never clicked the gold "Find people automatically · 100 free" CTA across 21 hours. Awaiting her verbal feedback. Background poller pid 1279 killed; state.json frozen at 14:47:12.
  • CF API token rotated mid-session. Old cfut_Shc... was CF code 1000 (revoked). Devin generated fresh cfut_Shc... token (same prefix is coincidence; new id 2d60b9fd...). Five deploys landed clean this session: 03970e32, f7f8b13f, 6b169c90, c07bad53, 1a68bc3c, e67a1f80.

Done earlier this session (2026-05-31 AM — multi-user smoke v1.4 audit + same-day fix-throughs)

  • v1.4 probe suite (FounderOS/deliverables/OPS-kp_multi_user_smoke_v1.4_2026-05-31.md). 62/62 probes pass. Covered all 6 items deferred from v1.3 + Phase 3 scan-request + Stripe webhook surface. Zero NEW cross-tenant leaks.
  • Re-seeded multi-tenant fixtures from scratch after wife's 2026-05-30 wipe (db/smoke-v14-setup.sql): Family B + 5 fixture users + face_tag+memory+album+branch. Cleaned up via db/smoke-v14-cleanup.sql. Lindsey's 19 photos untouched.
  • All 3 findings shipped same-day:
  • Scoreboard cross-tenant added_by exposure (Finding #1) — RETIRED, not patched. Initial intermediate fix shipped opt-out via migration 034 + users.scoreboard_opt_out (commit d6d74a0). Devin then pivoted: replaced the entire global-leaderboard concept with per-workspace on-demand contributor reports (commit 0162671, deploy f7f8b13f). Deleted /api/scoreboard, /scoreboard.html, /scoreboard.js, nav links from share+tree, PATCH /api/users/me, and scoreboard_opt_out from /api/auth/me. New endpoint GET /api/tenants/me/contributor-report?format=csv|md (owner/admin gated, scoped to ctx.tenantId, aggregates face_tags + memories + suggestions, Content-Disposition attachment). Settings → Privacy card swapped for Contributor Report card with CSV+MD buttons. users.scoreboard_opt_out column kept vestigial.
  • Bridge default-tenant resolution (Finding #2) — FIXED (commit d6d74a0). getTenantContext now ORDER BY tm.joined_at ASC, t.slug ASC (was just t.slug). First-joined wins; slug only as tiebreaker. Robust under tenant slug renames.
  • smoke-signin hygiene (Finding #3) — FIXED (commit d6d74a0). Added WHERE status = 'active' filter to match verify-otp.js.
  • CF API token rotation completed mid-session. Old cfut_Shc... returned CF code 1000 (revoked). Devin generated fresh token; both v1.4-followup deploys (03970e32, f7f8b13f) landed cleanly.
  • What remains uncovered (only Item 6 from v1.3 list): UI tenant switcher in /app.html — UI work, not a probe target.
  • Tooling: probe runner at C:/Users/devin/AppData/Local/Temp/kp-launch-test/smoke_v14_runner.py.

Done previous session (2026-05-30 — Phase 3 paid scan-request flow shipped, test mode)

  • Driver: wife's overnight test surfaced that paid Rekog vs free self-tagging was unclear, and there was no payment path. Phase 3 promoted from scheduled → activation-blocking.
  • Decisions locked (open Qs from TECH-scan_request_architecture spec): Q1 free cap = first 100 photos auto-scan free per user (lifetime trial). Q2 no-face refund = none, user paid for the check. Q3 bulk discount = not yet. Q4 multi-uploader = uploader-pays only. Self-tag stays always-free, equal-prominence CTA.
  • Stripe IDs (test mode): Product prod_Uc7KaWPry0khzs, Price price_1Tct6H0MzwWYk80n2nLhalWJ ($0.01 per_unit), Webhook we_1Tct6H0MzwWYk80nsRBLUkYJ/api/billing/stripe-webhook (5 events incl. checkout.session.completed). Live keys deferred — test mode lets wife pay with 4242 4242 4242 4242 test card without real charges.
  • Migration 033 applied to remote D1: scan_requests table + photos.processing_state (upload|visible|scanning|scanned|failed) + photos.rekog_paid_at + photos.rekog_free_tier + users.free_scans_used/free_scans_quota (default 100). Backfill: 2 photos scanned (had approved face_tags), 7 visible.
  • Endpoints (commit ae48a47): POST /api/photos/scan-request (uploader-pays gate + free-tier branch + Stripe Checkout for paid + {all_visible:true} SPA shorthand). POST /api/billing/stripe-webhook (HMAC-SHA256 sig verify against test+live secrets, idempotent state flip on checkout.session.completed). Middleware bypass added for webhook path.
  • Upload path rewired: processPhoto(skip_rekog=1) → new generateDerivativesOnly() in upload.js + finalize.js. Both set processing_state='visible' in the photos UPDATE. processPhoto simplified: dropped tenants.face_detection_enabled gate, perPhotoSkip branch, and auto-trash-no-face. cron-drain sets processing_state='scanned' on success / 'failed' on error, and marks scan_requests complete via json_each-driven query when all its photos leave 'scanning'.
  • SPA (LibraryView): new gold "Find people automatically · 100 free" pill button alongside "Tag faces (free)" + "Add photos". window.kpStartScanFlow global handler: BIPA notice via window.confirm → POST {all_visible:true} → free path (toast) or Stripe Checkout redirect. URL-param handler shows toast on ?scan_paid=... / ?scan_cancelled=... then strips them.
  • 3 CF Pages prod secrets pushed live (test mode) via direct PATCH /accounts/{aid}/pages/projects/kindredpics using wrangler OAuth bearer token extracted from ~/.wrangler/config/default.toml — bypassed the broken wrangler pages secret put /memberships path (still 9106). All 3 STRIPE_* vars present in prod env_vars (count 16 → 19). Redeployed (deploy 2e41d6ba) so functions read them. Webhook smoke verified: POST /api/billing/stripe-webhook with fake sig returns "invalid signature" (= secret loaded, HMAC verify ran). All three saved CLOUDFLARE_API_TOKEN values in .claude/.env return 9109 invalid — generate a fresh one before next CF Pages secret rotation or scripted ops.
  • Deferred this session: Playwright scan-flow.spec.ts. Per-photo "Scan this · $0.01" CTA in PhotoView. Multi-select grid UI. SPA "no faces detected" badge with keep-or-dismiss.

Done previous session (2026-05-30 evening → 2026-05-31 morning — full reset + wife smoke test live)

  • Full signup wipe at Devin's request: 177 rows across 22 tables deleted via db/reset-signups-2026-05-30.sql. Schema + Stripe config + smoke fixtures preserved. Autoincrement reset. R2 photo orphans skipped (~27 objects, harmless — saved R2 creds don't have kindredpics-photos scope; next uploads overwrite by key on collision).
  • Test tenant seeded: user_id=1 [email protected] (placeholder owner), tenant_id=1 "Test Family" slug test-family, family_code PT82XRSV (user-visible KP-PT82-XRSV).
  • Email sent to wife via gmail-personal MCP (message ID 19e7af0b2f97aacc) → [email protected]. Join link https://www.kindredpics.com/signup?code=KP-PT82-XRSV, BIPA-light explainer, test-card callout (4242 4242 4242 4242), and ask to break the discoverability + free/paid clarity.
  • Wife smoke-test telemetry (via background D1 poller C:/tmp/kp-watch/poll.sh writing deltas every 15s to C:/tmp/kp-watch/deltas.log):
  • 17:45 OTP requested for [email protected]
  • 17:46 OTP consumed → user_id=2 "Lindsey Davidson" created, joined tenant_id=1 as member
  • 17:53-17:55 uploaded 19 photos (photo_id 1-19, all processing_state=visible, uploader_user_id=2)
  • 17:55 → 2026-05-31 morning idle for 14+ hours, never clicked the gold "Find people automatically" button, zero scan_requests rows created
  • UX signal (unconfirmed, awaiting wife's verbal feedback): the gold CTA in LibraryView header may not be discoverable enough for a non-technical user, OR the "100 free" framing didn't trigger action, OR she just stepped away. Devin asking her for feedback at session-end. No code changes yet — wait for actual feedback signal before iterating.
  • Watcher stopped. Background poller killed cleanly. Restart for next session: bash C:/tmp/kp-watch/poll.sh & (script writes deltas + maintains state.json + prev.json in same dir). Deltas log preserved at C:/tmp/kp-watch/deltas.log for reference.

Done previous session (2026-05-28 — second-pass gap closure: code-lookup defenses, retention onboarding, batches UI, real account delete, polish)

  • TIER 1 (d644943). Hardened /api/tenants/lookup-by-code against enumeration: tightened per-IP from 30 to 20/min, added per-code cap of 5/min via existing rate-limit util. Audited /welcome + /onboard for the same banned tokens as the prior marketing sweep — zero matches; tap-to-identify language already in place. Shipped GET /api/health/email-config (INTERNAL_CRON_TOKEN-gated) returning presence booleans for EMAIL_FROM/RESEND_API_KEY/EMAIL_REPLY_TO. Verified the live Pages env has all three via wrangler pages secret list — no missing-var email needed.
  • TIER 2 (956f7e5, migration 031). /welcome step 4 now carries a one-paragraph 30-day-retention callout so new users see the timer + reminder email schedule before they upload. New GET /api/batches?limit=5&me=1 endpoint with photo_count subquery + days_until_purge. New RecentBatches rail in the dashboard sidebar — quiet when empty, color-escalates at ≤7d / ≤3d. Real account-deletion shipped: users.deleted_at/deleted_reason + new audit_log table; POST /api/auth/delete-account with typed-email anti-typo guard + sole-owner-of-active-tenant block; settings.html swap from mailto-only to a real form (mailto kept as accessibility fallback). Verified E2E via D1: created test user 3, signed in via smoke-signin, hit endpoint, confirmed deleted_at populated, memberships=0, audit_log row inserted, kp_session cookie cleared.
  • TIER 3 (57643ee). Three distinct per-guide OG images (1200×630 PIL-generated, gold/sage/sienna palettes) at src/assets/og/; live byte counts confirm uniqueness. Sitemap <lastmod> on all 6 URLs. Cookie consent banner cloned to /demo/index.html (same DOM-API safe-build pattern as /app.html). Leave tenant now requires typing the workspace slug to confirm — symmetric with Delete Entire Tenant.
  • Test data cleaned up post-verification (user_id=3 + their membership row removed). Smoke fixture [email protected] unchanged.

Done previous session (2026-05-27 — auth-page brand fix + smoke OTP infra + Phase 2 batch backbone + R2 CORS recovery)

  • Auth-page logo conflict resolved (478a49a). Dropped redundant <h1 class="brand-name">KindredPics</h1> from signin/signup/forgot-password/reset-password (it stacked a serif "K" under the cursive monogram K). Promoted .card-title h2 → h1 for proper heading hierarchy. Monogram scaled 64 → 88px. .brand-name CSS rule retained for welcome.html (still uses it for the "Welcome" h1). Verified zero brand conflicts via chrome-devtools-mcp on production: signup + signin both report {h1s: ["<page title>"], brandNameExists: false, monogramHeight: 88}.
  • Smoke OTP peek infra shipped (478a49a, migration 028 applied). Hardcoded whitelist email [email protected] in functions/utils/otp.js (SMOKE_OTP_WHITELIST_EMAIL). When request-otp.js sees this email it ALSO writes the plaintext 6-digit code to smoke_otp_codes. New endpoint POST /api/auth/smoke-otp-peek (gated by env.SMOKE_TOKEN — same secret as smoke-signin) returns + marks consumed the latest plaintext for the whitelist email. Real users' codes remain SHA-256 hashed in email_otp_codes. Verified E2E: signup → peek → verify-otp → land at /app; sign out → signin OTP → peek → verify → land at /app (same user_id=2, tenant_id=2, family_code CMM7C6NV formatted as KP-CMM7-C6NV).
  • Phase 2 P0.4 backbone shipped (0150ae4 + afc53a2). Migration 029 (upload_batches table + photos.upload_batch_id index, applied to remote D1). Endpoints POST /api/batches/create (name + visibility, returns batch_id) and POST /api/batches/:id/close (idempotent, rejects if pending photos remain, sets upload_completed_at + purge_due_at = +30 days). sign.js accepts optional upload_batch_id; if omitted, find-or-create on a 30-min idle window for the same uploader (auto-batch fallback so existing clients aren't broken). Initial auto-close-on-pending=0 logic in finalize.js was racy (small batches finalize photo #1 before photo #2 signs, prematurely closing) — replaced with explicit /close endpoint. Verified E2E via the "Add photos" modal: 3 UI uploads (ui-a.jpg / ui-b.jpg / ui-c.jpg) all landed in upload_batch_id=4, status='upload_complete' after explicit close, photo retention + batch purge_due_at both at 2026-06-26.
  • R2 CORS recovered on kindredpics-photos (9a829dc). Browser-direct PUTs were failing with net::ERR_FAILED (preflight 403, "CORS not configured for this bucket"). The 2026-05-23 bucket rename from nanny-pics-photos dropped the prior CORS rules. Re-applied via wrangler r2 bucket cors set kindredpics-photos --file infra/r2-cors-kindredpics-photos.json — verified preflight returns 204 + Access-Control-Allow-Origin: https://www.kindredpics.com. Config + recovery steps now committed to infra/.

Done previous session (2026-05-25 morning — UI polish + role rename)

  • Step-4 illustration replaced (72a016e) — scene-matched watercolor (album + glasses + candle + rosemary). Resolves the cream-bg stopgap.
  • WebP conversion (df50929) — all 4 onboarding PNGs → WebP via PIL q=85 method=6. 10.2 MB → 655 KB (94% reduction). PNGs deleted; welcome.html refs swapped.
  • Landing OG swapped to 4-panel (d43c8c6) — 1200×630 JPG (129 KB) + WebP companion (65 KB), full og:image:width/height/type/alt + twitter:card summary_large_image. App/admin routes still use wordmark OG.
  • Role label "owner" → "host" (4d0c4fa) — 8 user-facing files updated. Driver: "owner" implied possessive hierarchy over family memories; "host" fits the kitchen-table metaphor. DB role value 'owner' unchanged; settings.html badge has display-mapping (role==='owner' ? 'host' : role). /api/tenants/transfer-ownership endpoint + JSON field new_owner_user_id unchanged. "tenant owner" in privacy/terms also swapped to "workspace host" for consistency. Legal terms kept: DMCA "rights owner," IP "ownership of every photo."
  • Inviter name on /signup lookup (8f7df45) — anti-phishing trust signal. Lookup-by-code endpoint adds JOIN to tenant_members+users; returns host_name. Preview text shows "Invited by {host} to {family} ({n} members)". Falls back to old "Found:" copy if host_name is null. No new personal data exposed (host display_name already public to members).

Friction-point sequencing (in-flight 2026-05-25)

After scenario walk-through, Devin approved fixes for friction points 1, 2, 5 (out of 5 identified for grandma's invite path):

  • #5 inviter name — shipped 8f7df45
  • #1 email OTP — shipped ec935be. Migration 027 (new email_otp_codes table; users.password_hash/salt/set_at relaxed to NULLABLE). Endpoints /api/auth/request-otp + /api/auth/verify-otp. Signup is OTP-only (two-step form). Signin is dual-path (OTP primary, password fallback via toggle). Sender stays [email protected] — stampready.org would need Resend Pro $20/mo, deferred per Q2 ADR. D1 wiped clean before migration (2 users + 2 tenants + 2 members removed per Devin authorization). New signups become user_id=1. Devin signup-tested end-to-end 2026-05-25 — workflow worked.
  • Brand tile asset (5ee3c3c) — src/assets/marketing/kp_brand_tile_1024.png. Framed KP monogram with four profile-face corner ornaments. For IG/Pinterest/press tile contexts only. Not a replacement for the in-product logo. Gemini ✧ scrubbed before commit.

Pending for next session (queued 2026-05-25 evening — interrupted mid-build)

  1. Logo conflict fix on auth pages (signin, signup, forgot-password, reset-password) — Devin flagged "conflicting and overlapping logos" on /signup after OTP signup-test. Issue: the lockup shows the kp_logo_letters_512.png KP monogram image AND a redundant <h1 class="brand-name">KindredPics</h1> text H1 directly below. The H1 "K" visually echoes the cursive K in the monogram → double-K stacking. Plan: drop the <h1 class="brand-name"> text, promote the page-specific .card-title (h2) to h1 for proper heading hierarchy, scale monogram 64px → 88px for presence. Apply uniformly to all 4 auth pages. Tagline stays.
  2. Smoke-test infra for visual OTP flow — Devin wants screenshot-able end-to-end test runs. Use existing KP_SMOKE_TOKEN + chrome-devtools-mcp + [email protected] as whitelisted test inbox. Plan: in request-otp.js, when email matches smoke whitelist (compare against env KP_SMOKE_TEST_EMAIL), also write the plaintext code to a separate smoke_otp_codes table (or KV with TTL). New endpoint /api/auth/smoke-otp-peek (gated by KP_SMOKE_TOKEN) returns the latest unconsumed plaintext for the whitelisted email. Real users' codes stay hashed and unrecoverable. Smoke script then drives: navigate /signup → fill form with whitelisted email → POST request-otp → poll smoke-otp-peek → fill code → verify → screenshot at every step. Output PNG sequence into C:/Users/devin/AppData/Local/Temp/kp-smoke/.
  3. Friction point #2 — Apple/Google OAuth — still deferred per recommendation until 50+ workspaces.
  4. ⏸️ #2 OAuth (Apple/Google) — deferred until 50+ active workspaces; OTP closes ~80% of the gap
  5. ⏸️ #3 server-side email invite — Devin chose NOT to ship; Web Share + inviter-name carry the load for now
  6. ⏸️ #4 i18n on /signup + /welcome — Devin chose NOT to ship; revisit when ICP language data demands it

Done previous session (2026-05-24 evening — /welcome watercolor illustrations)

  • Generated 4 onboarding wizard illustrations via Gemini 2.5 Flash Image (nano-banana). Style: hand-painted watercolor + faded graphite linework, kitchen-table memoir aesthetic. Cream/sienna/sage/dusty-rose palette, soft upper-left light, hand-drawn imperfection.
  • Step 1 (Welcome): photos + chipped mug + dried flower on weathered table — transparent PNG
  • Step 2 (Invite): two hands of different ages passing a small photograph — transparent PNG
  • Step 3 (Upload): open shoebox with photos floating up, knitted shawl draped on table — transparent PNG (minor text-leak on box label, acceptable)
  • Step 4 (Done): open family album + reading glasses + lit candle + rosemary sprig — REPLACED 2026-05-25 with scene-matched watercolor (commit 72a016e). All 4 illustrations use the same painted-checkerboard "transparency" border convention (note: they're opaque RGBA with painted art mimicking a transparent edge, not literal alpha=0).
  • Wired into src/welcome.html: new .step-illustration class (360px max, 4:3, drop-shadow), step 1 eager-loaded, steps 2-4 lazy-loaded. Width/height attrs prevent CLS. Alt text descriptive for screen-readers.
  • Bonus marketing assets staged at src/assets/marketing/: vertical 4-panel (story asset) + horizontal panorama (OG image candidate).
  • Deployed via bash scripts/deploy.sh — wrangler uploaded 5184 files, all 4 PNGs serving HTTP 200 at /assets/onboarding/. verify_deploy.sh flagged environment=None (known false-positive between CF API and the verify script), but the deploy landed.
  • Smoke-tested in Chrome via devtools MCP: authed via /api/auth/smoke-signin with KP_SMOKE_TOKEN, walked all 4 steps. Zero console errors, stepper dots advance correctly, illustrations composite cleanly against the white card (transparency works as expected — initial fullPage screenshot artifact showed checkerboard but real viewport renders fine).
  • Pushed to origin/main as 7b375f2 [BRAND] /welcome: wire watercolor step illustrations (1-4).

Open from this session: - Regenerate step 4 DONE 2026-05-25 commit 72a016e — scene-matched watercolor (album + glasses + candle + rosemary) drops cleanly into the painted-checkerboard set. - Convert PNGs to WebP DONE 2026-05-25 commit df50929 — PIL q=85 method=6 delivered 94% reduction (10.2 MB → 655 KB). PNGs deleted; welcome.html refs swapped to .webp. WebP support is universal (Safari 14+, 2020), no <picture> fallback wrapped. - Style drift acknowledged on steps 1-3 (polished Ghibli-leaning) vs. the softer original watercolor anchor — Devin accepted as cohesive set; flag for revisit if brand polish ever escalates. - welcome-panorama.png as og:image REJECTED 2026-05-25 — confusing as a single image. Used welcome-4-panel.png instead (commit d43c8c6): center-cropped 1200×896 → 1200×630, exported as JPG (129 KB) + WebP (65 KB), wired into src/index.html with full og:image:width/height/type/alt + twitter:card summary_large_image. App/admin routes still use the wordmark OG.

Done this session (2026-05-23)

  • Landing page redesigned: 4-section flow (Question→Relatable→Problem→Solution), KP script wordmark, centered narrow column (kills wasted right-side space). Commits 1e38107, 02f2882, etc.
  • KP script wordmark persisted across all surfaces: favicon (kp_logo_square_512.png), apple-touch (kp_logo_apple_180.png), OG image (kp_logo_og_1200x630.png 1200×630 cream), topbar/footer, hero, auth pages (4), app.html sidebar/mobile-topbar via rewritten Wordmark + nulled Crest. Old logo-* PNGs deleted.
  • iOS Safari upload fix ee13c2d confirmed holding — Sentry KINDREDPICS-WEB-2 resolved.
  • CF Pages env var [email protected] set.
  • /photos modal trap fix 1f297ef#album-modal/#share-modal hidden attribute now respected via !important (inline display:flex was overriding user-agent [hidden] { display: none }).
  • /welcome 4-step onboarding wizard 106324c/d2c297a: Welcome → Invite (code + copy + Web Share) → Upload (camera roll) → Done. Auto-shown on first signin/signup, opt-out via localStorage.kp_onboarded. Verified end-to-end on prod.
  • Orphan SMOKE_TOKEN rotated. New value stored as KP_SMOKE_TOKEN in kindredpics-site/.env; documented in FounderOS/deliverables/SEC-secrets_inventory_v1.0_2026-05-23.md.
  • Davidson tenant_id=1 family_code migrated from 12-char to 8-char (KP-B385-YKRS). Soft-launch suite now 27/27 + 6/6 family-code pass.

Do NOT

  • Don't claim KP is "live" with the workspace model end-to-end — Phases 2-4 are still ahead. Only retention + auth + auto-Rekog-removal shipped.
  • Don't add a Rekognition call from any upload path — auto-Rekog is explicitly removed (Codex P0.3). Face indexing only from admin manual trigger until Phase 3 paid-scan UI ships.
  • Don't soften the 30-day retention without counsel sign-off — it's BIPA-class compliance, not a UX choice.
  • Don't git push and assume deploy. Run bash scripts/deploy.sh explicitly.
  • Don't sign content-type on R2 presigned PUTs — Safari mangles. Sign only host.
  • Don't add a new public route without updating _middleware.js bypass list.
  • Don't reinstate magic-link in any form — it's been replaced + 410'd.
  • Don't rewrite the kitchen-table hero promise without asking — founder's exact words are locked.
  • Old nanny-pics D1/R2 deletion is DONE (2026-05-26) — no longer a guard rail.

Last handoff

deliverables/OPS-handoff_v1.0_2026-05-23.md exists in the KP repo (pre-FounderOS). After next real KP session: pwsh -File C:\Users\devin\FounderOS\scripts\new-handoff.ps1 -Project kindredpics.

Open decisions

See decisions/2026-05-23-kp-rebuild-followups.md for the full SPEC Q2-Q9 disposition. Status:

Resolved (decided 2026-05-23): - Q3 multi-tenant Library view: filter dropdown with surnames ([All ▾] [Davidson] [Smith]) - Q4 tenant-switcher in upload UI: yes — required when user has 2+ memberships - Q6 old-resource grace: 24hr — already shipped in rebuild commit, delete old nanny-pics D1/R2 after 2026-05-24 - Q7 auto-rekog removal phase: Phase 1 — already shipped via Codex P0.3 partial - Q8 legal/policy questions: defer all four to counsel; Phase 2 ships without them. Conservative defaults: no extensions, no non-face labels, single 30-day retention preset, owner/admin only for paid scans - Family-code format: TARGET = 4-4 Crockford KP-XXXX-XXXX (shipped is 4-4-4 12-char; migration free)

Decided 2026-05-23 evening (full disposition in ADR): - Q2 password-reset From: KindredPics <[email protected]> (existing Resend sender; brand-bleed accepted to avoid $20/mo Resend Pro). Add Reply-To: [email protected]. Revisit on first paying customer / deliverability complaint. - Q5 smoke-signin fate: already resolved by post-rebuild code (SMOKE_TOKEN-gated session mint, no email). Magic-link removal closed the gap implicitly. - Q9 marketing-docs rewrite: hybrid surgical edit (~30% of docs). Keep kitchen-table promise + Ancestry frame + ICP triggers + founder story; edit retention/storage language.

Other open: - Storage tier model (docs/icp.md flagged): likely moot under 30-day framing - Cloudflare Queues for parallel Rekognition: lower priority now that Rekog is admin-manual-only - Client-side EXIF strip: P0.1 server-side shipped; client-side JPEG strip before R2 PUT still on list - KP CLAUDE.md rewrite: drop magic-link references; reframe "Auth model (post-2026-05-22)" section - [email protected] domain/email verification: blocking the password-reset From wire-up