## 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>
194 lines
7.2 KiB
Plaintext
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>
|