Skip to main content
Front components are React components that render directly inside Twenty’s UI. They run in an isolated Web Worker using Remote DOM — your code executes inside a sandboxed, opaque-origin iframe, yet its UI still renders natively in the page rather than being confined to that iframe.
Front components are still under active development. Your code runs against a partial DOM, not a real browser page, so advanced usages can fail, often silently. See Current limitations.

Where front components can be used

Front components can render in four locations within Twenty:
  • Side panel — Non-headless front components open in the right-hand side panel. This is the default behavior when a front component is triggered from the command menu.
  • Widgets (dashboards and record pages) — Front components can be embedded as widgets inside page layouts. When configuring a dashboard or a record page layout, users can add a front component widget.
  • App settings — Pointed at by a defineSettingsMenuItem() declaration, the front component renders the page behind one of the app’s settings menu items. An app can declare several.
  • AI chat — A logic function exposed as an AI tool can name a front component, which then renders that tool’s calls inline in the chat in place of the default step row.
A front component on its own isn’t reachable from the UI — you need to surface it. The four ways to do that are:

Basic example

The quickest way to see a front component in action is to pair it with a defineCommandMenuItem, so it appears as a quick-action button in the top-right corner of the page:
src/front-components/hello-world.tsx
src/command-menu-items/hello-world.command-menu-item.ts
After syncing with yarn twenty dev (or running a one-shot yarn twenty apply), the quick action appears in the top-right corner of the page:
Quick action button in the top-right corner
Click it to render the component inline.

Configuration fields

Placing a front component on a page

Beyond commands, you can embed a front component directly into a record page by adding it as a widget in a page layout. See Page Layouts for details. widgetAvailability decides which widget picker offers the component while a page layout is being edited. A front component is built either for record pages, where it works on the shown record, or for dashboards, so it declares one pageLayoutType:
Leave widgetAvailability out for a component that is only opened from a command, a settings menu item or a timeline row. It never limits where a component is displayed: commands, settings menu items and the page layouts your app declares render it whatever its value. The build warns about a visible component that declares none and is opened from nowhere else.

Settings menu items

An app can add its own entries to the settings menu, each rendering a front component. Declare one with defineSettingsMenuItem() in its own file, pointing at the component that renders it:
src/settings-menu-items/sync-settings.ts
The menu item points at the front component rather than the component declaring itself a settings entry. One component can therefore back several menu items, and an item can be repointed at a different component without touching either. Each item renders as a tab on the app’s page under Settings > Apps, after General, in position order. Declare as many as you need. Twenty’s own General item — auto-upgrade, App URL and connections — always comes first and cannot be overridden by an app; App URL only shows when the app has HTTP-triggered functions.

Fields

position is a decimal, so an item can be slotted between two existing ones without renumbering. Items sort by ascending position, ties broken on universalIdentifier so the order is the same in every workspace. Two items of one app cannot share a position in the same scope — the sync refuses it rather than letting install order decide which renders first.

Workspace and user scope

scope says who an item is configured by:
  • WORKSPACE (default) — configured once for the whole workspace, alongside the other workspace settings. These are the items that render as tabs on the app’s page.
  • USER — configured by each member for themselves, under their own user settings.
A USER item renders as a tab on the app’s page under Settings > App preferences, where only the signed-in member sees it, and only when their role grants the CONNECTED_ACCOUNTS permission. That page is not enabled in every workspace yet: where it is not, a USER item is persisted and synced but nothing renders it.
A USER item runs as the member viewing it. To store something per person, declare the variable with scope: 'USER': getApplicationVariable returns the viewing member’s value, and your component saves it with updateMyUserApplicationVariable, which writes that member’s value only and needs SystemPermissionFlag.CONNECTED_ACCOUNTS in your app’s role, if it declares one (see Reading and updating variables at runtime). Like a WORKSPACE item, a USER item replaces the generated Variables form, so it should let the member set every user variable your app needs. A workspace variable holds one value shared by everyone, so one member’s save would overwrite another’s.
defineSettingsFrontComponent() is deprecated in favor of defineSettingsMenuItem(). An app still using it keeps working — its component is treated as a single Settings menu item — but it can only ever declare one, which is what defineSettingsMenuItem() exists to lift.

Headless vs non-headless

Front components come in two rendering modes controlled by the isHeadless option: Non-headless (default) — The component renders a visible UI. When triggered from the command menu it opens in the side panel. This is the default behavior when isHeadless is false or omitted. Headless (isHeadless: true) — The component mounts invisibly in the background. It does not open the side panel. Headless components are designed for actions that execute logic and then unmount themselves — for example, running an async task, navigating to a page, or showing a confirmation modal. They pair naturally with the SDK Command components described below.
src/front-components/sync-tracker.tsx
Because the component returns null, Twenty skips rendering a container for it — no empty space appears in the layout. The component still has access to all hooks and the host communication API.

SDK Command components

The twenty-sdk package provides four Command helper components designed for headless front components. Each component executes an action on mount, handles errors by showing a snackbar notification, and automatically unmounts the front component when done. Import them from twenty-sdk/front-component:
  • Command — Runs an async callback via the execute prop.
  • CommandLink — Navigates to an app path. Props: to, params, queryParams, options.
  • CommandModal — Opens a confirmation modal. If the user confirms, executes the execute callback. Props: title, subtitle, execute, confirmButtonText, confirmButtonAccent.
  • CommandOpenSidePanelPage — Opens content in the side panel. Routed pages use the same typed to, params, and queryParams props as CommandLink, plus optional hash, pageTitle, and resetNavigationStack props. The workspace route registry decides whether the requested AppPath supports the side panel. pageTitle overrides the panel header title, while resetNavigationStack clears earlier panel pages before opening the route. Purpose-built panel tools use their own page-specific props.
Here is a full example of a headless front component using Command to run an action from the command menu:
src/front-components/run-action.tsx
src/command-menu-items/run-action.command-menu-item.ts
And an example using CommandModal to ask for confirmation before executing:
src/front-components/delete-draft.tsx
And an example using CommandOpenSidePanelPage to open the current record in the side panel on a specific tab. hash is a page layout tab id without the leading # (default layouts use ids like company-tab-emails or company-tab-timeline; custom layouts use the tab’s own id). If the id doesn’t exist in the record’s layout, the default tab opens instead:
src/front-components/open-company-emails.tsx

Calling a logic function

Front components run browser-side in a Web Worker sandboxed inside an opaque-origin iframe, while logic functions run server-side. There is no direct in-process call between the two — instead, a front component reaches a logic function over HTTP. A logic function declared with httpRouteTriggerSettings is reachable over HTTP at its route path. RestApiClient treats paths starting with /s/ as app routes, resolves them to the URL your functions are served from, and authenticates them with TWENTY_APP_ACCESS_TOKEN.
On Twenty Cloud, HTTP-triggered logic functions are served on a dedicated per-workspace domain at https://<your-workspace-subdomain>.withtwenty.com<path>. For external callers, copy the exact URL from the function’s HTTP trigger settings or the application’s Settings tab.
A headless front component can run the call on mount via the Command component, then unmount automatically:
src/front-components/sync-prs.tsx
The path passed to RestApiClient is the logic function’s httpRouteTriggerSettings.path, prefixed with /s. Keep isAuthRequired: true; the TWENTY_APP_ACCESS_TOKEN Twenty mints for your component authenticates the request:
src/logic-functions/fetch-prs.logic-function.ts
TWENTY_APP_ACCESS_TOKEN is injected automatically — see Application variables. Because secret application variables are never exposed to front components, keep API keys and other sensitive logic in the logic function, not in the front component.

Calling the Twenty REST API

To call app HTTP routes or read and write Twenty records from a front component, use RestApiClient from twenty-client-sdk/rest. It sends /s/... paths to your workspace’s functions base URL and every other path, including /rest/..., to TWENTY_API_URL. It always acts as the person looking at the page. runAs: 'application' is a logic-function option only: a component never receives your application’s own token, so asking for it here throws. Put work that needs the app’s own access behind a logic function and call that instead. options accepts headers, query (a record of query-string params; nullish values are skipped), and an AbortSignal via signal. A non-FormData object body is JSON-serialized automatically. On a 401, the client refreshes the access token once through the host and retries the request. The base URL and token are resolved from the environment by default. Pass overrides to the constructor when needed — for example in tests:
Failed requests throw a RestApiClientError exposing status, statusText, url, and the parsed body:

Accessing runtime context

Inside your component, use SDK hooks to access the current user, record, and component instance:
src/front-components/record-info.tsx
Available hooks:

Object metadata

Use useSelectedObjectMetadata() to identify the object associated with a record-page widget, a custom timeline row, an object-scoped command, or a front component opened in the side panel with an object (from a command, or through openSidePanelPage with an objectNameSingular). This lets an integration select templates or other configuration for the current object without searching across objects for a record ID.
The result contains id (the workspace’s object-metadata UUID), nameSingular, and namePlural. The names are metadata names, such as person and people, rather than display labels. Object metadata can be available even when no records are selected. It belongs to this front component’s context, so a side panel can expose a different object from the main page. The hook updates when that context changes and returns null when the object cannot be resolved, when no object context is supplied, or when running on an older host that does not provide object metadata.

Application variables

Application variables defined in defineApplication() with isSecret: false are available inside front components via the getApplicationVariable utility:
src/front-components/greeting.tsx
Secret variables (isSecret: true) are not exposed to front components. They are only available in logic functions, which run server-side. This prevents sensitive values like API keys from being sent to the browser.
getApplicationVariable always returns a string (or undefined), regardless of the variable’s declared type. The string is serialized consistently by type (booleans as "true" / "false", numbers as decimal strings, arrays / objects as JSON), the same format used for logic-function process.env — parse it yourself (Number(...), JSON.parse(...), === 'true'). See Variable types.

Reading and updating variables at runtime

getApplicationVariable returns the values injected when the component loaded, so a value written afterwards only shows up on the next load. To read the stored values, or to write one (what a settings menu item does when it saves what the user typed), go through MetadataApiClient:
src/front-components/app-settings.tsx
Notes:
  • Your component’s token is bound to your own application. updateOneApplicationVariable targets it, so applicationId is optional and naming another application is refused. Reading the applicationVariables of another application is refused the same way, and an application belonging to another workspace reads as not found.
  • The key must be one your app declares in defineApplication(). Writing a key that is not declared is a not found.
  • Secret variables read back masked, never as the stored value. Writing them works normally.
  • Reading a variable’s value, and writing it, need the APPLICATIONS settings permission on both your app’s role and the signed-in person’s role, since the component’s token is the intersection of the two. Add SystemPermissionFlag.APPLICATIONS to permissionFlagUniversalIdentifiers in your application role.
  • A user variable (scope: 'USER') is injected with the viewing member’s value, unless it is secret. updateOneApplicationVariable refuses it: save it with updateMyUserApplicationVariable, as saveMyLanguage does above. It writes the viewing member’s value only and refuses naming another application. It needs no APPLICATIONS permission, but it needs the CONNECTED_ACCOUNTS permission on the viewing member’s role and on your app’s role, if it declares one: add SystemPermissionFlag.CONNECTED_ACCOUNTS to permissionFlagUniversalIdentifiers in your application role. See Workspace and user variables.

System variables

The following system variables are always available via process.env:

TWENTY_FUNCTIONS_URL

Twenty also injects TWENTY_FUNCTIONS_URL into front components and logic functions: the base URL your app’s HTTP-triggered logic functions are served from. It exists because that URL is not always the Twenty server itself. On Twenty Cloud, app routes are served on a dedicated per-workspace domain (https://<your-workspace-subdomain>.withtwenty.com, or the application’s primary public domain when one is configured) so that app-authored responses run on their own origin rather than on the Twenty app origin. Self-hosted and local instances serve app routes under the /s prefix on the server itself and may not set the variable at all. Since the base URL varies per workspace and per instance, your code cannot hard-code it — the server injects the right value at runtime. You rarely need to read it directly. Call your routes through RestApiClient with a /s/-prefixed path and the client resolves the URL for you: it strips the /s prefix and targets TWENTY_FUNCTIONS_URL, falling back to <TWENTY_API_URL>/s when the variable is not set. Use resolveUrl('/s/<path>') to get the absolute URL without sending a request, e.g. for a link. Read the variable directly only when building a URL by hand:

Host communication API

Front components can trigger navigation, modals, and notifications using functions from twenty-sdk/front-component: Use openCommandConfirmationModal from twenty-sdk/front-component for dialogs. It accepts structured title, subtitle and button options, and resolves to 'confirm' or 'cancel'. Twenty owns the modal, focus management, dismissal and page scrolling. Front components must not render Dialog, AlertDialog, native dialog elements or native browser popovers directly. Supplying a local portal container does not make direct app-owned modality supported. Here is an example that uses the host API to show a snackbar and close the side panel after an action completes:
src/front-components/archive-record.tsx

Storage

localStorage and sessionStorage work as they do in a normal page, with the standard synchronous API. Your keys are scoped to your app install and the signed-in user: no other app can read them, and another user signing into the same browser starts from an empty store. Values written to localStorage stay on the device across reloads; sessionStorage lasts for the browser session.
src/front-components/note-draft.tsx
Twenty stores the values on your app’s behalf, so a write is applied locally right away and saved in the background. Reads never wait on the host. Nothing is synced: the values do not follow the user to another browser or machine, so use a logic function’s key-value store for anything that must survive a device change. Writes are capped, and every limit counts characters rather than bytes: keys are at most 512 characters, a single value at most 262,144 characters, and each storage at most 1,048,576 characters per app and user. A write that breaks a limit throws a QuotaExceededError, like the browser API.

Working with multiple records

Use useSelectedRecordIds() to handle multiple selected records. This is useful for bulk operations:
src/front-components/bulk-export.tsx
Surface it with a command menu item restricted to record selections:
src/command-menu-items/bulk-export.command-menu-item.ts

Select all

When the user selects all records of a view, useSelectedRecordIds() returns an empty array: the selection is defined by the view’s filters, search and unticked rows, and can hold far more records than fit in memory. useSelectedRecordsFilter() returns a filter matching exactly the selected records in every case, so query with it instead of listing IDs:
For large selections, pass the filter to a logic function and process the records there, page by page.

Public assets

Front components can access files from the app’s public/ directory using getPublicAssetUrl:
See the public assets section for details.

Sharing dependencies across front components

By default, every front component bundles its own copy of the libraries it imports, so an app with five components ships React five times. Declare shared dependencies in your app’s package.json to build those libraries once and have every component of the app load them from a single cached file:
package.json
Each component then imports its dependencies exactly as before — nothing changes in your component code:
src/front-components/counter.tsx
A few things to know:
  • One shared dependencies bundle per app. The bundle is built from your app’s own dependencies, so you keep full control of the versions you ship.
  • List the exact specifiers you import. twenty-ui/primitives/input and twenty-ui/primitives/data-display are two entries; a package name alone does not cover its subpaths. Listing react automatically covers react/jsx-runtime.
  • Share react-dom/client alongside react. Every component renders through createRoot, so leaving it out means each component still bundles React DOM.
  • The bundle is cached. It is served under a content-hash URL with a long-lived immutable cache, so it is downloaded once and reused across all components of the app until one of its dependencies changes.
  • Components that import none of the shared packages never download it.

Styling

Front components support multiple styling approaches. You can use:
  • Inline styles — style={{ color: 'red' }}
  • Twenty UI components — Twenty’s own component library; see Using Twenty UI components below
  • Emotion — CSS-in-JS with @emotion/react
  • Styled-components — styled.div patterns
  • Tailwind CSS — utility classes
  • Any CSS-in-JS library compatible with React

Using Twenty UI components

Twenty ships its component library as the twenty-ui package. Front components can use it for buttons, tags, status pills, chips, avatars, icons, typography, and theme tokens that automatically match the workspace’s light and dark theme.
twenty-ui is still in alpha. Its version number follows the Twenty SDK release cycle. APIs and component behavior may change between releases.

Installation

twenty-ui is published alongside the SDK packages with the same version. Add it to your app, pinned to the version of twenty-sdk and twenty-client-sdk you use:
twenty-ui is bundled into your front component at build time, so it only needs to be a dependency of your app — there is nothing to configure at runtime.

Importing components

Import from the matching subpath rather than the package root, so only the components you use end up in your bundle: Foundational controls are grouped under twenty-ui/primitives/<family>. Shared design presets and app building blocks are available from twenty-ui/components. These entry points do not require monaco-editor. Use Dropdown for action menus, option pickers, and form panels. The standalone Dialog and AlertDialog APIs are also exported by Twenty UI, but direct use in front components is prohibited. Use the host communication API for dialogs.

Icons

Import individual icons from twenty-ui/icon:
Each named icon is tree-shaken, so importing a handful adds little to your bundle. Avoid IconsProvider and useIcons in front components. Front components are bundled without code splitting, so the provider’s dynamic icon catalog (the full Tabler set, roughly 2 MB minified) ends up in your bundle. See the icon reference for available exports and supported props.

Theming and theme tokens

Twenty UI components automatically match the workspace’s light and dark theme — the renderer applies the active color scheme on the host, and the components resolve their colors against it. To use the same design tokens in your own inline styles, call the useTheme() hook. It returns Twenty’s theme tokens (spacing, colors, radii, fonts) wired to the active theme, with no ThemeProvider setup needed in your component:
Because useTheme() is a hook, you read tokens inside the component body, so the values always reflect the live theme. The same token map is also exported as the themeCssVariables constant, but prefer useTheme() in front components — a module-level constant that dereferences themeCssVariables can be undefined while the app manifest is extracted. To branch on the active scheme explicitly, read it with useColorScheme() from twenty-sdk/front-component, which returns 'light' or 'dark'.

Current limitations

Front components are under active development. Rendering, styling, handling events, measuring elements, body portals and browser storage work well. Anything that reaches past those (calling a DOM method on a ref, for example) is missing or incomplete today, and most of it fails silently: no exception, and no TypeScript error either, since the scaffold is typed against the full browser DOM. If one of these blocks you, open an issue so it gets prioritized.

Layout and measurement

Elements can measure themselves: the host mirrors geometry into the sandbox, so reads are answered locally but can be up to one frame stale, and the first read of a never-measured element returns zeros. After writing, re-read in a requestAnimationFrame callback or an effect. Positioning from getBoundingClientRect and size observation through ResizeObserver both work from the same mirror, so resize callbacks trail the host by about a frame. Prefer CSS for layout anyway: your stylesheet reaches the real page, so flexbox, grid, aspect-ratio, clamp() and @container behave normally, with no frame lag. matchMedia() evaluates width, height, orientation, -webkit-device-pixel-ratio, resolution, hover, pointer and prefers-color-scheme, in the min-/max- form, the Level 4 range form ((width >= 600px), (400px <= width <= 800px)) and the valueless form ((width)). Widths, heights and orientation describe your widget’s box, not the browser viewport, so they follow document.body.clientWidth rather than window.innerWidth, and em and rem count as 16px. hover and pointer describe the primary input of the user’s device. Any other condition counts as unknown, the way browsers treat unknown features: the comma-separated member it appears in evaluates to false, even under not, unless the rest of that member already decides the result, as in not print and (prefers-reduced-motion: reduce). That covers unknown features and the syntax this parser does not implement inside a condition, such as nested groups like ((min-width: 600px)), not inside a group and calc() values. An or between conditions makes its member false. The MediaQueryList and MediaQueryListEvent constructors are not exposed either, so instanceof checks against them throw a ReferenceError. Because that stylesheet reaches the real page, a @media (min-width: ...) rule is evaluated by the host against the browser viewport, while matchMedia('(min-width: ...)') answers for your widget’s box. To make CSS follow the same box your JavaScript sees, declare your own container-type on a wrapper and query that: an @container rule without one resolves against the nearest host container, not your widget. The same split applies to color scheme, where a stylesheet @media (prefers-color-scheme: dark) follows the browser and OS preference while matchMedia('(prefers-color-scheme: dark)') follows the theme selected in Twenty, so style from useTheme() tokens when the two must agree. The height you are handed is your own box as well, and depending on where the host mounts you that box can be sized by your own content. Branching layout on min-height, max-height or orientation can then feed back into itself and oscillate. Prefer width-based queries, or @container with container-type: inline-size, for layout decisions.
requestAnimationFrame works, with or without the window. prefix. Only Chrome runs a real animation frame inside a worker; Firefox and Safari get a 16 ms timer, so use it to defer a re-read past a write, not to drive an animation.

Global scope

The worker’s own natives are also aliased onto window, so they work with or without the window. prefix: fetch, setTimeout/clearTimeout, setInterval/clearInterval, queueMicrotask, console, crypto, performance, structuredClone, btoa/atob, URL/URLSearchParams, AbortController/AbortSignal, Headers/Request/Response, Blob, File, FormData and TextEncoder/TextDecoder.

DOM access

A ref gives you a sandbox element, not an HTMLElement. element.classList works, backed by the class attribute: add, remove, toggle, replace, contains, item, length, value and iteration all behave as they do in a browser, and className stays in sync. element.dataset works the same way, backed by the data-* attributes; write data-* names in lowercase, since the sandbox keeps attribute names as written instead of lowercasing them like a browser. Body portals can extend up to 200px beyond each edge of your component, including outside a widget’s scroll frame. Larger overlays are clipped to this area, so give long menus their own scrolling container. Native top-layer APIs such as showModal(), showPopover() and requestFullscreen() are blocked.

Events

Mouse, pointer, touch, drag, keyboard, focus, input/change/submit, scroll/wheel/contextmenu and animationend/transitionend cross to the host, plus a few per element: load/error on img, clipboard and composition on input/textarea, media on video/audio, toggle on details/dialog. Anything else (onAuxClick, onSelect, onInvalid, onReset, onAnimationStart, pointer capture, onLoad off img) is dropped without warning. Handlers receive typed events built from the page event, PointerEvent, MouseEvent, KeyboardEvent, InputEvent, WheelEvent or FocusEvent, and those constructors exist in the sandbox for dispatchEvent(). Each forwarded event also carries React’s synthetic event surface (nativeEvent, isDefaultPrevented(), isPropagationStopped(), persist()), so code written against React events keeps working even though the sandbox hands the event object to your handler directly. A page event crosses when the element the user interacted with, or one of its ancestors in your component, has a handler for that event type. It crosses once and then propagates inside the sandbox as it does on the page: event.target is the element the user interacted with, currentTarget is the element whose handler is running, stopPropagation() keeps ancestors’ handlers from running, and relatedTarget points to the sandbox element focus or the pointer moved from or to, or is null when that element is outside your component. Like in React, onFocus and onBlur also run for focus changes inside the element, and onClickCapture and the other capture handlers run during the capture phase. Handlers a library adds with React.cloneElement, such as the ones behind Base UI’s render prop, run after the element’s own handlers instead of replacing them, so passing onClick: undefined to cloneElement does not remove the element’s handler. A handler on a Base UI render element can still call event.preventBaseUIHandler() to skip Base UI’s own handler. event.target.value and checked reflect the control once the browser has applied the user’s change: in input, change, click, blur and focusout handlers, and in keyup handlers for value. In keydown, beforeinput and pointer handlers they hold the control’s value and checked state as of its last edit, whatever your component listens to: an Enter handler reads the text typed so far, but a handler for a printable key does not see that key yet, so read live values from a controlled input or an onChange handler. document listeners receive the events that cross from your component, but nothing that happens elsewhere on the page, so to catch shortcuts with document.addEventListener('keydown', ...), put an onKeyDown on an element that wraps your component. window listeners only receive error and unhandledrejection. Pointer moves only cross over elements that have a move handler, and pointer capture stays on the page, which is why a drag stops as soon as the pointer leaves those elements. event.preventDefault() does not cross either; form submission, dragover/drop and link clicks are already guarded for you.

Attributes and styling

Each element forwards its own properties to the host DOM (href on a, src/alt on img, value/placeholder/disabled on input, and so on), plus a common set on every element: id, className, style, title, tabIndex, role, draggable and any aria-* / data-* attribute (hyphenated, so ariaLabel is dropped). Anything outside that is silently discarded, so express custom state as data-*. Component CSS, whether from import './styles.css', CSS-in-JS or a style element, is injected into the host page’s head unscoped. So class names collide with Twenty’s own (prefix them, and never write bare div { ... } selectors), and @media matches the browser window rather than your widget (use @container with your own container-type). Inline style props are unaffected.

Storage and network

localStorage and sessionStorage are provided by Twenty rather than the browser: the component runs in a worker at an opaque origin, so the host stores the values on your app’s behalf. See storage for their scoping and limits. IndexedDB, cookies, the Cache API and BroadcastChannel remain unavailable. To persist state across devices, call a logic function and use its key-value store. fetch works, with caveats:
  • Calls to the Twenty API and your app’s routes are proxied by the host, so prefer RestApiClient. On proxied calls, AbortSignal and the other RequestInit options are dropped, and only string and URLSearchParams bodies are supported.
  • Other origins leave the sandbox with Origin: null, so a third-party API answers only if it sends Access-Control-Allow-Origin: *. Call it from a logic function instead.
  • fetch('/rest/people') is never matched to the Twenty API, because the sandbox has no page URL to resolve a relative path against.

File selection

File inputs deliver native File objects with metadata and readable contents. Use text() or arrayBuffer() to read a selected file, or pass it to the uploadFile host function. Opening the native chooser requires calling input.click() within one second of a trusted click inside your component. The SDK-built React and Preact flows are verified in Chromium. Worker-created object URLs assigned to img.src support previews. Revoke your URLs when replacing the image or unmounting your component. Your application owns file validation, upload progress and cancellation.

Media capture

Declare microphone, camera, or both in the requestedCapabilities array passed to defineApplication() so these capabilities appear during installation. Approval is workspace-wide and requires a member who can manage apps. If an installed app calls navigator.mediaDevices.getUserMedia() without the required access, Twenty pauses capture and shows an approval dialog. This also supports older apps without capability declarations. Members who cannot manage apps must ask a workspace administrator to approve access. Cancelling the dialog rejects the request before browser or operating system permissions are requested. Package updates preserve existing workspace grants and do not automatically grant additional media access. Locally synced development apps receive the capabilities declared in their manifest. Once access is approved, Twenty does not ask again for each capture, but browser and operating system permissions still apply. An orange dot in the workspace header indicates active capture. Hover over or select the dot to see the apps using the microphone or camera and stop their capture. Pending requests appear as an outlined dot with a cancellation control. getUserMedia() and MediaRecorder run through sandbox polyfills, so standard recording code and MediaRecorder.isTypeSupported() work unchanged after approval. Detailed constraint objects are accepted but not forwarded. The host captures with its defaults for the requested kinds. Different applications can capture concurrently, and stopping an app affects only its own sessions. Each front component supports one pending or active capture at a time. Store a recorded Blob with the uploadFile host function.

Other gaps

  • Drag-and-drop payloads. Drag events fire, but event.dataTransfer is undefined.
  • Node built-ins. fs, path and node:crypto fail the build, so move that work into a logic function. Web Crypto replaces node:crypto, and fetch, TextEncoder and URL are available too, see global scope — but nothing replaces fs: the sandbox has no filesystem.
  • iframe is always re-sandboxed without allow-same-origin, so an embed relying on its own session renders logged out. It has no onLoad either.