feat(server): run server-exposed logic functions in the owner workspace (#22002)

## Summary

Implements the server-level logic-function tier in the simplest shape: a
logic function is "server-exposed" iff its manifest entry carries
`serverWebhookTriggerSettings`. Execution delegates to the
owner-workspace copy of that function — billing, throttling, env vars,
and the existing executor all apply uniformly against that workspace.

Supersedes #21971 with the simplified design from that discussion (no
`applicationRegistrationLogicFunction` registry, no dedicated manifest
type, no separate SDK helper, no special throttling).

## Design

- **Manifest**: `LogicFunctionManifest` gains
`serverWebhookTriggerSettings?`. The declarative `workspaceIdResolver`
shape is dropped.
- **Materialization**: those settings become two new jsonb columns on
`LogicFunctionEntity`. The manifest → flat converter and the
create-from-source DTO/util forward them; the property-config map and
editable-properties list are extended.
- **Lookup**: a single QB query joins `logicFunction → application →
applicationRegistration` and filters on `lf.workspaceId =
reg.workspaceId` to get only the owner workspace's copy.
- **Webhook**: `POST /webhooks/server/:logicFunctionUniversalIdentifier`
→ `ServerWebhookTriggerService.handle` → join lookup →
`LogicFunctionTriggerService.run`. No registry table, no
`:applicationRegistrationUniversalIdentifier` segment, no resolver.
- **Gate**: `IS_SERVER_LOGIC_FUNCTION_ENABLED` config var (disabled by
default).

## Test plan
- [x] `npx jest server-webhook-trigger` — 9 unit tests across the
webhook service.
- [x] `npx jest logic-function` — 88 existing tests stay green.
- [x] `npx nx typecheck twenty-server`.
- [x] `npx nx lint:diff-with-main twenty-server`.
- [x] Reset DB → init → run `database:migrate:prod` → run
`database:migrate:generate --name pending-migration-check` → no drift.
- [ ] Manual: hit `/webhooks/server/<uid>` end-to-end against a manifest
carrying `serverWebhookTriggerSettings`.

https://claude.ai/code/session_01GgsnCGmYJ26xRirx8va1Yh

---
_Generated by [Claude
Code](https://claude.ai/code/session_01GgsnCGmYJ26xRirx8va1Yh)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22002?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
martmull
2026-06-24 15:34:12 +02:00
committed by GitHub
parent 20ac0a52bf
commit b5a1aed24b
36 changed files with 1231 additions and 1061 deletions
@@ -3114,13 +3114,6 @@ type Query {
getViewGroup(id: String!): ViewGroup
myMessageFolders(messageChannelId: UUID): [MessageFolder!]!
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
findApplicationRegistrationByClientId(clientId: String!): PublicApplicationRegistration
findApplicationRegistrationByUniversalIdentifier(universalIdentifier: String!): ApplicationRegistration
findManyApplicationRegistrations: [ApplicationRegistration!]!
findOneApplicationRegistration(id: String!): ApplicationRegistration!
findApplicationRegistrationStats(id: String!): ApplicationRegistrationStats!
findApplicationRegistrationVariables(applicationRegistrationId: String!): [ApplicationRegistrationVariableDTO!]!
applicationRegistrationTarballUrl(id: String!): String
minimalMetadata: MinimalMetadata!
appConnections(filter: ListAppConnectionsInput): [AppConnection!]!
appConnection(id: ID!): AppConnection!
@@ -3139,6 +3132,13 @@ type Query {
checkWorkspaceSubdomainAvailability(subdomain: String!): SubdomainAvailabilityDTO!
getWorkspaceCreationDefaults: WorkspaceCreationDefaultsDTO!
validatePasswordResetToken(passwordResetToken: String!): ValidatePasswordResetToken!
findApplicationRegistrationByClientId(clientId: String!): PublicApplicationRegistration
findApplicationRegistrationByUniversalIdentifier(universalIdentifier: String!): ApplicationRegistration
findManyApplicationRegistrations: [ApplicationRegistration!]!
findOneApplicationRegistration(id: String!): ApplicationRegistration!
findApplicationRegistrationStats(id: String!): ApplicationRegistrationStats!
findApplicationRegistrationVariables(applicationRegistrationId: String!): [ApplicationRegistrationVariableDTO!]!
applicationRegistrationTarballUrl(id: String!): String
currentUser: User!
currentWorkspace: Workspace!
getPublicWorkspaceDataByDomain(origin: String): PublicWorkspaceData!
@@ -3381,15 +3381,6 @@ type Mutation {
updateMessageFolder(input: UpdateMessageFolderInput!): MessageFolder!
updateMessageFolders(input: UpdateMessageFoldersInput!): [MessageFolder!]!
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
createApplicationRegistration(input: CreateApplicationRegistrationInput!): CreateApplicationRegistration!
updateApplicationRegistration(input: UpdateApplicationRegistrationInput!): ApplicationRegistration!
deleteApplicationRegistration(id: String!): Boolean!
rotateApplicationRegistrationClientSecret(id: String!): RotateClientSecret!
createApplicationRegistrationVariable(input: CreateApplicationRegistrationVariableInput!): ApplicationRegistrationVariable!
updateApplicationRegistrationVariable(input: UpdateApplicationRegistrationVariableInput!): ApplicationRegistrationVariable!
deleteApplicationRegistrationVariable(id: String!): Boolean!
uploadAppTarball(file: Upload!, universalIdentifier: String): ApplicationRegistration!
transferApplicationRegistrationOwnership(applicationRegistrationId: String!, targetWorkspaceSubdomain: String!): ApplicationRegistration!
createChatThread: AgentChatThread!
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileAttachments: [FileAttachmentInput!]): SendChatMessageResult!
stopAgentChatStream(threadId: UUID!): Boolean!
@@ -3423,6 +3414,15 @@ type Mutation {
generatePlaygroundToken: AuthToken!
emailPasswordResetLink(email: String!, workspaceId: UUID): EmailPasswordResetLink!
updatePasswordViaResetToken(passwordResetToken: String!, newPassword: String!): InvalidatePassword!
createApplicationRegistration(input: CreateApplicationRegistrationInput!): CreateApplicationRegistration!
updateApplicationRegistration(input: UpdateApplicationRegistrationInput!): ApplicationRegistration!
deleteApplicationRegistration(id: String!): Boolean!
rotateApplicationRegistrationClientSecret(id: String!): RotateClientSecret!
createApplicationRegistrationVariable(input: CreateApplicationRegistrationVariableInput!): ApplicationRegistrationVariable!
updateApplicationRegistrationVariable(input: UpdateApplicationRegistrationVariableInput!): ApplicationRegistrationVariable!
deleteApplicationRegistrationVariable(id: String!): Boolean!
uploadAppTarball(file: Upload!, universalIdentifier: String): ApplicationRegistration!
transferApplicationRegistrationOwnership(applicationRegistrationId: String!, targetWorkspaceSubdomain: String!): ApplicationRegistration!
initiateOTPProvisioning(loginToken: String!, origin: String!): InitiateTwoFactorAuthenticationProvisioning!
initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioning!
deleteTwoFactorAuthenticationMethod(twoFactorAuthenticationMethodId: UUID!): DeleteTwoFactorAuthenticationMethod!
@@ -4023,6 +4023,7 @@ input CreateLogicFunctionFromSourceInput {
cronTriggerSettings: JSON
databaseEventTriggerSettings: JSON
httpRouteTriggerSettings: JSON
serverRouteTriggerSettings: JSON
toolTriggerSettings: JSON
workflowActionTriggerSettings: JSON
}
@@ -4409,6 +4410,40 @@ input UpdateCalendarChannelInputUpdates {
isSyncEnabled: Boolean
}
input FileAttachmentInput {
id: UUID!
filename: String!
}
input CreateSkillInput {
id: UUID
name: String!
label: String!
icon: String
description: String
content: String!
}
input UpdateSkillInput {
id: UUID!
name: String
label: String
icon: String
description: String
content: String
isActive: Boolean
}
input GetAuthorizationUrlForSSOInput {
identityProviderId: UUID!
workspaceInviteHash: String
}
input SignUpInNewWorkspaceInput {
displayName: String
subdomain: String
}
input CreateApplicationRegistrationInput {
name: String!
universalIdentifier: String
@@ -4447,40 +4482,6 @@ input UpdateApplicationRegistrationVariablePayload {
description: String
}
input FileAttachmentInput {
id: UUID!
filename: String!
}
input CreateSkillInput {
id: UUID
name: String!
label: String!
icon: String
description: String
content: String!
}
input UpdateSkillInput {
id: UUID!
name: String
label: String
icon: String
description: String
content: String
isActive: Boolean
}
input GetAuthorizationUrlForSSOInput {
identityProviderId: UUID!
workspaceInviteHash: String
}
input SignUpInNewWorkspaceInput {
displayName: String
subdomain: String
}
input UpdateWorkspaceMemberSettingsInput {
workspaceMemberId: UUID!
update: JSON!