rules • Lines: 257rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// ── Helpers (check both prod and dev_ prefixed collections) ──
function isVerified() {
return request.auth != null
&& (
(exists(/databases/$(database)/documents/profiles/$(request.auth.uid))
&& get(/databases/$(database)/documents/profiles/$(request.auth.uid)).data.emailVerified == true)
||
(exists(/databases/$(database)/documents/dev_profiles/$(request.auth.uid))
&& get(/databases/$(database)/documents/dev_profiles/$(request.auth.uid)).data.emailVerified == true)
);
}
function isOwnUsername(uname) {
return uname == "" ||
(
uname is string
&& uname.size() >= 3
&& (
(exists(/databases/$(database)/documents/usernames/$(uname))
&& get(/databases/$(database)/documents/usernames/$(uname)).data.uid == request.auth.uid)
||
(exists(/databases/$(database)/documents/dev_usernames/$(uname))
&& get(/databases/$(database)/documents/dev_usernames/$(uname)).data.uid == request.auth.uid)
)
);
}
function userRole(uid) {
return exists(/databases/$(database)/documents/profiles/$(uid))
? get(/databases/$(database)/documents/profiles/$(uid)).data.role
: exists(/databases/$(database)/documents/dev_profiles/$(uid))
? get(/databases/$(database)/documents/dev_profiles/$(uid)).data.role
: null;
}
function profileData(uid) {
return exists(/databases/$(database)/documents/profiles/$(uid))
? get(/databases/$(database)/documents/profiles/$(uid))
: get(/databases/$(database)/documents/dev_profiles/$(uid));
}
function profileExists(uid) {
return exists(/databases/$(database)/documents/profiles/$(uid))
|| exists(/databases/$(database)/documents/dev_profiles/$(uid));
}
function usernameData(uname) {
return exists(/databases/$(database)/documents/usernames/$(uname))
? get(/databases/$(database)/documents/usernames/$(uname))
: get(/databases/$(database)/documents/dev_usernames/$(uname));
}
// ── Production: scores ──
match /scores/{docId} {
allow read: if true;
allow create: if isVerified()
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.name is string
&& request.resource.data.name.size() >= 1
&& request.resource.data.rawShorthand is string
&& request.resource.data.rawShorthand.size() >= 2
&& request.resource.data.rawShorthand.size() <= 10000
&& request.resource.data.username is string
&& request.resource.data.username.size() >= 1
&& request.resource.data.slug is string
&& request.resource.data.slug.size() >= 1
&& (!("published" in request.resource.data) || request.resource.data.published is bool);
allow update: if isVerified()
&& request.auth.uid == resource.data.userId
&& (!("userId" in request.resource.data) || request.resource.data.userId == resource.data.userId)
&& request.resource.data.name is string
&& request.resource.data.name.size() >= 1
&& request.resource.data.rawShorthand is string
&& request.resource.data.rawShorthand.size() >= 2
&& request.resource.data.rawShorthand.size() <= 10000
&& request.resource.data.username is string
&& request.resource.data.username.size() >= 1
&& request.resource.data.slug is string
&& request.resource.data.slug.size() >= 1
&& (!("published" in request.resource.data) || request.resource.data.published is bool);
allow delete: if isVerified()
&& request.auth.uid == resource.data.userId;
}
// ── Dev: scores ──
match /dev_scores/{docId} {
allow read: if true;
allow create: if isVerified()
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.name is string
&& request.resource.data.name.size() >= 1
&& request.resource.data.rawShorthand is string
&& request.resource.data.rawShorthand.size() >= 2
&& request.resource.data.rawShorthand.size() <= 10000
&& request.resource.data.username is string
&& request.resource.data.username.size() >= 1
&& request.resource.data.slug is string
&& request.resource.data.slug.size() >= 1
&& (!("published" in request.resource.data) || request.resource.data.published is bool);
allow update: if isVerified()
&& request.auth.uid == resource.data.userId
&& (!("userId" in request.resource.data) || request.resource.data.userId == resource.data.userId)
&& request.resource.data.name is string
&& request.resource.data.name.size() >= 1
&& request.resource.data.rawShorthand is string
&& request.resource.data.rawShorthand.size() >= 2
&& request.resource.data.rawShorthand.size() <= 10000
&& request.resource.data.username is string
&& request.resource.data.username.size() >= 1
&& request.resource.data.slug is string
&& request.resource.data.slug.size() >= 1
&& (!("published" in request.resource.data) || request.resource.data.published is bool);
allow delete: if isVerified()
&& request.auth.uid == resource.data.userId;
}
// ── Production: comments ──
match /comments/{docId} {
allow read: if true;
allow create: if isVerified()
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.text is string
&& request.resource.data.text.size() >= 1
&& request.resource.data.text.size() <= 2000;
allow update: if isVerified();
allow delete: if isVerified()
&& (
resource.data.userId == request.auth.uid
|| userRole(request.auth.uid) == "moderator"
);
}
// ── Dev: comments ──
match /dev_comments/{docId} {
allow read: if true;
allow create: if isVerified()
&& request.resource.data.userId == request.auth.uid
&& request.resource.data.text is string
&& request.resource.data.text.size() >= 1
&& request.resource.data.text.size() <= 2000;
allow update: if isVerified();
allow delete: if isVerified()
&& (
resource.data.userId == request.auth.uid
|| userRole(request.auth.uid) == "moderator"
);
}
// ── Production: profiles ──
match /profiles/{userId} {
allow read: if true;
allow create, update: if request.auth != null
&& request.auth.uid == userId
&& (!("displayName" in request.resource.data) || request.resource.data.displayName is string)
&& (!("username" in request.resource.data) || isOwnUsername(request.resource.data.username));
allow delete: if false;
}
// ── Dev: profiles ──
match /dev_profiles/{userId} {
allow read: if true;
allow create, update: if request.auth != null
&& request.auth.uid == userId
&& (!("displayName" in request.resource.data) || request.resource.data.displayName is string)
&& (!("username" in request.resource.data) || isOwnUsername(request.resource.data.username));
allow delete: if false;
}
// ── Production: usernames ──
match /usernames/{username} {
allow read: if true;
allow create: if request.auth != null
&& request.resource.data.uid == request.auth.uid
&& (
!profileExists(request.auth.uid)
|| profileData(request.auth.uid).data.username == ""
|| profileData(request.auth.uid).data.username == username
);
allow delete: if request.auth != null
&& resource.data.uid == request.auth.uid;
allow update: if false;
}
// ── Dev: usernames ──
match /dev_usernames/{username} {
allow read: if true;
allow create: if request.auth != null
&& request.resource.data.uid == request.auth.uid
&& (
!profileExists(request.auth.uid)
|| profileData(request.auth.uid).data.username == ""
|| profileData(request.auth.uid).data.username == username
);
allow delete: if request.auth != null
&& resource.data.uid == request.auth.uid;
allow update: if false;
}
// ── Production: page tracking ──
match /page_stats/{docId} {
allow read: if true;
allow write: if request.auth != null;
}
match /visits/{docId} {
allow read: if true;
allow write: if request.auth != null;
}
// ── Dev: page tracking ──
match /dev_page_stats/{docId} {
allow read: if true;
allow write: if request.auth != null;
}
match /dev_visits/{docId} {
allow read: if true;
allow write: if request.auth != null;
}
// ── Production: article claps ──
match /claps/{contentId} {
allow read: if true;
allow write: if request.auth != null;
}
// ── Dev: article claps ──
match /dev_claps/{contentId} {
allow read: if true;
allow write: if request.auth != null;
}
}
}