Permission
TailorDB's permission system provides enhanced flexibility and performance for controlling access to your data. It introduces two key resources that work together to provide comprehensive access control:
Permission- Record-level access controlGQLPermission- GraphQL operation-level access control
The new permission system is recommended for all new applications. It addresses several limitations of the legacy permission system (deprecated) including better performance, support for non-UUID fields, and the ability to reference record values directly in permission rules.
Permission (Record-Level Control)
Permission is defined within the Type resource and controls which users can operate on which records.
Basic Structure
db.type("Example", {
// field definitions
}).permission({
create: [
/* policies */
],
read: [
/* policies */
],
update: [
/* policies */
],
delete: [
/* policies */
],
});Permission Types
Each permission type has different semantics:
Read Permission
Read Permission act as automatic filters. Only records that match at least one policy can be retrieved.
db.type("Example", {
userId: db.uuid(),
}).permission({
read: [[{ record: "userId" }, "=", { user: "id" }]],
});Create/Update/Delete Permissions
These Permissions act as validation rules. If a record doesn't match any policy, the operation is prohibited and returns a permission denied error.
db.type("Example", {
// fields
}).permission({
create: [[{ user: "role" }, "=", "ADMIN"]],
});Policy Evaluation
Multiple policies can be defined for each permission type. The evaluation follows these rules:
- Explicit allow required: If no policy matches, access is denied by default (implicit deny)
- Explicit deny takes precedence: A policy with
permit = "deny"always overrides allow policies - All conditions must match: Within a policy, all conditions must be satisfied for the policy to match
Operands
The following operands can be used in conditions:
record
Uses the value of a specified field from the record. Cannot be used in Update Permission (use old_record or new_record instead).
Supported field types: String, UUID, Enum, Boolean, and their array forms.
// Check if the record's status is "TODO"
[{ record: "status" }, "=", "TODO"];old_record / new_record
Used in Update Permission to reference the existing or updated record values. Cannot be used in Create/Read/Delete Permissions.
The supported field types are the same as for record.
// Check if the old record's assigneeId matches the user ID
[{ oldRecord: "assigneeId" }, "=", { user: "id" }];user
Uses the value of a specified field from the user's AttributeMap (defined in the Auth service).
// Check if the user's role is "ADMIN"
[{ user: "role" }, "=", "ADMIN"];value
Uses a specified value directly. Supports String, Boolean, and their array types.
// Check if the record's status is in a specific set of values
[{ record: "status" }, "in", ["TODO", "IN_PROGRESS"]];Operators
The following operators are supported:
eq / ne
Equality and inequality comparison.
// Check if the record's status is "TODO"
[{ record: "status" }, "=", "TODO"][
// Check if the user's role is not "ADMIN"
({ user: "role" }, "!=", "ADMIN")
];in / nin
Array membership and non-membership.
// Check if the record's status is in a set of values
[{ record: "status" }, "in", ["TODO", "IN_PROGRESS"]][
// Check if the user's role is not in a set of values
({ user: "role" }, "nin", ["GUEST", "USER"])
];hasAny / nhasAny
Array overlap and non-overlap. Checks whether two string arrays share any common elements. Both operands must be string arrays.
hasAny— true if the two arrays have at least one element in commonnhasAny— true if the two arrays have no elements in common
Supported array field types: String, UUID, Enum.
// Check if the record's roles share any values with the given list
[{ record: "roles" }, "hasAny", ["ADMIN", "EDITOR"]][
// Check if the user's roles have no overlap with restricted roles
({ user: "roles" }, "nhasAny", ["BLOCKED", "SUSPENDED"])
];You can also compare a user attribute array against a record field array:
// Allow access if the user's roles overlap with the record's allowedRoles
[{ user: "roles" }, "hasAny", { record: "allowedRoles" }];Complete Example
const TaskStatus = db.enum("TaskStatus", [
{ value: "TODO", description: "Task is pending" },
{ value: "IN_PROGRESS", description: "Task is currently being worked on" },
{ value: "DONE", description: "Task has been completed" },
]);
db.type("Task", {
title: db.string({ required: true, description: "Task title" }),
status: TaskStatus({
description: "Task status",
hooks: { create: "'TODO'" },
}),
assigneeId: db.uuid({
description: "ID of the user assigned to this task",
hooks: { create: "user.id" },
}),
}).permission({
create: [
// Administrators can create any task
[{ user: "role" }, "=", "ADMIN"],
// Users can create tasks assigned to themselves with TODO status
[
[{ record: "assigneeId" }, "=", { user: "id" }],
[{ record: "status" }, "=", "TODO"],
],
],
read: [
// Administrators can read all tasks
[{ user: "role" }, "=", "ADMIN"],
// Users can read tasks assigned to them
[{ record: "assigneeId" }, "=", { user: "id" }],
],
update: [
// Administrators can update any task
[{ user: "role" }, "=", "ADMIN"],
// Users can update tasks assigned to them
[
[{ oldRecord: "assigneeId" }, "=", { user: "id" }],
[{ newRecord: "assigneeId" }, "=", { user: "id" }],
],
],
delete: [
// Administrators can delete any task
[{ user: "role" }, "=", "ADMIN"],
],
});GQLPermission (GraphQL-Level Control)
GQLPermission is defined as a separate resource and controls which users can execute specific GraphQL operations. This setting does not affect SQL execution via the Function service.
Basic Structure
db.gqlPermission("Example", {
policies: [
{
conditions: [
/* conditions */
],
actions: [
/* actions */
],
permit: "allow", // or "deny"
description: "Policy description",
},
],
});Conditions
The method for defining Conditions is basically the same as Permission. Just note that record / old_record / new_record operands are not available here.
Actions
Each GraphQL operation is categorized into the following actions:
| Action | GraphQL operation |
|---|---|
all | All GraphQL operations for the type |
create | create<Type> mutation |
read | get<Type>, get<Type>By, list<Type>s queries |
update | update<Type> mutation |
delete | delete<Type> mutation |
aggregate | aggregate<Type> query |
bulk_upsert | bulkUpsert<Type>, bulkUpsert<Type>By mutations |
Complete Example
db.gqlPermission("Task", {
policies: [
{
conditions: [[{ user: "role" }, "=", "ADMIN"]],
actions: ["all"],
permit: "allow",
description: "Administrators have full access to all GraphQL operations",
},
{
conditions: [[{ user: "loggedIn" }, "=", true]],
actions: ["create", "read", "update"],
permit: "allow",
description: "Authenticated users can create, read, and update tasks",
},
],
});Auth Integration
User attributes referenced in permissions are defined through the Auth service configuration.
User Profile Configuration
auth.idp.tailordb({
type: "User",
usernameField: "email",
attributeMap: {
// Reference the value of the role field as "role" using the "user" operand
role: "role",
},
});Machine User Configuration
auth.machineUser("admin", {
// Set the role attribute to "ADMIN"
role: "ADMIN",
});Built-in User Attributes
In addition to custom attributes, two built-in fields are always available:
_id
The user's unique identifier.
// Check if the user ID matches the record's assigneeId
[{ user: "id" }, "=", { record: "assigneeId" }];_loggedIn
Boolean indicating whether the user is authenticated.
// Check if the user is logged in
[{ user: "loggedIn" }, "=", true];SQL Operation Behavior
Unlike GQLPermission, Permission is enforced at the SQL level as well.
For detailed information about how the settings affect SQL operations when accessing TailorDB via the Function service, see Permission Enforcement.
GraphQL Operation Behavior
When accessing TailorDB via GraphQL, Permission required for each operation is determined based on the corresponding equivalent SQL:
Create Operations
mutation {
createTask(input: { title: "New task" }) {
id
}
}SQL Equivalent: INSERT INTO "Task" ("title") VALUES ('New task') RETURNING "id"
Required Permissions:
- Permission: Create, Read (for returning created record)
- GQLPermission: Create
Read Operations
query {
tasks(query: { status: { eq: "TODO" } }, first: 10) {
edges {
node {
id
title
}
}
}
}SQL Equivalent: SELECT "id", "title" FROM "Task" WHERE "status" = 'TODO' LIMIT 10
Required Permissions:
- Permission: Read
- GQLPermission: Read
Update Operations
mutation {
updateTask(
id: "<uuid>"
input: { status: "DONE" }
condition: { status: { eq: "IN_PROGRESS" } }
) {
id
}
}SQL Equivalent: UPDATE "Task" SET "status" = 'DONE' WHERE "id" = 'uuid' AND "status" = 'IN_PROGRESS' RETURNING "id"
Required Permissions:
- Permission: Update, Read (for candidate record retrieval and returning updated record)
- GQLPermission: Update
Delete Operations
mutation {
deleteTask(id: "<uuid>")
}SQL Equivalent: DELETE FROM "Task" WHERE "id" = 'uuid'
Required Permissions:
- Permission: Delete, Read (for candidate record retrieval)
- GQLPermission: Delete
Bulk Upsert Operations
mutation {
bulkUpsertTasksBy(
field: title
input: [{ title: "Task 1", status: "TODO" }, { title: "Task 2", status: "DONE" }]
)
}SQL Equivalent: INSERT INTO "Task" ("title", "status") VALUES ('Task 1', 'TODO'), ('Task 2', 'DONE') ON CONFLICT ("title") DO UPDATE SET "status" = EXCLUDED."status"
Required Permissions:
- Permission: Create (INSERT case), Update + Read (UPDATE case)
- GQLPermission: BulkUpsert
Aggregate Operations
query {
aggregateTasks {
groupBy {
status
}
count
}
}SQL Equivalent: SELECT "status", COUNT(*) FROM "Task" GROUP BY "status"
Required Permissions:
- Permission: Read
- GQLPermission: Aggregate
Compatibility
- When both
PermissionandRecordPermissionare defined,Permissiontakes precedence - When both
GQLPermissionandTypePermissionare defined,GQLPermissiontakes precedence - This ensures backward compatibility while enabling gradual migration