rules_version = '2'; service firebase.storage { match /b/{bucket}/o { // Profile photos: only the owner may write; both the owner and their // partner can read via the tokenized download URL. Direct read is // scoped to the owner so raw storage paths aren't publicly accessible. // // Size cap: 5 MB. Content-type must be an image (enforced at upload, // not at read, so the header check only blocks mismatched writes). match /users/{uid}/profile.jpg { allow write: if request.auth != null && request.auth.uid == uid && request.resource.size < 5 * 1024 * 1024 && request.resource.contentType.matches('image/.*'); // Partners receive the tokenized download URL (generated server-side or // at upload time) which bypasses these rules. Direct rule-based read is // scoped to the owner only. allow read: if request.auth != null && request.auth.uid == uid; } // Encrypted chat media: the author writes under their own path (already E2E-encrypted // ciphertext, so Storage never holds anything readable). The partner reads via the tokenized // download URL, which bypasses these rules — same model as profile photos. 15 MB cap. match /users/{uid}/chat_media/{file} { allow write: if request.auth != null && request.auth.uid == uid && request.resource.size < 15 * 1024 * 1024; allow read: if request.auth != null && request.auth.uid == uid; } // Encrypted conversation-backup snapshots. Written by the uploading member under their OWN path // (couple-key ciphertext, so Storage holds nothing readable). The PARTNER reads via the tokenized // download URL recorded in the couple-gated backup manifest — same model as chat media. Living // under users/{uid}/ means the existing onUserDelete `users/{uid}/` cleanup covers backups too. // 50 MB cap (a full-history snapshot for a long-lived couple). match /users/{uid}/backups/{file} { allow write: if request.auth != null && request.auth.uid == uid && request.resource.size < 50 * 1024 * 1024; allow read: if request.auth != null && request.auth.uid == uid; } // Deny all other paths by default. match /{allPaths=**} { allow read, write: if false; } } }