diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
index d09443516c..cad178d61b 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.graphql
@@ -323,6 +323,7 @@ type FrontComponent {
isHeadless: Boolean!
usesSdkClient: Boolean!
applicationTokenPair: ApplicationTokenPair
+ applicationVariables: JSON
}
type CommandMenuItem {
diff --git a/packages/twenty-client-sdk/src/metadata/generated/schema.ts b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
index f8f4fbc623..fa1453d2ad 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/schema.ts
+++ b/packages/twenty-client-sdk/src/metadata/generated/schema.ts
@@ -276,6 +276,7 @@ export interface FrontComponent {
isHeadless: Scalars['Boolean']
usesSdkClient: Scalars['Boolean']
applicationTokenPair?: ApplicationTokenPair
+ applicationVariables?: Scalars['JSON']
__typename: 'FrontComponent'
}
@@ -3173,6 +3174,7 @@ export interface FrontComponentGenqlSelection{
isHeadless?: boolean | number
usesSdkClient?: boolean | number
applicationTokenPair?: ApplicationTokenPairGenqlSelection
+ applicationVariables?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
diff --git a/packages/twenty-client-sdk/src/metadata/generated/types.ts b/packages/twenty-client-sdk/src/metadata/generated/types.ts
index 2cb30aa50c..56106c99df 100644
--- a/packages/twenty-client-sdk/src/metadata/generated/types.ts
+++ b/packages/twenty-client-sdk/src/metadata/generated/types.ts
@@ -707,6 +707,9 @@ export default {
"applicationTokenPair": [
33
],
+ "applicationVariables": [
+ 15
+ ],
"__typename": [
1
]
diff --git a/packages/twenty-docs/developers/extend/apps/config/application.mdx b/packages/twenty-docs/developers/extend/apps/config/application.mdx
index c5825c51d9..5a29870fbc 100644
--- a/packages/twenty-docs/developers/extend/apps/config/application.mdx
+++ b/packages/twenty-docs/developers/extend/apps/config/application.mdx
@@ -31,7 +31,7 @@ export default defineApplication({
Notes:
- `universalIdentifier` fields are deterministic IDs you own. Generate them once and keep them stable across syncs.
-- `applicationVariables` become environment variables for your functions and front components (e.g., `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
+- `applicationVariables` become environment variables for your functions and front components. In logic functions (server-side), they are available as `process.env.VARIABLE_NAME`. In front components, use `getApplicationVariable('VARIABLE_NAME')` from `twenty-sdk/front-component`. Variables marked with `isSecret: true` are only injected into logic functions. Front components receive only non-secret variables.
- The default role is detected automatically from the role file marked with [`defineApplicationRole()`](/developers/extend/apps/config/roles) — you do not need to reference it from `defineApplication()`.
- Pre-install and post-install functions are detected automatically during the manifest build — you do not need to reference them in `defineApplication()`.
- Passing `defaultRoleUniversalIdentifier` explicitly is still supported for backward compatibility, but is deprecated in favor of `defineApplicationRole()`.
diff --git a/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx b/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx
index 1c7ab6fb9a..f42e254c40 100644
--- a/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx
+++ b/packages/twenty-docs/developers/extend/apps/layout/front-components.mdx
@@ -239,6 +239,38 @@ Available hooks:
| `useFrontComponentId()` | `string` | This component instance's ID |
| `useFrontComponentExecutionContext(selector)` | varies | Access the full execution context with a selector function |
+## Application variables
+
+Application variables defined in [`defineApplication()`](/developers/extend/apps/config/application) with `isSecret: false` are available inside front components via the `getApplicationVariable` utility:
+
+```tsx src/front-components/greeting.tsx
+import { defineFrontComponent } from 'twenty-sdk/define';
+import { getApplicationVariable } from 'twenty-sdk/front-component';
+
+const Greeting = () => {
+ const recipientName = getApplicationVariable('DEFAULT_RECIPIENT_NAME') ?? 'World';
+
+ return
Hello, {recipientName}!
;
+};
+
+export default defineFrontComponent({
+ universalIdentifier: '...',
+ name: 'greeting',
+ component: Greeting,
+});
+```
+
+
+Secret variables (`isSecret: true`) are **not** exposed to front components. They are only available in [logic functions](/developers/extend/apps/logic/logic-functions), which run server-side. This prevents sensitive values like API keys from being sent to the browser.
+
+
+The following system variables are always available via `process.env`:
+
+| Variable | Description |
+|----------|-------------|
+| `TWENTY_API_URL` | Base URL of the Twenty API |
+| `TWENTY_APP_ACCESS_TOKEN` | Short-lived token scoped to your app's role |
+
## Host communication API
Front components can trigger navigation, modals, and notifications using functions from `twenty-sdk`:
diff --git a/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx b/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx
index 63767ba0e4..5707497cc8 100644
--- a/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx
+++ b/packages/twenty-front-component-renderer/src/host/components/FrontComponentRenderer.tsx
@@ -23,6 +23,7 @@ type FrontComponentContentProps = {
applicationAccessToken?: string;
apiUrl?: string;
sdkClientUrls?: SdkClientUrls;
+ applicationVariables?: Record;
executionContext: FrontComponentExecutionContext;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
onError: (error?: Error) => void;
@@ -34,6 +35,7 @@ export const FrontComponentRenderer = ({
applicationAccessToken,
apiUrl,
sdkClientUrls,
+ applicationVariables,
executionContext,
frontComponentHostCommunicationApi,
onError,
@@ -55,6 +57,7 @@ export const FrontComponentRenderer = ({
applicationAccessToken={applicationAccessToken}
apiUrl={apiUrl}
sdkClientUrls={sdkClientUrls}
+ applicationVariables={applicationVariables}
frontComponentId={executionContext.frontComponentId}
setReceiver={setReceiver}
setThread={setThread}
@@ -69,6 +72,7 @@ export const FrontComponentRenderer = ({
applicationAccessToken,
apiUrl,
sdkClientUrls,
+ applicationVariables,
executionContext.frontComponentId,
]);
diff --git a/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx b/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx
index 6abcc53f13..c25dbc1799 100644
--- a/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx
+++ b/packages/twenty-front-component-renderer/src/remote/components/FrontComponentWorkerEffect.tsx
@@ -36,6 +36,7 @@ type FrontComponentWorkerEffectProps = {
applicationAccessToken?: string;
apiUrl?: string;
sdkClientUrls?: SdkClientUrls;
+ applicationVariables?: Record;
frontComponentId: string;
setReceiver: React.Dispatch>;
setThread: React.Dispatch<
@@ -52,6 +53,7 @@ export const FrontComponentWorkerEffect = ({
applicationAccessToken,
apiUrl,
sdkClientUrls,
+ applicationVariables,
frontComponentId,
setReceiver,
setThread,
@@ -121,6 +123,7 @@ export const FrontComponentWorkerEffect = ({
applicationAccessToken,
apiUrl,
sdkClientUrls,
+ applicationVariables,
})
.catch((error: Error) => {
setError(error);
@@ -143,6 +146,7 @@ export const FrontComponentWorkerEffect = ({
applicationAccessToken,
apiUrl,
sdkClientUrls,
+ applicationVariables,
frontComponentId,
setError,
setReceiver,
diff --git a/packages/twenty-front-component-renderer/src/remote/worker/remote-worker.ts b/packages/twenty-front-component-renderer/src/remote/worker/remote-worker.ts
index b221fda899..401fd471ea 100644
--- a/packages/twenty-front-component-renderer/src/remote/worker/remote-worker.ts
+++ b/packages/twenty-front-component-renderer/src/remote/worker/remote-worker.ts
@@ -91,6 +91,13 @@ const render: WorkerExports['render'] = async (
document.body.append(root);
installStyleBridge(root);
+ if (isDefined(renderContext.applicationVariables)) {
+ setWorkerEnv({
+ applicationVariables: JSON.stringify(renderContext.applicationVariables),
+ });
+ }
+
+ // System variables are set after application variables so they cannot be overridden
if (isDefined(renderContext.apiUrl)) {
setWorkerEnv({
TWENTY_API_URL: renderContext.apiUrl,
diff --git a/packages/twenty-front-component-renderer/src/remote/worker/utils/setWorkerEnv.ts b/packages/twenty-front-component-renderer/src/remote/worker/utils/setWorkerEnv.ts
index 5e470887b5..ab6c8e07a9 100644
--- a/packages/twenty-front-component-renderer/src/remote/worker/utils/setWorkerEnv.ts
+++ b/packages/twenty-front-component-renderer/src/remote/worker/utils/setWorkerEnv.ts
@@ -1,4 +1,4 @@
-export const setWorkerEnv = (environmentVariables: Record) => {
+export const setWorkerEnv = (variables: Record) => {
const globalObject = globalThis as Record;
const processObject =
(globalObject['process'] as Record | undefined) ?? {};
@@ -7,7 +7,7 @@ export const setWorkerEnv = (environmentVariables: Record) => {
processObject['env'] = {
...processEnvironment,
- ...environmentVariables,
+ ...variables,
};
globalObject['process'] = processObject;
diff --git a/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts b/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts
index dc4eb9bb9b..83c99e644c 100644
--- a/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts
+++ b/packages/twenty-front-component-renderer/src/types/HostToWorkerRenderContext.ts
@@ -8,4 +8,5 @@ export type HostToWorkerRenderContext = {
applicationAccessToken?: string;
apiUrl?: string;
sdkClientUrls?: SdkClientUrls;
+ applicationVariables?: Record;
};
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index d9a141efd8..92b967453e 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -1841,6 +1841,7 @@ export type FrontComponent = {
__typename?: 'FrontComponent';
applicationId: Scalars['UUID'];
applicationTokenPair?: Maybe;
+ applicationVariables?: Maybe;
builtComponentChecksum: Scalars['String'];
builtComponentPath: Scalars['String'];
componentName: Scalars['String'];
@@ -6587,7 +6588,7 @@ export type FindOneFrontComponentQueryVariables = Exact<{
}>;
-export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, builtComponentChecksum: string, isHeadless: boolean, usesSdkClient: boolean, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null };
+export type FindOneFrontComponentQuery = { __typename?: 'Query', frontComponent?: { __typename?: 'FrontComponent', id: string, name: string, applicationId: string, builtComponentChecksum: string, isHeadless: boolean, usesSdkClient: boolean, applicationVariables?: any | null, applicationTokenPair?: { __typename?: 'ApplicationTokenPair', applicationAccessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, applicationRefreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } | null } | null };
export type LogicFunctionFieldsFragment = { __typename?: 'LogicFunction', id: string, name: string, description?: string | null, runtime: string, timeoutSeconds: number, sourceHandlerPath: string, handlerName: string, cronTriggerSettings?: any | null, databaseEventTriggerSettings?: any | null, httpRouteTriggerSettings?: any | null, toolTriggerSettings?: any | null, workflowActionTriggerSettings?: any | null, applicationId?: string | null, universalIdentifier?: string | null, createdAt: string, updatedAt: string };
@@ -8045,7 +8046,7 @@ export const UploadFilesFieldFileDocument = {"kind":"Document","definitions":[{"
export const UploadWorkflowFileDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadWorkflowFile"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadWorkflowFile"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"size"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode;
export const RenewApplicationTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RenewApplicationToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"applicationRefreshToken"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"renewApplicationToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"applicationRefreshToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"applicationRefreshToken"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]} as unknown as DocumentNode;
export const FindManyFrontComponentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyFrontComponents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentChecksum"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentPath"}},{"kind":"Field","name":{"kind":"Name","value":"componentName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}},{"kind":"Field","name":{"kind":"Name","value":"sourceComponentPath"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"usesSdkClient"}}]}}]}}]} as unknown as DocumentNode;
-export const FindOneFrontComponentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneFrontComponent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentChecksum"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}},{"kind":"Field","name":{"kind":"Name","value":"usesSdkClient"}},{"kind":"Field","name":{"kind":"Name","value":"applicationTokenPair"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]}}]} as unknown as DocumentNode;
+export const FindOneFrontComponentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneFrontComponent"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"frontComponent"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"builtComponentChecksum"}},{"kind":"Field","name":{"kind":"Name","value":"isHeadless"}},{"kind":"Field","name":{"kind":"Name","value":"usesSdkClient"}},{"kind":"Field","name":{"kind":"Name","value":"applicationVariables"}},{"kind":"Field","name":{"kind":"Name","value":"applicationTokenPair"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"applicationAccessToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"applicationRefreshToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]}}]} as unknown as DocumentNode;
export const CreateOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateLogicFunctionFromSourceInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode;
export const DeleteOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"LogicFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"LogicFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"LogicFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"timeoutSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"sourceHandlerPath"}},{"kind":"Field","name":{"kind":"Name","value":"handlerName"}},{"kind":"Field","name":{"kind":"Name","value":"cronTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"databaseEventTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"httpRouteTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"toolTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"workflowActionTriggerSettings"}},{"kind":"Field","name":{"kind":"Name","value":"applicationId"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode;
export const ExecuteOneLogicFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ExecuteOneLogicFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ExecuteOneLogicFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"executeOneLogicFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"logs"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode;
diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx
index 30698f5df0..650740a5a3 100644
--- a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx
+++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx
@@ -100,6 +100,9 @@ export const FrontComponentRenderer = ({
const accessToken = applicationTokenPair.applicationAccessToken.token;
+ const applicationVariables =
+ data.frontComponent.applicationVariables ?? undefined;
+
if (usesSdkClient) {
return (
@@ -112,6 +115,7 @@ export const FrontComponentRenderer = ({
frontComponentHostCommunicationApi={
frontComponentHostCommunicationApi
}
+ applicationVariables={applicationVariables}
onError={handleError}
/>
@@ -127,6 +131,7 @@ export const FrontComponentRenderer = ({
apiUrl={REACT_APP_SERVER_BASE_URL}
executionContext={executionContext}
frontComponentHostCommunicationApi={frontComponentHostCommunicationApi}
+ applicationVariables={applicationVariables}
onError={handleError}
/>
diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx
index 7f16e9013b..0d2402a7cd 100644
--- a/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx
+++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentRendererWithSdkClient.tsx
@@ -16,6 +16,7 @@ type FrontComponentRendererWithSdkClientProps = {
applicationId: string;
executionContext: FrontComponentExecutionContext;
frontComponentHostCommunicationApi: FrontComponentHostCommunicationApi;
+ applicationVariables?: Record;
onError: (error?: Error) => void;
};
@@ -26,6 +27,7 @@ export const FrontComponentRendererWithSdkClient = ({
applicationId,
executionContext,
frontComponentHostCommunicationApi,
+ applicationVariables,
onError,
}: FrontComponentRendererWithSdkClientProps) => {
const sdkClientState = useAtomValue(
@@ -50,6 +52,7 @@ export const FrontComponentRendererWithSdkClient = ({
frontComponentHostCommunicationApi={
frontComponentHostCommunicationApi
}
+ applicationVariables={applicationVariables}
onError={onError}
/>
)}
diff --git a/packages/twenty-front/src/modules/front-components/graphql/queries/findOneFrontComponent.ts b/packages/twenty-front/src/modules/front-components/graphql/queries/findOneFrontComponent.ts
index f13be24c1c..17c80c18a0 100644
--- a/packages/twenty-front/src/modules/front-components/graphql/queries/findOneFrontComponent.ts
+++ b/packages/twenty-front/src/modules/front-components/graphql/queries/findOneFrontComponent.ts
@@ -9,6 +9,7 @@ export const FIND_ONE_FRONT_COMPONENT = gql`
builtComponentChecksum
isHeadless
usesSdkClient
+ applicationVariables
applicationTokenPair {
applicationAccessToken {
token
diff --git a/packages/twenty-front/src/modules/settings/logic-functions/components/SettingsLogicFunctionCodeEditor.tsx b/packages/twenty-front/src/modules/settings/logic-functions/components/SettingsLogicFunctionCodeEditor.tsx
index 1f08de4064..5ff5871db2 100644
--- a/packages/twenty-front/src/modules/settings/logic-functions/components/SettingsLogicFunctionCodeEditor.tsx
+++ b/packages/twenty-front/src/modules/settings/logic-functions/components/SettingsLogicFunctionCodeEditor.tsx
@@ -16,6 +16,7 @@ type SettingsLogicFunctionCodeEditorProps = Omit & {
currentFilePath: string;
files: File[];
onChange: (value: string) => void;
+ applicationVariableKeys?: string[];
};
export const SettingsLogicFunctionCodeEditor = ({
@@ -24,6 +25,7 @@ export const SettingsLogicFunctionCodeEditor = ({
onChange,
height = 450,
options = undefined,
+ applicationVariableKeys,
}: SettingsLogicFunctionCodeEditorProps) => {
const { logicFunctionId = '' } = useParams();
const { availablePackages } = useGetAvailablePackages({
@@ -63,15 +65,16 @@ export const SettingsLogicFunctionCodeEditor = ({
target: monaco.languages.typescript.ScriptTarget.ESNext,
});
- // TODO load that with proper env variables
- const environmentVariables = {};
+ const applicationVariables = Object.fromEntries(
+ (applicationVariableKeys ?? []).map((key) => [key, '']),
+ );
- if (isDefined(environmentVariables)) {
- const envTypeDefinitions = Object.keys(environmentVariables)
+ if (isDefined(applicationVariables)) {
+ const envTypeDefinitions = Object.keys(applicationVariables)
// oxlint-disable-next-line lingui/no-unlocalized-strings
.map((key) => `${key}: string;`)
.join('\n');
- const environmentDefinition = `
+ const applicationVariableDefinition = `
declare namespace NodeJS {
interface ProcessEnv {
${envTypeDefinitions}
@@ -85,7 +88,7 @@ export const SettingsLogicFunctionCodeEditor = ({
monaco.languages.typescript.typescriptDefaults.setExtraLibs([
{
- content: environmentDefinition,
+ content: applicationVariableDefinition,
filePath: 'ts:process-env.d.ts',
},
]);
diff --git a/packages/twenty-front/src/modules/settings/logic-functions/components/tabs/SettingsLogicFunctionCodeEditorTab.tsx b/packages/twenty-front/src/modules/settings/logic-functions/components/tabs/SettingsLogicFunctionCodeEditorTab.tsx
index 9e1c7e12bf..45d03d7460 100644
--- a/packages/twenty-front/src/modules/settings/logic-functions/components/tabs/SettingsLogicFunctionCodeEditorTab.tsx
+++ b/packages/twenty-front/src/modules/settings/logic-functions/components/tabs/SettingsLogicFunctionCodeEditorTab.tsx
@@ -23,11 +23,13 @@ export const SettingsLogicFunctionCodeEditorTab = ({
handleExecute,
onChange,
isTesting = false,
+ applicationVariableKeys,
}: {
files: File[];
handleExecute: () => void;
onChange: (value: string) => void;
isTesting?: boolean;
+ applicationVariableKeys?: string[];
}) => {
const activeTabId = useAtomComponentStateValue(
activeTabIdComponentState,
@@ -68,6 +70,7 @@ export const SettingsLogicFunctionCodeEditorTab = ({
files={files}
currentFilePath={activeTabId}
onChange={(newCodeValue: string) => onChange(newCodeValue)}
+ applicationVariableKeys={applicationVariableKeys}
/>
)}
diff --git a/packages/twenty-front/src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx b/packages/twenty-front/src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
index 53c48f489a..3bfea560b4 100644
--- a/packages/twenty-front/src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
+++ b/packages/twenty-front/src/pages/settings/logic-functions/SettingsLogicFunctionDetail.tsx
@@ -44,6 +44,11 @@ export const SettingsLogicFunctionDetail = () => {
const applicationName = data?.findOneApplication?.name;
+ const applicationVariableKeys =
+ data?.findOneApplication?.applicationVariables?.map(
+ (variable) => variable.key,
+ ) ?? [];
+
const workspaceCustomApplicationId =
currentWorkspace?.workspaceCustomApplication?.id;
@@ -152,6 +157,7 @@ export const SettingsLogicFunctionDetail = () => {
handleExecute={handleTestFunction}
onChange={onChange('sourceHandlerCode')}
isTesting={isExecuting}
+ applicationVariableKeys={applicationVariableKeys}
/>
)}
{isTriggersTab && (
diff --git a/packages/twenty-sdk/src/sdk/front-component/functions/getApplicationVariable.ts b/packages/twenty-sdk/src/sdk/front-component/functions/getApplicationVariable.ts
new file mode 100644
index 0000000000..fc971385ab
--- /dev/null
+++ b/packages/twenty-sdk/src/sdk/front-component/functions/getApplicationVariable.ts
@@ -0,0 +1,11 @@
+export const getApplicationVariable = (key: string): string | undefined => {
+ const raw = process.env.applicationVariables;
+
+ if (!raw) {
+ return undefined;
+ }
+
+ const variables = JSON.parse(raw) as Record;
+
+ return variables[key];
+};
diff --git a/packages/twenty-sdk/src/sdk/front-component/index.ts b/packages/twenty-sdk/src/sdk/front-component/index.ts
index eb7b31d03e..86cfb8ba25 100644
--- a/packages/twenty-sdk/src/sdk/front-component/index.ts
+++ b/packages/twenty-sdk/src/sdk/front-component/index.ts
@@ -29,6 +29,7 @@ export {
objectMetadataItem,
} from './conditional-availability/conditional-availability-variables';
export { closeSidePanel } from './functions/closeSidePanel';
+export { getApplicationVariable } from './functions/getApplicationVariable';
export { enqueueSnackbar } from './functions/enqueueSnackbar';
export { navigate } from './functions/navigate';
export { openCommandConfirmationModal } from './functions/openCommandConfirmationModal';
diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/front-component.dto.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/front-component.dto.ts
index 5b2377aea6..9cc7c93f2c 100644
--- a/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/front-component.dto.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/front-component/dtos/front-component.dto.ts
@@ -8,6 +8,7 @@ import {
IsString,
IsUUID,
} from 'class-validator';
+import { GraphQLJSON } from 'graphql-type-json';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { ApplicationTokenPairDTO } from 'src/engine/core-modules/application/application-oauth/dtos/application-token-pair.dto';
@@ -74,4 +75,7 @@ export class FrontComponentDTO {
@Field(() => ApplicationTokenPairDTO, { nullable: true })
applicationTokenPair?: ApplicationTokenPairDTO;
+
+ @Field(() => GraphQLJSON, { nullable: true })
+ applicationVariables?: Record;
}
diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts
index e6b4335ec2..1c65111764 100644
--- a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.module.ts
@@ -11,6 +11,7 @@ import { FrontComponentService } from 'src/engine/metadata-modules/front-compone
import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor';
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
import { SubscriptionsModule } from 'src/engine/subscriptions/subscriptions.module';
+import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration.module';
@@ -23,6 +24,7 @@ import { WorkspaceMigrationModule } from 'src/engine/workspace-manager/workspace
PermissionsModule,
FlatFrontComponentModule,
SubscriptionsModule,
+ WorkspaceCacheModule,
],
controllers: [FrontComponentController],
providers: [
diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts
index 8c655f2a42..42376ad8e3 100644
--- a/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts
+++ b/packages/twenty-server/src/engine/metadata-modules/front-component/front-component.resolver.ts
@@ -2,6 +2,7 @@ import { Inject, UseGuards, UseInterceptors } from '@nestjs/common';
import { Args, Mutation, Query } from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
+import { isDefined } from 'twenty-shared/utils';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@@ -21,6 +22,8 @@ import { FrontComponentDTO } from 'src/engine/metadata-modules/front-component/d
import { UpdateFrontComponentInput } from 'src/engine/metadata-modules/front-component/dtos/update-front-component.input';
import { FrontComponentService } from 'src/engine/metadata-modules/front-component/front-component.service';
import { FrontComponentGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/front-component/interceptors/front-component-graphql-api-exception.interceptor';
+import { stripSecretFromApplicationVariables } from 'src/engine/metadata-modules/front-component/utils/strip-secret-from-application-variables';
+import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
@UseGuards(WorkspaceAuthGuard)
@@ -35,6 +38,7 @@ export class FrontComponentResolver {
private readonly frontComponentService: FrontComponentService,
@Inject(ApplicationTokenService)
private readonly applicationTokenService: ApplicationTokenService,
+ private readonly workspaceCacheService: WorkspaceCacheService,
) {}
@Query(() => [FrontComponentDTO])
@@ -67,9 +71,31 @@ export class FrontComponentResolver {
userId: user.id,
});
+ const { applicationVariableMaps } =
+ await this.workspaceCacheService.getOrRecompute(workspace.id, [
+ 'applicationVariableMaps',
+ ]);
+
+ const variableUniversalIdentifiers =
+ applicationVariableMaps.universalIdentifiersByApplicationId[
+ dto.applicationId
+ ] ?? [];
+
+ const flatApplicationVariables = variableUniversalIdentifiers
+ .map(
+ (universalIdentifier) =>
+ applicationVariableMaps.byUniversalIdentifier[universalIdentifier],
+ )
+ .filter(isDefined);
+
+ const applicationVariables = stripSecretFromApplicationVariables(
+ flatApplicationVariables,
+ );
+
return {
...dto,
applicationTokenPair: tokenPair,
+ applicationVariables,
};
}
diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/utils/__tests__/strip-secret-from-application-variables.spec.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/utils/__tests__/strip-secret-from-application-variables.spec.ts
new file mode 100644
index 0000000000..6b709d633a
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/front-component/utils/__tests__/strip-secret-from-application-variables.spec.ts
@@ -0,0 +1,104 @@
+import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type';
+import { stripSecretFromApplicationVariables } from 'src/engine/metadata-modules/front-component/utils/strip-secret-from-application-variables';
+
+const makeFlatVariable = (
+ overrides: Partial,
+): FlatApplicationVariable => ({
+ id: '1',
+ key: 'KEY',
+ value: 'value',
+ description: '',
+ isSecret: false,
+ applicationId: 'app-1',
+ workspaceId: '00000000-0000-0000-0000-000000000000',
+ universalIdentifier: '00000000-0000-0000-0000-000000000000',
+ applicationUniversalIdentifier: '00000000-0000-0000-0000-000000000000',
+ createdAt: '2024-01-01T00:00:00.000Z',
+ updatedAt: '2024-01-01T00:00:00.000Z',
+ ...overrides,
+});
+
+describe('stripSecretFromApplicationVariables', () => {
+ it('should return empty object for empty array', () => {
+ expect(stripSecretFromApplicationVariables([])).toEqual({});
+ });
+
+ it('should include non-secret variables', () => {
+ const variables = [
+ makeFlatVariable({ key: 'PUBLIC_URL', value: 'https://example.com' }),
+ makeFlatVariable({ id: '2', key: 'DEBUG', value: 'true' }),
+ ];
+
+ expect(stripSecretFromApplicationVariables(variables)).toEqual({
+ PUBLIC_URL: 'https://example.com',
+ DEBUG: 'true',
+ });
+ });
+
+ it('should exclude secret variables', () => {
+ const variables = [
+ makeFlatVariable({ key: 'PUBLIC_URL', value: 'https://example.com' }),
+ makeFlatVariable({
+ id: '2',
+ key: 'API_SECRET',
+ value: 'encrypted_secret',
+ isSecret: true,
+ }),
+ makeFlatVariable({ id: '3', key: 'DEBUG', value: 'true' }),
+ ];
+
+ const result = stripSecretFromApplicationVariables(variables);
+
+ expect(result).toEqual({
+ PUBLIC_URL: 'https://example.com',
+ DEBUG: 'true',
+ });
+ expect(result).not.toHaveProperty('API_SECRET');
+ });
+
+ it('should handle null and undefined values', () => {
+ const variables = [
+ makeFlatVariable({
+ key: 'NULL_VALUE',
+ value: null as unknown as string,
+ }),
+ makeFlatVariable({
+ id: '2',
+ key: 'UNDEFINED_VALUE',
+ value: undefined as unknown as string,
+ }),
+ ];
+
+ expect(stripSecretFromApplicationVariables(variables)).toEqual({
+ NULL_VALUE: '',
+ UNDEFINED_VALUE: '',
+ });
+ });
+
+ it('should convert non-string values to strings', () => {
+ const variables = [
+ makeFlatVariable({
+ key: 'NUMBER_VALUE',
+ value: 123 as unknown as string,
+ }),
+ ];
+
+ expect(stripSecretFromApplicationVariables(variables)).toEqual({
+ NUMBER_VALUE: '123',
+ });
+ });
+
+ it('should return empty object when all variables are secret', () => {
+ const variables = [
+ makeFlatVariable({ key: 'SECRET_1', value: 'val1', isSecret: true }),
+ makeFlatVariable({
+ id: '2',
+ key: 'SECRET_2',
+ value: 'val2',
+ isSecret: true,
+ }),
+ ];
+
+ expect(stripSecretFromApplicationVariables(variables)).toEqual({});
+ });
+});
diff --git a/packages/twenty-server/src/engine/metadata-modules/front-component/utils/strip-secret-from-application-variables.ts b/packages/twenty-server/src/engine/metadata-modules/front-component/utils/strip-secret-from-application-variables.ts
new file mode 100644
index 0000000000..0bf9a4c607
--- /dev/null
+++ b/packages/twenty-server/src/engine/metadata-modules/front-component/utils/strip-secret-from-application-variables.ts
@@ -0,0 +1,20 @@
+import { type FlatApplicationVariable } from 'src/engine/metadata-modules/flat-application-variable/types/flat-application-variable.type';
+
+export const stripSecretFromApplicationVariables = (
+ flatApplicationVariables: FlatApplicationVariable[],
+): Record => {
+ return flatApplicationVariables.reduce>(
+ (acc, flatApplicationVariable) => {
+ if (flatApplicationVariable.isSecret) {
+ return acc;
+ }
+
+ acc[flatApplicationVariable.key] = String(
+ flatApplicationVariable.value ?? '',
+ );
+
+ return acc;
+ },
+ {},
+ );
+};
diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts
index 9d776fbf6b..579e0b0376 100644
--- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts
+++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration/workspace-migration-runner/services/workspace-migration-runner.service.ts
@@ -110,6 +110,14 @@ export class WorkspaceMigrationRunnerService {
);
}
+ if (flatMapsKeysSet.has('flatApplicationVariableMaps')) {
+ asyncOperations.push(
+ this.workspaceCacheService.invalidateAndRecompute(workspaceId, [
+ 'applicationVariableMaps',
+ ]),
+ );
+ }
+
return asyncOperations;
}