Schemas
Roles and permissions

Authorization and access control

⚠️

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). The server will assign that token some number of roles based on the claims present in the token.

Default roles

anonymous

The server will assign the anonymous role to any client that presents the anon token generated in the Triplit dashboard or by the Triplit CLI. You might use this token to allow unauthenticated users to access your database.

authenticated

The server will assign the authenticated role to any client that presents a token that has a sub claim (opens in a new tab). The sub or "subject" claim is a standard JWT claim that identifies the principal user that is the subject of the JWT. Tokens with the sub claim should be issued by an authentication provider such as Clerk or Supabase. Because the sub claim is a unique identifier, we can use it to both attribute data to a user and to restrict access to that data to them.

Example usage

You might use the anonymous role to allow unauthenticated users to read your database, but restrict inserts and updates to authenticated users. The following example allows any user to read the todos collection, but only authenticated users to insert or update todos:

schema.ts
import { Schema as S } from '@triplit/client';
 
export const schema = S.Collections({
  todos: {
    schema: S.Schema({
      id: S.Id(),
      text: S.String(),
      authorId: S.String(),
    }),
    permissions: {
      anonymous: {
        read: {
          filter: [true],
        },
      },
      authenticated: {
        read: {
          filter: [true],
        },
        insert: {
          filter: [['authorId', '=', '$token.sub']],
        },
        update: {
          filter: [['authorId', '=', '$token.sub']],
        },
        delete: {
          filter: [['authorId', '=', '$token.sub']],
        },
      },
    },
  },
});

Custom roles

The default roles exist to make it possible to add authorization rules to your database with minimal configuration. However, your app may require more complicated role-based permission schemes than can't be modeled with only the defaults. In that case you can define your own roles.

Each custom role must have a name 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:

schema.ts
import { Roles } from '@triplit/client';
 
const roles: Roles = {
  admin: {
    match: {
      type: 'admin',
    },
  },
  user: {
    match: {
      type: 'user',
      sub: '$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",
  "sub": "$userId",
}
// Token
{
  "type": "user",
  "sub": 123
}
// Query - resolves to db.query('todos').Where('authorId', '=', 123);
db.query('todos').Where('authorId', '=', '$role.userId');

You do not need to assign a token's sub claim to a $role variable to reference it in a filter. You can access all of the claims on a token directly by using the $token variable prefix. e.g. $token.sub.

Your schema file should export the roles object for use in your schema definitions.

Combining custom and default roles

The default roles will only be applied to tokens when your schema has not defined any custom roles. If you define a custom role, the default roles will not be applied to any tokens. If you want to reuse the default and add your own, you can do so with the DEFAULT_ROLES constant.

schema.ts
import { DEFAULT_ROLES, type Roles, Schema as S } from '@triplit/client';
const roles: Roles = {
  ...DEFAULT_ROLES,
  admin: {
    match: {
      type: 'admin',
    },
  },
  user: {
    match: {
      type: 'user',
      sub: '$userId',
    },
  },
};

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:

schema.ts
import { type Roles, Schema as S } from '@triplit/client';
 
const roles: Roles = {
  // Role definitions
};
 
const schema = S.Collections({
  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:

schema.ts
import { type Roles, Schema as S } from '@triplit/client';
 
const roles: Roles = {
  // Role definitions
};
 
const schema = S.Collections({
  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:

schema.ts
import { type Roles, Schema as S } from '@triplit/client';
 
const roles: Roles = {
  // Custom role definitions
};
 
const schema = S.Collections({
  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:

schema.ts
import { type Roles, Schema as S } from '@triplit/client';
 
const roles = {
  // Custom role definitions
};
 
const schema = S.Collections({
  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:

schema.ts
import { type Roles, Schema as S } from '@triplit/client';
 
const roles: Roles = {
  // Custom role definitions
};
 
const schema = S.Collections({
  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 as S } from '@triplit/client';
 
// schema.ts
const roles: Roles = {
  // Custom role definitions
};
 
const schema = S.Collections({
  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:

schema.ts
import { type Roles, Schema } from '@triplit/client';
 
const roles: Roles = {
  user: {
    match: {
      sub: '$userId',
    },
  },
};
 
const schema = S.Collections({
  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']],
        },
      },
    },
  },
});