34b927ff23
## 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>
166 lines
5.9 KiB
Plaintext
166 lines
5.9 KiB
Plaintext
---
|
|
title: Migrating from Self-Hosted to Cloud
|
|
description: Step-by-step guide to migrate your Twenty self-hosted instance to Twenty Cloud.
|
|
---
|
|
|
|
## Overview
|
|
|
|
This guide walks you through migrating your data from a Twenty self-hosted instance to Twenty Cloud. The process involves setting up your cloud workspace, exporting your data, and re-importing it.
|
|
|
|
<Note>Views, workflows, and roles must be recreated manually after migration. Plan time for this configuration work.</Note>
|
|
|
|
## Step 1: Create Your Cloud Workspace
|
|
|
|
1. Go to [app.twenty.com](https://app.twenty.com) and create a new workspace
|
|
2. Complete the initial setup wizard
|
|
3. Note your new workspace URL
|
|
|
|
## Step 2: Recreate Your Data Model
|
|
|
|
Before importing data, recreate your custom objects and fields:
|
|
|
|
1. Go to **Settings → Data Model** in your cloud instance
|
|
2. Create custom objects that match your self-hosted setup
|
|
3. Add custom fields to standard and custom objects
|
|
4. Configure field settings (unique, required, etc.)
|
|
|
|
<Note>Take screenshots of your self-hosted data model for reference, or keep both instances open side by side.</Note>
|
|
|
|
## Step 3: Invite All Users
|
|
|
|
<Warning>
|
|
**Critical: Invite users BEFORE importing data.**
|
|
|
|
Users must accept their invitations before you import any records that reference them (like Account Owner fields). If users don't exist yet, those relations cannot be mapped.
|
|
</Warning>
|
|
|
|
1. Go to **Settings → Members** in your cloud instance
|
|
2. Invite all team members who had accounts on self-hosted
|
|
3. **Wait for everyone to accept** their invitation
|
|
4. Verify all users appear in your Members list
|
|
|
|
## Step 4: Export Data from Self-Hosted
|
|
|
|
Export each object from your self-hosted instance:
|
|
|
|
1. Navigate to each object (Companies, People, Opportunities, etc.)
|
|
2. Configure the view to show **all columns** you want to migrate
|
|
3. Click **⋮ → Export view**
|
|
4. Save each CSV file with a clear name (e.g., `companies-export.csv`)
|
|
|
|
**Export in this order** (for reference when importing):
|
|
1. Companies
|
|
2. People
|
|
3. Opportunities
|
|
4. Custom objects (following their dependencies)
|
|
5. Tasks, Notes
|
|
|
|
## Step 5: Update Workspace Member References
|
|
|
|
The exported CSVs contain user IDs from your self-hosted instance. These IDs won't match your cloud instance, so you need to replace them with emails.
|
|
|
|
**For each CSV file with user references (Owner, Assignee, etc.):**
|
|
|
|
1. Open the CSV in a spreadsheet application
|
|
2. Add a new column next to each user ID column (e.g., `accountOwnerEmail` next to `accountOwnerId`)
|
|
3. Fill in the **email address** of each user
|
|
4. You can delete the old ID column or leave it (it will be skipped during import)
|
|
|
|
**Example:**
|
|
|
|
Before:
|
|
```csv
|
|
name,domain,accountOwnerId
|
|
Acme Corp,https://acme.com,old-uuid-123
|
|
```
|
|
|
|
After:
|
|
```csv
|
|
name,domain,accountOwnerEmail
|
|
Acme Corp,https://acme.com,john@yourcompany.com
|
|
```
|
|
|
|
<Note>Use the same email addresses that users used to accept their cloud workspace invitation.</Note>
|
|
|
|
## Step 6: Plan Your Import Order
|
|
|
|
Import files in the correct order to maintain relationships:
|
|
|
|
1. **Companies** first (no dependencies)
|
|
2. **People** second (link to Companies)
|
|
3. **Opportunities** third (link to Companies and People)
|
|
4. **Custom objects** (following their dependencies)
|
|
5. **Tasks and Notes** last (link to other records)
|
|
|
|
See [How to Import Relations](/user-guide/data-migration/how-tos/import-relations-between-objects-via-csv) for details on maintaining relationships.
|
|
|
|
## Step 7: Import to Cloud
|
|
|
|
For each CSV file, in order:
|
|
|
|
1. Navigate to the object in your cloud instance
|
|
2. Click **⋮ → Import records**
|
|
3. Upload the CSV file
|
|
4. Map columns to fields:
|
|
- Map user email columns to the appropriate relation fields
|
|
- Map other columns as usual
|
|
5. Review and fix any errors
|
|
6. Confirm the import
|
|
7. Verify a few records before proceeding to the next file
|
|
|
|
## Step 8: Recreate Configuration
|
|
|
|
After importing data, manually recreate:
|
|
|
|
### Views
|
|
- Recreate saved views with filters, sorts, and column configurations
|
|
- Set up any kanban or calendar views
|
|
|
|
### Workflows
|
|
- Recreate automations in **Settings → Workflows**
|
|
- Test each workflow before relying on it
|
|
|
|
### Roles and Permissions
|
|
- Configure roles in **Settings → Members → Roles**
|
|
- Assign users to appropriate roles
|
|
|
|
### Integrations
|
|
- Reconnect email and calendar sync for each user
|
|
- Reconfigure any API integrations with new API keys
|
|
|
|
## Post-Migration Checklist
|
|
|
|
<Check>All data imported successfully</Check>
|
|
<Check>Relations between objects working correctly</Check>
|
|
<Check>User assignments (Owner, Assignee) mapped correctly</Check>
|
|
<Check>Views recreated</Check>
|
|
<Check>Workflows recreated and tested</Check>
|
|
<Check>Roles and permissions configured</Check>
|
|
<Check>Email/calendar sync reconnected</Check>
|
|
<Check>API integrations updated with new keys</Check>
|
|
|
|
## FAQ
|
|
|
|
<AccordionGroup>
|
|
<Accordion title="Can I migrate workflows automatically?">
|
|
Not currently. Workflows must be recreated manually in your cloud instance.
|
|
</Accordion>
|
|
|
|
<Accordion title="What about file attachments and images?">
|
|
File attachments are not included in CSV exports. You'll need to re-upload any attachments manually, migrate them via API or contact our team for assistance with large migrations.
|
|
</Accordion>
|
|
|
|
<Accordion title="Can I keep both instances running during migration?">
|
|
Yes, we recommend keeping your self-hosted instance running until you've verified the cloud migration is complete. Just be careful not to create new data in both places.
|
|
</Accordion>
|
|
|
|
<Accordion title="What if a user hasn't accepted their invitation yet?">
|
|
Records referencing that user will fail to import or the relation will be empty. Ensure all users accept invitations before importing data.
|
|
</Accordion>
|
|
</AccordionGroup>
|
|
|
|
## Need Help?
|
|
|
|
For complex migrations or large datasets, contact us at [contact@twenty.com](mailto:contact@twenty.com) or explore our [Implementation Services](/user-guide/getting-started/capabilities/implementation-services).
|
|
|