Business Management · By Emalfred Digital
Sign in to your business dashboard
pk_test_ or pk_live_.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function isOwner(ownerUid) {
return request.auth.uid == ownerUid;
}
function isActive() {
return request.auth.token.accountStatus == 'active'
|| !('accountStatus' in request.auth.token);
}
function hasPlan() {
// Accept both casings defensively: your app stores plan as Title Case
// ("Basic"/"Pro"/"Enterprise") but this rule previously only matched
// lowercase — verify against your actual Cloud Functions claim values
// and drop whichever casing turns out to be unused.
return request.auth.token.plan in ['basic','pro','enterprise','Basic','Pro','Enterprise']
|| !('plan' in request.auth.token);
}
function isStaffOf(ownerUid) {
// Your app only ever assigns roles "Owner"/"Staff"/"Viewer" — 'manager'
// never appears anywhere in the codebase and 'staff' (lowercase) never
// matched your actual "Staff" role string. Fixed to match your real
// role vocabulary; both casings accepted until claim casing is confirmed.
return request.auth.token.ownerUid == ownerUid
&& request.auth.token.role in ['Staff','staff'];
}
function isViewerOf(ownerUid) {
// Previously there was no read-only tier at all: a "Viewer" role could
// never satisfy isStaffOf(), so Viewer accounts had zero access to
// their own business's data. This restores read-only access without
// granting Viewers any write capability.
return request.auth.token.ownerUid == ownerUid
&& request.auth.token.role in ['Viewer','viewer'];
}
function isSuperAdmin() {
return request.auth.token.superAdmin == true
|| request.auth.token.email == 'info@emalfred.com';
}
function isOwnerOfBiz() {
// Same casing issue as isStaffOf: app stores "Owner" (Title Case), the
// rule only ever matched lowercase 'owner'. An owner whose token
// already has role:"Owner" set would previously fail this check
// outright (the "claim absent" fallback only applies to brand-new
// signups, not existing owners) — most likely breaking owner-invites-
// staff and similar owner-initiated writes. Both casings accepted
// until claim casing is confirmed against your Cloud Functions.
return request.auth.token.role in ['Owner','owner']
|| !('role' in request.auth.token);
}
match /admins/{adminId} {
allow read, write: if isSignedIn() && isSuperAdmin();
}
match /adminPractice/{uid}/{document=**} {
allow read, write: if isSignedIn()
&& request.auth.uid == uid
&& isSuperAdmin();
}
match /users/{userId} {
allow read: if isSignedIn()
&& (
request.auth.uid == userId
|| isSuperAdmin()
// Lets an Owner query their own staff list (where ownerUid ==
// their uid) — Firestore can validate this per-document for a
// query, unlike the uid==userId check above, which only ever
// works for a direct single-document read of your own doc.
// Root cause of "created staff not showing in the staff list
// on a fresh session" — the query was always being rejected.
|| resource.data.ownerUid == request.auth.uid
);
allow write: if isSignedIn() && isSuperAdmin();
allow create: if isSignedIn()
&& isActive()
&& isOwnerOfBiz()
&& request.resource.data.ownerId == request.auth.uid;
allow update: if isSignedIn()
&& isActive()
&& (
isSuperAdmin()
|| (isOwnerOfBiz() && resource.data.ownerId == request.auth.uid)
|| (
// A user may edit their OWN doc, but only a safe allow-list of
// fields — never role/ownerId/plan/accountStatus, which would
// otherwise let a client self-elevate privileges client-side.
request.auth.uid == userId
&& (
request.resource.data.diff(resource.data).affectedKeys()
.hasOnly(['logoUrl','bizCurrency','customTaxName','customTaxRate'])
|| (
// Narrow self-repair carve-out: the designated platform-owner
// email may restore its OWN role to SuperAdmin if it was ever
// corrupted, and may change nothing else in the same write.
request.auth.token.email == 'info@emalfred.com'
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['role'])
&& request.resource.data.role == 'SuperAdmin'
)
)
)
);
allow delete: if isSignedIn()
&& (
isSuperAdmin()
|| (isOwnerOfBiz() && resource.data.ownerId == request.auth.uid)
);
}
match /activityLog/{logId} {
allow read, write: if isSignedIn() && isSuperAdmin();
}
match /payments/{paymentId} {
allow read, write: if isSignedIn() && isSuperAdmin();
}
match /settings/{settingId} {
allow read, write: if isSignedIn() && isSuperAdmin();
}
match /bizData/{ownerUid} {
allow read: if isSignedIn()
&& (isOwner(ownerUid) || isStaffOf(ownerUid) || isViewerOf(ownerUid) || isSuperAdmin())
&& isActive();
allow write: if isSignedIn()
&& (isOwner(ownerUid) || isSuperAdmin())
&& isActive();
match /branches/{branchId} {
allow read: if isSignedIn()
&& (isOwner(ownerUid) || isStaffOf(ownerUid) || isViewerOf(ownerUid) || isSuperAdmin())
&& isActive();
allow write: if isSignedIn()
&& (isOwner(ownerUid) || isSuperAdmin())
&& isActive();
match /{collection}/{docId} {
allow read: if isSignedIn()
&& (isOwner(ownerUid) || isStaffOf(ownerUid) || isViewerOf(ownerUid) || isSuperAdmin())
&& isActive();
allow create: if isSignedIn()
&& (isOwner(ownerUid) || isStaffOf(ownerUid) || isSuperAdmin())
&& isActive()
&& hasPlan();
allow update: if isSignedIn()
&& (isOwner(ownerUid) || isStaffOf(ownerUid) || isSuperAdmin())
&& isActive();
allow delete: if isSignedIn()
&& (isOwner(ownerUid) || isSuperAdmin())
&& isActive();
}
}
match /stockAlerts/{alertId} {
allow read, write: if isSignedIn()
&& (isOwner(ownerUid) || isSuperAdmin())
&& isActive();
}
// ── ADDED: activity log written by logAct() for client accounts.
// Without this block, writes/reads to bizData/{ownerUid}/activityLog
// fall through to the default deny — this is what caused the
// "Missing or insufficient permissions" error on client logins,
// and why the platform owner's Log tab never showed client activity.
match /activityLog/{logId} {
allow read: if isSignedIn()
&& (isOwner(ownerUid) || isStaffOf(ownerUid) || isViewerOf(ownerUid) || isSuperAdmin())
&& isActive();
allow write: if isSignedIn()
&& (isOwner(ownerUid) || isStaffOf(ownerUid) || isSuperAdmin())
&& isActive();
}
}
match /userData/{uid}/{document=**} {
// Previously only the literal doc owner (request.auth.uid == uid) had
// any access here at all — no isStaffOf/isViewerOf/isSuperAdmin branch
// existed. That meant Basic-plan staff/viewers were completely locked
// out of their own business's data, and SuperAdmin-side tools reading
// a Basic-plan client's data (e.g. the Import tab) would fail with
// permission-denied. Mirrored the same owner/staff/viewer/admin split
// already used throughout bizData, so Basic-plan tenants get the same
// access model as Pro/Enterprise ones — least-privilege preserved:
// Viewers still cannot write, Staff cannot delete.
allow read: if isSignedIn()
&& (request.auth.uid == uid || isStaffOf(uid) || isViewerOf(uid) || isSuperAdmin())
&& isActive();
allow create, update: if isSignedIn()
&& (request.auth.uid == uid || isStaffOf(uid) || isSuperAdmin())
&& isActive()
&& hasPlan();
allow delete: if isSignedIn()
&& (request.auth.uid == uid || isSuperAdmin())
&& isActive();
}
}
}
Business Management · By Emalfred Digital
Fill in your details to get started with TrackBizPro