Authorization and access control
This feature is available for all hosted Triplit Cloud users. If you are self-hosting Triplit, this feature is available in Triplit 0.3.58 and higher.
Access control checks run exclusively on the server, and are not enforced on the client. Invalid writes will only be rejected when they have been sent to the server.
Triplit provides a flexible way to define access control rules for your database, ensuring that your application data is secure without the need for complex server-side logic.
Roles
When a client authenticates with a Triplit server and begins a session, it provides a token that contains some information about itself (see authentication for more information on tokens). You can define roles
to access information from a token to inform permissions decisions, like 'Can this user read this todo?'.
Each role has an identifier and a match
object. When a client authenticates with a Triplit server, Triplit will check if the token matches any defined roles in the schema. If it does, the client is granted that role and will be subject to any permissions that have been defined for that it.
For example, you may author admin
and user
tokens with the following structure:
import { Roles } from '@triplit/client';
const roles: Roles = {
admin: {
match: {
type: 'admin',
},
},
user: {
match: {
type: 'user',
uid: '$userId',
},
},
};
Wildcards in the match
object (prefixed with $
) will be assigned to variables with the prefix $role
. For example, a JWT with the following structure would match the user
role and assign the value 123
to the $role.userId
variable for use in your application's permission definitions:
// match object
{
"type": "user",
"uid": "$userId",
}
// Token
{
"type": "user",
"uid": 123
}
// Query - resolves to db.query('todos').where('authorId', '=', 123);
db.query('todos').where('authorId', '=', '$role.userId');
Your schema file should export the roles
object for use in your schema definitions.
Permissions
Access control at the attribute level is not yet supported, but will be in a future release.
By default, there are no access controls on the database and they must be configured by adding a permissions
definition to the schema. Each collection in a schema can have a permissions
object that defines the access control rules for that collection. Once a permissions object is defined, Triplit will enforce the provided rules for each operation on the collection. If no rules for an operation are provided, the operation not be allowed by default.
The following example turns off all access to the todos
collection so it is only accessible with your service
token:
const roles = {
// Role definitions
};
const schema = {
todos: {
schema: S.Schema({
id: S.Id(),
text: S.String(),
authorId: S.String(),
}),
permissions: {},
},
};
export { schema, roles };
Collection permissions are defined for each operation and role. If a role is not included, it will not be allowed to perform that operation. When performing each operation, Triplit will check the set of set of filter clauses that must be satisfied for the operation to be allowed.
{
"role": {
"operation": {
"filter": // Boolean filter expression
}
}
}
Read
To allow clients to read data, you must define a read
permission that specifies the roles that may read data and any additional restrictions. The following example allows a user
to read the todos that they authored and an admin
to read any todo:
import { type Roles, Schema as S } from '@triplit/client';
const roles: Roles = {
// Role definitions
};
const schema = {
todos: {
schema: S.Schema({
id: S.Id(),
text: S.String(),
authorId: S.String(),
}),
permissions: {
admin: {
read: {
// Allow all reads
filter: [true],
},
},
user: {
read: {
// Allow reads where authorId is the user's id
filter: [['authorId', '=', '$role.userId']],
},
},
},
},
};
export { schema, roles };
Insert
To allow clients to insert data, you must define an insert
permission that specifies the roles that may insert data and any additional restrictions. The following example allows a user
to insert a todo that they author and an admin
to insert any todo:
import { type Roles, Schema as S } from '@triplit/client';
const roles: Roles = {
// Role definitions
};
const schema = {
todos: {
schema: S.Schema({
id: S.Id(),
text: S.String(),
authorId: S.String(),
}),
permissions: {
admin: {
insert: {
// Allow all inserts
filter: [true],
},
},
user: {
insert: {
// Allow inserts where authorId is the user's id
filter: [['authorId', '=', '$role.userId']],
},
},
},
},
};
export { schema, roles };
Update
To allow users to update data, you must define an update
permission that specifies the roles that may update data and any additional restrictions. For updates, the permission is checked against the "old" state of the entity, before it has been updated. The following example allows a user
to update todos that they authored and an admin
to update any todo:
import { type Roles, Schema as S } from '@triplit/client';
const roles = {
// Role definitions
};
const schema = {
todos: {
schema: S.Schema({
id: S.Id(),
text: S.String(),
authorId: S.String(),
}),
permissions: {
admin: {
update: {
// Allow all updates
filter: [true],
},
},
user: {
update: {
// Allow updates where authorId is the user's id
filter: [['authorId', '=', '$role.userId']],
},
},
},
},
};
export { schema, roles };
Post update
You may also optionally define a postUpdate
permission that will be run after an update operation has been completed. This is useful for confirming that updated data is valid. For example, this checks that a user
has not re-assigned a todo to another user
:
import { type Roles, Schema } from '@triplit/client';
const roles: Roles = {
// Role definitions
};
const schema = {
todos: {
schema: S.Schema({
id: S.Id(),
text: S.String(),
authorId: S.String(),
}),
permissions: {
user: {
update: {
// Allow updates where authorId is the user's id
filter: [['authorId', '=', '$role.userId']],
},
postUpdate: {
// Check that the authorId has not changed
filter: [['authorId', '=', '$role.userId']],
},
},
},
},
};
export { schema, roles };
Delete
To allow users to delete data, you must define a delete
permission that specifies the roles that may delete data and any additional restrictions. The following example allows a user
to delete todos that they authored and an admin
to delete any todo:
import { type Roles, Schema } from '@triplit/client';
// schema.ts
const roles: Roles = {
// Role definitions
};
const schema = {
todos: {
schema: S.Schema({
id: S.Id(),
text: S.String(),
authorId: S.String(),
}),
permissions: {
admin: {
delete: {
// Allow all deletes
filter: [true],
},
},
user: {
delete: {
// Allow deletes where authorId is the user's id
filter: [['authorId', '=', '$role.userId']],
},
},
},
},
};
export { schema, roles };
Editing permissions
Permissions are a part of your schema and can be added or updated by modifying your schema. In a future release, you will be able to manage permissions in your project's Dashboard (opens in a new tab).
Modeling permissions with external authentication
When using an external authentication provider like Clerk, the provider is the source of truth for identifying users. This means that in your Triplit database you might not need a traditional users collection. Permissions that restrict access to specific authenticated users should use the ids provided by the auth service. If you want to store additional information about a user in Triplit, we recommend using a profiles
collection that uses the same ID as the user ID provided from your auth provider. When your app loads and a user authenticates, you can fetch their profile or create it if it doesn't exist. Here’s an example schema:
import { type Roles, Schema } from '@triplit/client';
const roles: Roles = {
user: {
match: {
sub: '$userId',
},
},
};
const schema = {
profiles: {
schema: S.Schema({
id: S.Id(), // Use the user ID from your auth provider when inserting
nickname: S.String(),
created_at: S.Date({ default: S.Default.now() }),
}),
permissions: {
user: {
read: {
filter: [['id', '=', '$role.userId']],
},
update: {
filter: [['id', '=', '$role.userId']],
},
insert: {
filter: [['id', '=', '$role.userId']],
},
},
},
},
todos: {
schema: S.Schema({
id: S.Id(),
text: S.String(),
authorId: S.String(),
}),
permissions: {
user: {
read: {
filter: [['authorId', '=', '$role.userId']],
},
insert: {
filter: [['authorId', '=', '$role.userId']],
},
update: {
filter: [['authorId', '=', '$role.userId']],
},
delete: {
filter: [['authorId', '=', '$role.userId']],
},
},
},
},
};