Files
twenty/packages/twenty-docs/user-guide/permissions-access/capabilities/permissions.mdx
T
34b927ff23 feat(public-domain): bind public domains to apps + reorganize settings (#20360)
## Summary

- **Public domains can now be bound to a specific app.** When a request
hits an app-bound public domain, route resolution restricts
logic-function matching to that app's HTTP-routed functions only —
isolating each app's routes to its own domain instead of letting routes
from other apps in the workspace match nondeterministically.
- **Settings sidebar reorganized.** Removed the standalone Domains page.
Workspace Domain → General. Approved Domains + Invitations → Members
"Access" tab. Emailing Domains + Public Domains → Apps "Developer" tab.
Roles → Members "Roles" tab.

## Why

The use case: someone building a partner portal app or a lead-collection
app declares private objects (leads, partners…) plus a few public HTTP
routes. Each app needs its own domain (`partners.acme.com`,
`leads.acme.com`) without those domains exposing every other app's
routes in the same workspace. Today's PublicDomainEntity is
workspace-scoped only, so all HTTP-routed logic functions in a workspace
compete for any public domain — first match wins nondeterministically.

## Backend

- Added nullable `applicationId` FK to `PublicDomainEntity`
(cascade-deleted with the app); indexed for the route-trigger lookup.
- New fast instance command
`2-4-instance-command-fast-1798000003000-add-application-id-to-public-domain`
adds the column, index, and FK constraint.
- `createPublicDomain(domain, applicationId)` accepts an optional app
binding; new `updatePublicDomain(domain, applicationId)` mutation
rebinds/unbinds an existing domain. Both validate the application
belongs to the workspace.
- `WorkspaceDomainsService.resolveWorkspaceAndPublicDomain(origin)`
returns both the workspace and the matched public domain in one query —
replacing the old back-to-back lookups in the route-trigger hot path.
`getWorkspaceByOriginOrDefaultWorkspace` is preserved as a thin wrapper.
- `RouteTriggerService` filters `logicFunction` by `applicationId` when
the matched public domain is app-scoped; falls back to workspace-wide
when unbound.
- Three sequential validation queries in `createPublicDomain` now run in
parallel via `Promise.all`.

## Frontend

| Old location | New location |
|---|---|
| Settings sidebar → Domains (standalone page) | Removed |
| Domains page → Workspace Domain | General page |
| Domains page → Approved Domains | Members → Access tab |
| Domains page → Emailing Domains | Apps → Developer tab |
| Domains page → Public Domains | Apps → Developer tab |
| Settings sidebar → Roles (standalone) | Members → Roles tab |
| `pages/settings/roles/` | `pages/settings/members/roles/` |

- The Public Domain detail page has an Application picker that uses
`Select`'s native `emptyOption` + `null` value pattern (matches
`SettingsDataModelObjectIdentifiersForm`).
- Members page tabs use the existing `TabListFromUrlOptionalEffect`
mechanism (rendered automatically by `TabList`) for hash-based tab
activation.
- `/settings/members/roles` redirects to `/settings/members#roles` so
role sub-pages' `navigate(SettingsPath.Roles)` lands on the Members page
with the Roles tab pre-selected.
- All affected breadcrumbs updated to nest under their new parents.
- `SettingsPath.Roles` and friends now nest under `members/`;
`Subdomain` and `CustomDomain` under `general/`; `PublicDomain` and
`EmailingDomain` under `applications/`.

## Test plan

- [x] `nx typecheck twenty-front` passes
- [x] `nx typecheck twenty-server` passes
- [x] `oxlint --type-aware` clean on all touched files
- [x] `prettier --check` clean on all touched files
- [x] Migration applied locally; `publicDomain.applicationId` (uuid,
nullable) confirmed in DB
- [x] GraphQL schema exposes `PublicDomain.applicationId`,
`createPublicDomain.applicationId`, `updatePublicDomain` mutation
- [x] **End-to-end route resolution scenarios verified locally:**
  - Domain bound to App A, function in App A → route matches 
- Domain bound to App B, function in App A → route does NOT match (HTTP
404 `TRIGGER_NOT_FOUND`) 
- Domain unbound (`applicationId = NULL`) → route matches workspace-wide

  - Unknown path on bound domain → returns 404 cleanly 
- [x] UI sanity (browser-tested at `apple.localhost:3001`):
  - General page shows Workspace Domain card
  - Members page shows Team / Access / Roles tabs
  - Access tab combines Invite by link + by email + Approved Domains
  - Roles tab embeds the role list
- `/settings/members/roles` direct URL → redirects + Roles tab
pre-selected
  - Apps Developer tab shows Emailing Domains + Public Domains sections
- Public Domain detail page has Application picker dropdown listing
workspace apps
- Sidebar nav: "Domains" and "Roles" no longer present (now folded into
General/Members)

## Notes for reviewers

- Creating a public domain via the UI still requires Cloudflare
credentials in the dev `.env` (`CLOUDFLARE_API_KEY`,
`CLOUDFLARE_PUBLIC_DOMAIN_ZONE_ID`, `PUBLIC_DOMAIN_URL`). The DNS step
is unchanged from main.
- The `applicationId` column is nullable, so existing public-domain rows
continue to work workspace-wide — no data backfill required.
- `SettingsRolesContainer` was deleted (no longer referenced after
`SettingsRoles` index page was removed).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:17:28 +02:00

194 lines
7.2 KiB
Plaintext

---
title: Permissions
description: Control access to objects, fields, and settings with role-based permissions.
---
Twenty's permission system allows you to control access to three main areas:
- **Objects and Fields**: Control who can view, edit, or delete records and individual fields
- **Settings**: Manage access to workspace configuration and administrative functions
- **Actions**: Control general workspace actions like importing data or sending emails
## Create a Role
To create a new role:
1. Go to **Settings → Members → Roles**
2. Under **All Roles**, click on **+ Create Role**
3. Enter a role name
4. In the default **Permissions** tab, [configure permissions](#customize-permissions)
5. Click **Save** to finish
## Delete a Role
To delete a role:
1. Go to **Settings → Members → Roles**
2. Click on the role you want to remove
3. Open the **Settings** tab, then click **Delete Role**
4. Click **Confirm** in the modal
<Note>
If a role is deleted, any workspace member assigned to it will be automatically reassigned to the default role. All except the **Admin** role can be deleted. There must always be at least one member assigned to the **Admin** role.
</Note>
## Assign Roles to Members
### View Current Assignments
- Go to **Settings → Members → Roles**
- See all roles and how many members are assigned to each
- View which members have which roles
### Assign a Role to a Member
1. Go to **Settings → Members → Roles**
2. Click on the role you want to assign
3. Open the **Assignment** tab
4. Click **+ Assign to member**
5. Select the workspace member from the list
6. Confirm the assignment
### Set Default Role
1. Go to **Settings → Members → Roles**
2. In the **Options** section, find **Default Role**
3. Select which role new members should automatically receive
4. New workspace members will be assigned this role when they join
<Note>
You can only assign roles to existing workspace members. To invite new members, use [Member Management](/user-guide/settings/capabilities/member-management).
</Note>
## Customize Permissions
Permissions determine what each role can access or modify within your workspace, including workspace objects records, settings, and actions.
### Object Permissions
The **Objects** section controls what this role can do with records across your workspace.
#### Set Default Permissions (All Objects)
First, configure the baseline permissions that apply to **all objects** by default:
| Permission | Description |
|------------|-------------|
| **See Records on All Objects** | View records in lists and detail pages |
| **Edit Records on All Objects** | Modify existing records |
| **Delete Records on All Objects** | Soft-delete records (can be restored) |
| **Destroy Records on All Objects** | Permanently delete records |
Select or unselect based on what should be the default behavior for this role.
<Note>
**Example — Intern role**: An intern should be able to see all objects but not edit them by default. Enable "See Records on All Objects" but leave "Edit Records on All Objects" unchecked.
</Note>
#### Add Object-Level Exceptions
After setting defaults, use the **Object-Level** sub-section to add rules that override the defaults for specific objects.
Click **+ Add rule** and select an object to create an exception.
**Example rules for an Intern role:**
| Rule | Effect |
|------|--------|
| Opportunities → disable "See Records" | Intern cannot see the Opportunities object at all |
| People → enable "Edit Records" | Intern can edit People records (but not other objects) |
### Field Permissions
Within each object-level rule, you can go further and configure **field-level permissions** to control access to specific fields.
| Permission | Description |
|------------|-------------|
| **See Field** | View the field value |
| **Edit Field** | Modify the field value |
| **No Access** | Field is completely hidden |
**Example — Restrict sensitive fields:**
For the Intern role with People edit access, you might want to restrict certain fields:
- People → Email → **See Field** only (cannot edit)
- People → Address → **No Access** (completely hidden)
This allows the intern to edit most People fields while protecting sensitive information.
### How Permission Inheritance Works
Permissions cascade from general to specific:
1. **All Objects** → sets the baseline for all objects
2. **Object-Level rules** → override the baseline for specific objects
3. **Field-Level rules** → override the object setting for specific fields
More specific settings always take precedence.
### Managing Permission Overrides
To override inherited permissions:
1. Click **X** to remove the inherited rule
2. Select the specific permissions you want
3. Click the orange **Undo** icon (circular arrow) to revert changes
When done, click **Finish**, then **Save** once redirected to the role page.
### Workspace Settings Permissions
Control access to workspace settings in two ways:
- Toggle **Settings All Access** to grant full access
- Or enable specific permissions (e.g., API key generation, workspace preferences, role assignment, data model configuration, security settings, and workflow management)
<Note>
**Current limitation**: Access to workflow management is currently required to manually trigger workflows. This behavior may change in future releases.
</Note>
### Workspace Action Permissions
Control access to general workspace actions:
- Toggle **Application All Access** to grant full permissions
- Or enable individual actions such as **Send Email**, **Import CSV**, and **Export CSV**
## Assigning Roles to API Keys and AI Agents
Beyond workspace members, roles can also be assigned to **API Keys** and **AI Agents**. This is particularly helpful for teams who want to control exactly "who" can do what in their workspace—including automated processes and integrations.
### Why Assign Roles to API Keys and AI Agents?
- **Security**: Limit what automated processes can access or modify
- **Compliance**: Ensure integrations only touch the data they need
- **Control**: Prevent accidental data changes from misconfigured automations
- **Auditability**: Track which actions were performed by which integration or agent
### Assign a Role to an API Key
1. Go to **Settings → Members → Roles**
2. Click on the role you want to assign
3. Open the **Assignment** tab
4. Under **API Keys**, click **+ Assign to API key**
5. Select the API key from the list
6. Confirm the assignment
The API key will now inherit all permissions defined by that role. Any API calls made with this key will be restricted accordingly.
<Note>
API keys without an assigned role use default permissions. For tighter security, always assign a specific role to production API keys.
</Note>
### Assign a Role to an AI Agent
1. Go to **Settings → Members → Roles**
2. Click on the role you want to assign
3. Open the **Assignment** tab
4. Under **AI Agents**, click **+ Assign to AI agent**
5. Select the AI agent from the list
6. Confirm the assignment
The AI agent will only be able to access data and perform actions allowed by its assigned role.
<Note>
For AI agents running within workflows, this ensures the agent cannot access or modify data outside its intended scope—even if the workflow has broader permissions.
</Note>