rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{userId} { // Validate write operations function validateWrite(affectedKeys){ // Keys that the authenticated user should not be able to update let protectedKeys = [ 'admin', 'stripeCustomerId', 'stripeSubscriptionId', 'stripePriceId', 'stripeSubscriptionStatus' ]; return ( // Make sure `email` is a string futureData().email is string // Require `email` be between 3 and 500 chars // 254 is technically the limit but overshoot to be safe (stackoverflow.com/a/574698) && futureData().email.trim().size() >= 3 && futureData().email.trim().size() <= 500 && ( // Make sure `name` either doesn't exist or ... !('name' in futureData().keys()) || ( // Make sure `name` is a string futureData().name is string // And require that it's between 1 and 144 chars && futureData().name.trim().size() >= 1 && futureData().name.trim().size() <= 144 ) ) // Make sure no protected keys were affected && affectedKeys.hasAny(protectedKeys) == false // Alternatively, instead of protected keys you could have an allow list //&& futureData().keys().hasOnly(allowedKeys) ); } // The authenticated user can only read their own doc allow read: if isUser(userId); // For create/update we call a validation function and pass in affected keys allow create: if isUser(userId) && validateWrite(futureData().keys()); allow update: if isUser(userId) && validateWrite(affectedKeys()); // The user doc can't be deleted (better to add an `isDeleted` field if that's needed) allow delete: if false; } match /items/{itemId} { // Validate write operations function validateWrite(affectedKeys){ return ( // Make sure `name` is a string futureData().name is string // Require `name` be between 1 and 144 chars && futureData().name.trim().size() >= 1 && futureData().name.trim().size() <= 144 // Make sure `featured` is a bool if it exists && (!('featured' in futureData().keys()) || futureData().featured is bool) // Write rules specific to the user's plan // By default this ensures only certain plans can update `featured` // Uncomment the following line after you've added your Stripe plans below //&& validateWriteForPlan(affectedKeys) ); } function validateWriteForPlan(affectedKeys){ // Add your Stripe plans here (also called "Price IDs") let starterPlan = 'price_xxxxxxxxxxxxxxx'; let proPlan = 'price_xxxxxxxxxxxxxxx'; let businessPlan = 'price_xxxxxxxxxxxxxxx'; // Specify protected keys for each plan // Currently we prevent updating of `featured` if user has no plan or is on starter plan let protectedKeysNoPlan = ['featured']; let protectedKeysStarter = ['featured']; // Add protected keys for the other plans if you'd like //let protectedKeysPro = []; // Get extra user data let user = getUserData(); // Specify write conditions for each plan (currently just check each plan's protected keys) // Note: There must be a `userHasPlan` check for each plan or write will fail for missing plans return ( (userHasNoPlan(user) && affectedKeys.hasAny(protectedKeysNoPlan) == false) || (userHasPlan(user, starterPlan) && affectedKeys.hasAny(protectedKeysStarter) == false) || (userHasPlan(user, proPlan)) || (userHasPlan(user, businessPlan)) // Example: Here's how you'd require the name "divjoy is cool" under the business plan //|| (userHasPlan(user, businessPlan) && futureData().name == "divjoy is cool") ); } // Can only read item if the authenticated user is the owner allow read: if isOwner(); // This would allow reads from any user //allow read: if true; // For create/update we call a validation function and pass in affected keys allow create: if wouldBeOwner() && validateWrite(futureData().keys()); // Notice when updating we need to make sure authenticated user is currently the owner // and that they would still be the owner if the write is successful (aka they can't change the owner) allow update: if isOwner() && wouldBeOwner() && validateWrite(affectedKeys()); // Users can delete their own items allow delete: if isOwner(); } // Helper functions that simplify our rules // Check if authenticated user's `uid` matches the specified `userId` function isUser(userId) { return request.auth.uid != null && request.auth.uid == userId; } // Get current data function currentData() { return resource.data; } // Get future data (the final data set if update goes through) function futureData() { return request.resource.data; } // Check if authenticated user's `uid` matches data `owner` function isOwner(){ return isUser(currentData().owner); } // Check if authenticated user's `uid` matches future data `owner` function wouldBeOwner(){ return isUser(futureData().owner); } // Get keys affected by an update // Requires a diff between `futureData` and `currentData` function affectedKeys() { return futureData().diff(currentData()).affectedKeys(); } // Query for extra user data belonging to the authenticated user function getUserData(){ return get(/databases/$(database)/documents/users/$(request.auth.uid)).data; } // Check if user has an active plan function planIsActive(user){ return 'stripeSubscriptionStatus' in user && [user.stripeSubscriptionStatus].toSet().hasAny(['active', 'trialing']); } // Check if user has the specified plan function userHasPlan(user, plan){ return planIsActive(user) && 'stripePriceId' in user && user.stripePriceId == plan } // Check if user has no plan function userHasNoPlan(user){ return planIsActive(user) == false; } } }