## Summary
Logic-function bundles produced by the SDK CLI were ~1.2 MB each (source
maps ~3.1 MB) because esbuild was inlining `twenty-sdk/define` and its
transitive dependencies (zod + locales, twenty-shared, etc.). Those
`define*` factories are pure build-time metadata used only by the
manifest extractor — the Lambda runtime only ever invokes
`default.config.handler`, so the factories are dead weight at runtime.
This PR shrinks the bundles to ~9.5 KB each (~99% reduction) without
changing runtime behaviour.
## What changes
- **Stub `twenty-sdk/define` at user-app build time.** New esbuild
plugin
(`packages/twenty-sdk/src/cli/utilities/build/common/plugins/stub-twenty-sdk-define.plugin.ts`)
intercepts every import of `twenty-sdk/define` during user-app builds
and replaces it with a tiny virtual module:
- Factory functions (`defineLogicFunction`,
`definePostInstallLogicFunction`, …) become `(config) => ({ success:
true, config, errors: [] })`.
- Enums and helpers become `Proxy`-based no-ops.
- Wired into both the one-shot build (`build-application.ts`) and the
watcher (`esbuild-watcher.ts`), for logic functions and front
components.
- **New runtime barrel `twenty-sdk/logic-function`.** Re-exports only
the types logic-function authors need (`InstallPayload`, `RoutePayload`,
`CronPayload`, `DatabaseEventPayload`, `LogicFunctionConfig`,
`InputJsonSchema`, …). Compiled `.mjs` is 36 bytes. Wired into Vite,
Rollup `.d.ts` bundling, `package.json#exports`, and `typesVersions`.
- **Lint enforcement.** Added an oxlint `no-restricted-imports` rule
that forbids `twenty-shared` / `twenty-shared/*` imports from
`**/*.logic-function.ts` and `**/logic-functions/**/*.ts`, with a help
message pointing at the new barrel. Applied to the `create-twenty-app`
template and to `github-connector`, `hello-world`, `postcard`.
- **Migrated existing sources.** All logic-function files across
`community/{github-connector, apollo-enrich}`, `examples/{hello-world,
postcard}`, and `internal/{twenty-for-twenty, self-hosting, exa}` now
import types from `twenty-sdk/logic-function` instead of
`twenty-sdk/define` or `twenty-shared/*`. Renamed leftover
`InstallLogicFunctionPayload` references to `InstallPayload`.
## Why this is safe
- `define*` exports from `twenty-sdk/define` are metadata factories
whose call expressions are statically inspected by the manifest
extractor (`manifest-extract-config.ts`). They're never evaluated at
runtime — the Lambda executor only walks `default.config.handler`
(`logic-function-drivers/constants/executor/index.mjs`).
- The stub keeps the same call shape (`{ success, config, errors }`), so
any logic-function module that re-exports
`defineX(config).config.handler` still resolves to the user's handler at
runtime.
- Front-component bundles are unaffected by the stub because the
pre-existing JSX transform plugin
(`jsx-transform-to-remote-dom-worker-format-plugin.ts`) unwraps
`defineFrontComponent(...)` earlier in the pipeline. That's intentional
— front-component bloat is React/Preact, not in scope here.
## Measurements (github-connector)
| Asset | Before | After |
|---|---|---|
| `*.logic-function.mjs` | ~1.2 MB | ~9.5 KB |
| `*.logic-function.mjs.map` | ~3.1 MB | ~22 KB |
This is a Twenty application bootstrapped with create-twenty-app.
Overview
Twenty for Twenty is the official internal Twenty app. It is organized into modules, each integrating a third-party service with Twenty.
Resend module (src/modules/resend/)
Two-way sync between Twenty and the Resend email platform. The module syncs contacts, segments, templates, broadcasts, and emails.
Inbound (Resend -> Twenty):
- A cron job runs every 5 minutes to pull all entities from the Resend API
- A webhook endpoint receives real-time events for contacts and emails
Outbound (Twenty -> Resend):
- Database event triggers push contact and segment changes back to Resend when records are created, updated, or deleted in Twenty
Getting Started
1. Install and run the app
yarn twenty dev
This registers the app with your local Twenty instance at http://localhost:3000/settings/applications.
2. Configure app variables
In Twenty, go to Settings > Applications > Twenty for Twenty and set:
- RESEND_API_KEY -- Your Resend API key. Create one at https://resend.com/api-keys (full access recommended).
- RESEND_WEBHOOK_SECRET -- The signing secret for verifying inbound webhooks (see "Webhook setup" below).
3. Webhook setup
The app exposes an HTTP endpoint at /s/webhook/resend that receives Resend webhook events. To connect it:
- Go to https://resend.com/webhooks
- Click Add webhook
- Set the Endpoint URL to your Twenty server's public URL +
/s/webhook/resend(e.g.https://your-domain.com/s/webhook/resend) - Set Events types to All Events
- Click Add
- Copy the signing secret Resend displays and paste it into the
RESEND_WEBHOOK_SECRETapp variable in Twenty
The webhook handles:
- Contact events (
contact.created,contact.updated,contact.deleted) -- upserts/deletes Resend contact records in Twenty - Email events (
email.sent,email.delivered,email.bounced,email.opened,email.clicked, etc.) -- updates delivery status on Resend email records in real-time - Domain events -- logged and skipped (no domain object in the app yet)
4. Testing webhooks locally
Install the Resend CLI:
brew install resend/cli/resend
Or via npm if Homebrew has issues:
npm install -g resend-cli
Authenticate:
resend login
Start the webhook listener with forwarding to your local Twenty server:
resend webhooks listen --forward-to http://localhost:3000/s/webhook/resend
The CLI will:
- Create a public tunnel automatically
- Register a temporary webhook in Resend pointing to that tunnel
- Forward incoming events (with Svix signature headers) to your local Twenty server
- Display events in the terminal as they arrive
- Clean up the temporary webhook when you press Ctrl+C
To trigger test events, create or update a contact in the Resend dashboard, or send a test email.
Sync behavior
Inbound sync
| Source | Mechanism | Entities |
|---|---|---|
| Cron (every 5 min) | Polls Resend API, upserts into Twenty | Contacts, segments, templates, broadcasts, emails |
| Webhook (real-time) | Receives Resend events via HTTP | Contacts, emails |
Outbound sync
| Twenty action | Resend API call |
|---|---|
| Create contact | contacts.create() -- writes resendId back to Twenty |
| Update contact (name, email, unsubscribed) | contacts.update() |
| Delete contact | contacts.remove() |
| Create segment | segments.create() -- writes resendId back to Twenty |
| Delete segment | segments.remove() |
Loop prevention
A lastSyncedFromResend field on contact, segment, and email records tracks when data came from Resend. Outbound triggers skip processing when this field is part of the update, preventing infinite echo loops between inbound and outbound sync.
Commands
Run yarn twenty help to list all available commands.