15
votes

I'm working on an iOS app which has (whoah surprise!) chat functionality. The whole app is heavily using the Firebase Tools, for the database I’m using the new Cloud Firestore solution.

Currently I'm in the process of tightening the security using the database rules, but I'm struggling a bit with my own data model :) This could mean that my data model is poorly chosen, but I'm really happy with it, except for implementing the rules part.

The conversation part of the model looks like this. At the root of my database I have a conversations collection:

/conversations/$conversationId
        - owner // id of the user that created the conversation
        - ts // timestamp when the conversation was created
        - members: {
                $user_id_1: true // usually the same as 'owner'
                $user_id_2: true // the other person in this conversation
                ...
          }
        - memberInfo: {
                // some extra info about user typing, names, last message etc.
                ...
          }

And then I have a subcollection on each conversation called messages. A message document is a very simple and just holding information about each sent message.

/conversations/$conversationId/messages/$messageId
        - body
        - sender
        - ts

And a screenshot of the model: the model

The rules on the conversation documents are fairly straightforward and easy to implement:

match /conversations/{conversationId} {
  allow read, write: if resource.data.members[(request.auth.uid)] == true;

  match /messages/{messageId} {
        allow read, write: if get(/databases/$(database)/documents/conversations/$(conversationId)).data.members[(request.auth.uid)] == true;
  }
}

Problem

My problem is with the messages subcollection in that conversation. The above works, but I don’t like using the get() call in there. Each get() call performs a read action, and therefore affects my bill at the end of the month, see documentation.

...

Which might become a problem if the app I’m building will become a succes, the document reads ofcourse are really minimal, but to do it every time a user opens a conversation seems a bit inefficient. I really like the subcollection solution in my model, but not sure how to efficiently implement the rules here.

I'm open for any datamodel change, my goal is to evaluate the rules without these get() calls. Any idea is very welcome.

2

2 Answers

9
votes

Honestly, I think you're okay with your structure and get call as-is. Here's why:

  1. If you're fetching a bunch of documents in a subcollection, Cloud Firestore is usually smart enough to cache values as needed. For example, if you were to ask to fetch all 200 items in "conversions/chat_abc/messages", Cloud Firestore would only perform that get operation once and re-use it for the entire batch operation. So you'll end up with 201 reads, and not 400.

  2. As a general philosophy, I'm not a fan of optimizing for pricing in your security rules. Yes, you can end up with one or two extra reads per operation, but it's probably not going to cause you trouble the same way, say, a poorly written Cloud Function might. Those are the areas where you're better off optimizing.

2
votes

If you want to save those extra reads, you can actually implement a "cache" based on custom claims.

You can, for example, save the chats the user has access to in the custom claims under the object "conversations". Keep in mind custom claims has a limit of 1000 bytes as mentioned in their documentation.

One workaround to the limit is to just save the most recent conversations in the custom claims, like the top 50. Then in the security rules you can do this:

allow read, write: if request.auth.token.conversations[conversationId] || get(/databases/$(database)/documents/conversations/$(conversationId)).data.members[(request.auth.uid)] == true;

This is especially great if you're already using cloud functions to moderate messages after they were posted, all you need is to update the custom claims