Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s
224 lines
9.6 KiB
Plaintext
224 lines
9.6 KiB
Plaintext
---
|
|
title: Roles & Permissions
|
|
description: Declare what objects and fields your app's logic functions and front components can read and write.
|
|
icon: 'shield-halved'
|
|
---
|
|
|
|
A **role** is a permission set: which objects an app can read or write, which fields it can see, and which platform-level capabilities it can use. Every app's logic functions and front components inherit the permissions of the role marked with `defineApplicationRole()` (see [The default function role](#the-default-function-role) below).
|
|
|
|
```ts src/roles/restricted-company-role.ts
|
|
import {
|
|
defineRole,
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
|
SystemPermissionFlag,
|
|
} from 'twenty-sdk/define';
|
|
|
|
export default defineRole({
|
|
universalIdentifier: '2c80f640-2083-4803-bb49-003e38279de6',
|
|
label: 'My new role',
|
|
description: 'A role that can be used in your workspace',
|
|
canReadAllObjectRecords: false,
|
|
canUpdateAllObjectRecords: false,
|
|
canSoftDeleteAllObjectRecords: false,
|
|
canDestroyAllObjectRecords: false,
|
|
canUpdateAllSettings: false,
|
|
canBeAssignedToAgents: false,
|
|
canBeAssignedToUsers: false,
|
|
canBeAssignedToApiKeys: false,
|
|
objectPermissions: [
|
|
{
|
|
objectUniversalIdentifier:
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
|
canReadObjectRecords: true,
|
|
canUpdateObjectRecords: true,
|
|
canSoftDeleteObjectRecords: false,
|
|
canDestroyObjectRecords: false,
|
|
},
|
|
],
|
|
fieldPermissions: [
|
|
{
|
|
objectUniversalIdentifier:
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
|
fieldUniversalIdentifier:
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.fields.name
|
|
.universalIdentifier,
|
|
canReadFieldValue: false,
|
|
canUpdateFieldValue: false,
|
|
},
|
|
],
|
|
permissionFlagUniversalIdentifiers: [SystemPermissionFlag.APPLICATIONS],
|
|
});
|
|
```
|
|
|
|
## Row-level security
|
|
|
|
Row-level permissions require the **Organization plan or above** — see
|
|
[pricing](https://twenty.com/pricing). What happens on a plan without the entitlement is covered in
|
|
the notes at the end of this section.
|
|
|
|
Object and field permissions decide _which objects and fields_ a role can touch. **Row-level
|
|
permission predicates** go further and decide _which records_ a role can see and act on — for
|
|
example, a self-service role where each external user sees only their own records.
|
|
|
|
Declare predicates with `rowLevelPermissionPredicates` on the role. Like the rest of the manifest,
|
|
each predicate carries its own `universalIdentifier`, and references an object and a field by their
|
|
`universalIdentifier`, an `operand`, and (optionally) a workspaceMember field whose value is injected
|
|
at query time — so you can express "the record's owner relation **is** the current workspace member":
|
|
|
|
```ts src/roles/partner-role.ts
|
|
import {
|
|
defineRole,
|
|
RowLevelPermissionPredicateOperand,
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
|
} from 'twenty-sdk/define';
|
|
|
|
import { ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER } from '../fields/account-owner.field';
|
|
|
|
export default defineRole({
|
|
universalIdentifier: 'c3c1dc2e-1a08-4de5-abb7-2139b3d99343',
|
|
label: 'Partner',
|
|
description: 'External partner — sees only its own records',
|
|
canBeAssignedToUsers: true,
|
|
objectPermissions: [
|
|
{
|
|
objectUniversalIdentifier:
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
|
canReadObjectRecords: true,
|
|
canUpdateObjectRecords: true,
|
|
},
|
|
],
|
|
rowLevelPermissionPredicates: [
|
|
{
|
|
universalIdentifier: 'd0f0c1a2-3b4c-4d5e-8f60-111111111111',
|
|
objectUniversalIdentifier:
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.company.universalIdentifier,
|
|
fieldUniversalIdentifier: ACCOUNT_OWNER_FIELD_UNIVERSAL_IDENTIFIER,
|
|
operand: RowLevelPermissionPredicateOperand.IS,
|
|
workspaceMemberFieldUniversalIdentifier:
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
|
|
.universalIdentifier,
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
Because predicates ship in the manifest, they are created, updated, and removed together with the
|
|
role on every install and upgrade — there is no separate post-install step to keep in sync.
|
|
|
|
### Combining predicates with groups
|
|
|
|
By default a role's predicates are combined with `AND`. To combine some of them with `OR` (or to
|
|
nest logic), declare a `rowLevelPermissionPredicateGroups` entry and point each predicate at it via
|
|
`predicateGroupUniversalIdentifier`. This role lets a partner see an Opportunity it either **owns**
|
|
or is the **point of contact** for:
|
|
|
|
```ts src/roles/partner-opportunities-role.ts
|
|
import {
|
|
defineRole,
|
|
RowLevelPermissionPredicateGroupLogicalOperator,
|
|
RowLevelPermissionPredicateOperand,
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS,
|
|
} from 'twenty-sdk/define';
|
|
|
|
const OPPORTUNITY = STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.opportunity;
|
|
const CURRENT_MEMBER =
|
|
STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember.fields.id
|
|
.universalIdentifier;
|
|
|
|
export default defineRole({
|
|
universalIdentifier: 'b2a1c0d9-8e7f-4a6b-9c5d-222222222222',
|
|
label: 'Partner (opportunities)',
|
|
canBeAssignedToUsers: true,
|
|
objectPermissions: [
|
|
{
|
|
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
|
|
canReadObjectRecords: true,
|
|
},
|
|
],
|
|
rowLevelPermissionPredicateGroups: [
|
|
{
|
|
universalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
|
|
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
|
|
logicalOperator: RowLevelPermissionPredicateGroupLogicalOperator.OR,
|
|
},
|
|
],
|
|
rowLevelPermissionPredicates: [
|
|
{
|
|
universalIdentifier: 'd4c3b2a1-0e9f-4c8b-9e7d-444444444444',
|
|
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
|
|
fieldUniversalIdentifier: OPPORTUNITY.fields.owner.universalIdentifier,
|
|
operand: RowLevelPermissionPredicateOperand.IS,
|
|
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
|
|
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
|
|
},
|
|
{
|
|
universalIdentifier: 'e5d4c3b2-1f0e-4d9c-8f8e-555555555555',
|
|
objectUniversalIdentifier: OPPORTUNITY.universalIdentifier,
|
|
fieldUniversalIdentifier:
|
|
OPPORTUNITY.fields.pointOfContact.universalIdentifier,
|
|
operand: RowLevelPermissionPredicateOperand.IS,
|
|
workspaceMemberFieldUniversalIdentifier: CURRENT_MEMBER,
|
|
predicateGroupUniversalIdentifier: 'c3b2a1d0-9f8e-4b7a-8d6c-333333333333',
|
|
},
|
|
],
|
|
});
|
|
```
|
|
|
|
Notes:
|
|
|
|
- Give every predicate and group a stable `universalIdentifier` (any uuid) — it keys the entity
|
|
across upgrades, and predicates reference groups by it.
|
|
- Predicates can reference objects and fields owned by your app or by Twenty's standard objects.
|
|
- Row-level security is enforced for workspaces on the **Organization plan or above**. On other
|
|
plans the predicates declared in an app manifest still sync, they are simply not enforced.
|
|
- The Metadata API behaves differently from the manifest here: without the entitlement,
|
|
`upsertRowLevelPermissionPredicates` is rejected with
|
|
`ROW_LEVEL_PERMISSION_FEATURE_DISABLED` rather than syncing. A workspace on a plan that does not
|
|
include row-level security therefore cannot create predicates outside of an app manifest, even
|
|
though the `rowLevelPermissionPredicates` field is present on the `Role` type when you introspect
|
|
the schema.
|
|
|
|
## The default function role
|
|
|
|
When you scaffold a new app, the CLI creates a default role file declared with `defineApplicationRole()`:
|
|
|
|
```ts src/roles/default-role.ts
|
|
import { defineApplicationRole } from 'twenty-sdk/define';
|
|
|
|
export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER =
|
|
'b648f87b-1d26-4961-b974-0908fd991061';
|
|
|
|
export default defineApplicationRole({
|
|
universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
|
label: 'Default function role',
|
|
description: 'Default role for function Twenty client',
|
|
canReadAllObjectRecords: true,
|
|
canUpdateAllObjectRecords: false,
|
|
canSoftDeleteAllObjectRecords: false,
|
|
canDestroyAllObjectRecords: false,
|
|
canUpdateAllSettings: false,
|
|
canBeAssignedToAgents: false,
|
|
canBeAssignedToUsers: false,
|
|
canBeAssignedToApiKeys: false,
|
|
objectPermissions: [],
|
|
fieldPermissions: [],
|
|
permissionFlagUniversalIdentifiers: [],
|
|
});
|
|
```
|
|
|
|
`defineApplicationRole()` is a thin wrapper around `defineRole()` that flags **the** role used as your application's default at install time. Validation is identical to `defineRole`, but the build pipeline auto-wires its `universalIdentifier` into the application manifest's `defaultRoleUniversalIdentifier` — so you do not need to reference it from [`defineApplication`](/developers/extend/apps/config/application) yourself.
|
|
|
|
Notes:
|
|
|
|
- Exactly **one** `defineApplicationRole(...)` is allowed per app — the manifest build will fail if it finds more than one.
|
|
- Use `defineRole()` (not `defineApplicationRole()`) for any **additional** roles your app ships.
|
|
- Setting `defaultRoleUniversalIdentifier` explicitly on `defineApplication()` is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`.
|
|
|
|
## Best practices
|
|
|
|
- Start from the scaffolded role, then progressively restrict it — the default grants broad read access, which is rarely what you want in production.
|
|
- Replace `objectPermissions` and `fieldPermissions` with the exact objects and fields your functions actually need.
|
|
- `permissionFlagUniversalIdentifiers` control access to platform-level capabilities. Keep them minimal.
|
|
- A permission flag your app declares with `definePermissionFlag()` carries a `permissionType`: `'tool'` (the default) for a capability an agent or a user can be granted, `'settings'` for a flag that unlocks a settings area.
|
|
- See a working example: [`examples/hello-world/src/roles/default-role.ts`](https://github.com/twentyhq/twenty/blob/main/packages/twenty-apps/examples/hello-world/src/roles/default-role.ts).
|