From a77b2951243e9c50a65732033a52ad5ff3c6cec9 Mon Sep 17 00:00:00 2001 From: null Date: Tue, 30 Jun 2026 16:52:01 -0500 Subject: [PATCH] feat(date-memories): add date_history + date_reflections Firestore security rules (batch 6/8) --- firestore.rules | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/firestore.rules b/firestore.rules index 7762905e..278f1a8a 100644 --- a/firestore.rules +++ b/firestore.rules @@ -643,6 +643,47 @@ service cloud.firestore { allow delete: if isCouplesMember(coupleId); } + // Date Replay — completed-date log. PLAINTEXT app metadata (date-idea title/category + timestamp, + // not private words; the reflection content is E2EE below). Idempotent merge on doc id = matchId. + match /date_history/{dateId} { + allow read: if isCouplesMember(coupleId); + allow create, update: if isCouplesMember(coupleId) + && request.resource.data.keys().hasOnly(['dateIdeaId', 'title', 'category', 'completedAt', 'addedBy']) + && request.resource.data.addedBy is string + && request.resource.data.completedAt is number; + allow delete: if isCouplesMember(coupleId); + } + + // Date reflection metadata: each user writes their own; both members read (drives "your turn"). + // Mirrors daily_question/answers — encrypted content is in the read-gated `secure` subdoc below. + match /date_reflections/{dateId}/answers/{userId} { + allow read: if isCouplesMember(coupleId); + allow create: if isCouplesMember(coupleId) + && request.auth.uid == userId + && request.resource.data.userId == request.auth.uid + && request.resource.data.isRevealed == false + && request.resource.data.keys().hasOnly(['userId', 'schemaVersion', 'createdAt', 'updatedAt', 'isRevealed']); + allow update: if isCouplesMember(coupleId) + && request.auth.uid == userId + && request.resource.data.userId == resource.data.userId + // Only the reveal flag may flip; the encrypted payload is immutable. + && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['isRevealed', 'updatedAt']); + allow delete: if false; + + // Couple-key encrypted reflection content. Read-gated: you can read your PARTNER's content only + // once YOU have also reflected (the "private until both" gate). Your own content is always readable. + match /secure/{doc} { + allow read: if isCouplesMember(coupleId) + && (request.auth.uid == userId + || exists(/databases/$(database)/documents/couples/$(coupleId)/date_reflections/$(dateId)/answers/$(request.auth.uid))); + allow create: if isCouplesMember(coupleId) + && request.auth.uid == userId + && isCiphertext(request.resource.data.encryptedPayload) + && request.resource.data.keys().hasOnly(['encryptedPayload']); + allow update, delete: if false; + } + } + // Couple Lore stores revealed answer summaries. Summary text must remain // encrypted with the couple key; prompts/metadata can stay plaintext. match /lore/{loreId} {