Setting the file. One moment.
Chapter 07 · Firebase Data Connect
Subchapter 7.14
reference/security.mdMarkdown9 KBView on GitHub
Every deployable query/mutation must have @auth. Without it, operations
default to NO_ACCESS.
query PublicData @auth(level: PUBLIC) { ... }
query UserData @auth(level: USER) { ... }
query AdminOnly @auth(expr: "auth.token.admin == true") { ... }| Argument | Description |
|---|---|
level | Preset access level |
expr | CEL expression (alternative to level) |
insecureReason | Suppress deploy warning for PUBLIC/unfiltered USER |
| Level | Who Can Access | CEL Equivalent |
|---|---|---|
PUBLIC | Anyone, authenticated or not | true |
USER_ANON | Any authenticated user (including anonymous) | auth.uid != nil |
USER | Authenticated users (excludes anonymous) | auth.uid != nil && auth.token.firebase.sign_in_provider != 'anonymous' |
USER_EMAIL_VERIFIED | Users with verified email | auth.uid != nil && auth.token.email_verified |
NO_ACCESS | Admin SDK only | false |
Important: Levels like
USERare starting points. Always add filters or expressions to verify the user can access specific data.
| Binding | Description |
|---|---|
auth.uid | Current user’s Firebase UID |
auth.token | Auth token claims (see below) |
vars | Operation variables (e.g., vars.movieId) |
request.time | Server timestamp |
request.operationName | “query” or “mutation” |
| Field | Description |
|---|---|
email | User’s email address |
email_verified | Boolean: email verified |
phone_number | User’s phone |
name | Display name |
sub | Firebase UID (same as auth.uid) |
firebase.sign_in_provider | password, google.com, anonymous, etc. |
<custom_claim> | Custom claims set via Admin SDK |
# Check custom claim
@auth(expr: "auth.token.role == 'admin'")
# Check verified email domain
@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')")
# Check multiple conditions
@auth(expr: "auth.uid != nil && (auth.token.role == 'editor' || auth.token.role == 'admin')")
# Check variable
@auth(expr: "has(vars.status) && vars.status in ['draft', 'published']")Compare database fields with auth values:
query MyPosts @auth(level: USER) {
posts(where: { authorUid: { eq_expr: "auth.uid" }}) {
id title
}
}
mutation UpdateMyPost($id: UUID!, $title: String!) @auth(level: USER) {
post_update(
first: { where: {
id: { eq: $id },
authorUid: { eq_expr: "auth.uid" }
}},
data: { title: $title }
)
}Use @check to validate data and @redact to hide results from client:
Validates a field value; aborts if check fails.
@check(expr: "this != null", message: "Not found")
@check(expr: "this == 'editor'", message: "Must be editor")
@check(expr: "this.exists(p, p.role == 'admin')", message: "No admin found")| Argument | Description |
|---|---|
expr | CEL expression; this = current field value |
message | Error message if check fails |
optional | If true, pass when field not present |
Hides field from response (still evaluated for @check):
query @redact { ... } # Query result hidden but @check still runsCheck database permissions before allowing mutation:
mutation UpdateMovie($id: UUID!, $title: String!)
@auth(level: USER)
@transaction {
# Step 1: Check user has permission
query @redact {
moviePermission(
key: { movieId: $id, userId_expr: "auth.uid" }
) @check(expr: "this != null", message: "No access to movie") {
role @check(expr: "this == 'editor'", message: "Must be editor")
}
}
# Step 2: Update if authorized
movie_update(id: $id, data: { title: $title })
}mutation MustDeleteMovie($id: UUID!) @auth(level: USER) @transaction {
movie_delete(id: $id)
@check(expr: "this != null", message: "Movie not found")
}# Create with owner
mutation CreatePost($content: String!) @auth(level: USER) {
post_insert(data: {
authorUid_expr: "auth.uid",
content: $content
})
}
# Read own data only
query MyPosts @auth(level: USER) {
posts(where: { authorUid: { eq_expr: "auth.uid" }}) {
id content
}
}
# Update own data only
mutation UpdatePost($id: UUID!, $content: String!) @auth(level: USER) {
post_update(
first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}},
data: { content: $content }
)
}
# Delete own data only
mutation DeletePost($id: UUID!) @auth(level: USER) {
post_delete(
first: { where: { id: { eq: $id }, authorUid: { eq_expr: "auth.uid" }}}
)
}# Admin-only query
query AllUsers @auth(expr: "auth.token.admin == true") {
users { id email name }
}
# Role from database
mutation AdminAction($id: UUID!) @auth(level: USER) @transaction {
query @redact {
user(key: { uid_expr: "auth.uid" }) {
role @check(expr: "this == 'admin'", message: "Admin required")
}
}
# ... admin action
}query PublicPosts @auth(level: PUBLIC) {
posts(where: {
visibility: { eq: "public" },
publishedAt: { lt_expr: "request.time" }
}) {
id title content
}
}query ProContent @auth(expr: "auth.token.plan == 'pro'") {
posts(where: { visibility: { in: ["public", "pro"] }}) {
id title content
}
}# BAD - any user can pass any userId
query GetUserPosts($userId: String!) @auth(level: USER) {
posts(where: { authorUid: { eq: $userId }}) { ... }
}
# GOOD - use auth.uid
query GetMyPosts @auth(level: USER) {
posts(where: { authorUid: { eq_expr: "auth.uid" }}) { ... }
}# BAD - any authenticated user sees all documents
query AllDocs @auth(level: USER) {
documents { id title content }
}
# GOOD - filter to user's documents
query MyDocs @auth(level: USER) {
documents(where: { ownerId: { eq_expr: "auth.uid" }}) { ... }
}# BAD - email not verified
@auth(expr: "auth.token.email.endsWith('@company.com')")
# GOOD - verify email first
@auth(expr: "auth.token.email_verified && auth.token.email.endsWith('@company.com')")During development, set operations to NO_ACCESS until you implement proper
authorization. Use emulator and VS Code extension for testing.