Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 3s
150 lines
8.4 KiB
Markdown
150 lines
8.4 KiB
Markdown
# Architecture — twenty-partners
|
|
|
|
Vertical-slice modules, one folder per domain. Adapted from `twentyhq/twenty-eng`
|
|
(the largest Twenty SDK app), which migrated flat → modular as it grew. Repo-wide
|
|
conventions (kebab-case files, named exports, no `any`, `types` over `interface`,
|
|
short `//` comments) live in the root `CLAUDE.md` and still apply — this file only
|
|
adds the partners-specific structure.
|
|
|
|
> **Status:** migration complete — every domain lives under `src/modules/`. The only
|
|
> top-level `src/` entries left are `constants/universal-identifiers.ts`, `scripts/`,
|
|
> `roles/`, `skills/`, `workflows/`, `__tests__/`, and the two root config files. Add new
|
|
> code to a domain module, never a flat top-level `src/<primitive>/` folder.
|
|
|
|
## Layout
|
|
|
|
```
|
|
src/
|
|
application-config.ts # app + application variables — unchanged
|
|
default-role.ts # unchanged
|
|
constants/universal-identifiers.ts # ⚠️ MUST NOT MOVE (see Invariants)
|
|
scripts/ # ⚠️ MUST NOT MOVE — seed/purge/rls/slugify
|
|
roles/ # app-wide RLS — stays at root (spans every object)
|
|
skills/ # bundled Claude skills — unchanged
|
|
workflows/ # bundled workflows — unchanged
|
|
__tests__/helpers/ # shared test setup (client, fixtures, cleanup)
|
|
modules/
|
|
shared/ # cross-cutting: secret-guard (http/), find-or-create, paginate,
|
|
# utils, front-components, cross-domain nav folders
|
|
<domain>/ # partner · application · opportunity · … (large domains add a <feature>/ tier)
|
|
<name>.logic-function.ts # thin SDK entrypoints — sit at the domain/feature root, NOT in a subfolder
|
|
objects/ fields/ view-fields/ views/ navigation-menu-items/ page-layouts/ # declarations
|
|
constants/ # value lists + declaration UUID maps (NOT types)
|
|
services/ # business logic (testable)
|
|
graphql/{queries,mutations}/ # the only place raw queries/mutations live
|
|
connector/ # OUTBOUND third-party API per folder (<name>.connector.ts · config · types)
|
|
mappers/ utils/ types/ front-components/
|
|
```
|
|
|
|
A domain is a self-contained slice: its declarations (`objects`, `fields`, `views`, …)
|
|
**and** its logic (`logic-functions` → `services` → `graphql`/`connector`) live together.
|
|
Adding a feature means adding a folder, not scattering files across ten flat piles.
|
|
|
|
**Feature tier for large domains:** when a domain holds several distinct features, insert a
|
|
`<feature>/` level — `modules/<domain>/<feature>/<primitive>/` (the twenty-eng standard,
|
|
e.g. `modules/code-build/build-task/services/`). It keeps `graphql/` and `services/` from
|
|
becoming flat piles. Partner uses `partner/{self-service,marketplace,application-intake,
|
|
directory}/…`; opportunity uses `opportunity/{intake,matching}/…`. A small single-feature
|
|
domain (application) may keep `<domain>/<primitive>/` directly.
|
|
|
|
## The dependency rule (the one that matters — never import upward)
|
|
|
|
```
|
|
logic-functions → services → { graphql, connector, mappers } → shared · utils · types
|
|
```
|
|
|
|
- **Entrypoints (`*.logic-function.ts`, at the domain/feature root)** — SDK entrypoint
|
|
ONLY. Parse input, call one service, return. No business logic, no raw GraphQL, no
|
|
external HTTP. >~40 lines means it's doing a service's job — extract it.
|
|
- **`services/`** — all business logic. Testable, no SDK/transport coupling. Must **not**
|
|
import a `logic-function`.
|
|
- **`graphql/`** — the ONLY place raw queries/mutations live (named typed operations,
|
|
not inline `client.query(...)`).
|
|
- **`connector/`** — the ONLY place **outbound** third-party APIs are called; one folder
|
|
per API (`<name>.connector.ts` / `config.ts` / `types.ts`). **Inbound webhooks are NOT connectors** —
|
|
a webhook the app *receives* is an ordinary `logic-functions/` entrypoint guarded by the
|
|
shared secret-guard util. (Today the one real connector is Discord; TFT/client-brief/
|
|
partner-application are inbound.)
|
|
- Never import **logic** sideways between domains — share via `modules/shared/`. (A relation
|
|
field naturally referencing its target object's ID constant is a schema reference, not a
|
|
logic dependency — that cross-module import is allowed.)
|
|
|
|
## Naming
|
|
|
|
`<name>.<primitive>.ts` — e.g. `submit-application.logic-function.ts`,
|
|
`resolve-candidacy.service.ts`, `partner.object.ts`. Files are kebab-case (no PascalCase,
|
|
even for React components: `profile-picture-upload.tsx`).
|
|
|
|
Tests co-locate with their subject and split by kind: `<name>.test.ts` = unit (mocked
|
|
client, fast, no infra); `<name>.integration-test.ts` = real workspace (needs a live
|
|
server + global setup; excluded by tsconfig). The split is a selectable vitest **project**
|
|
(`--project unit`), not a file-count mandate — one `vitest.config.ts` with two projects is
|
|
preferred. Don't rename a mocked unit test to `.integration-test.ts`. Shared test setup
|
|
lives in `src/__tests__/helpers/`.
|
|
|
|
## Invariants (moving code must not break these)
|
|
|
|
- The set of `universalIdentifier` UUIDs must stay **byte-identical** across a move —
|
|
the SDK tracks primitives by identifier, not path, so a pure `git mv` is safe but a
|
|
changed/dropped UUID re-registers or orphans an object.
|
|
- **Do not move** `src/constants/universal-identifiers.ts` — the env-toolkit rewrites it
|
|
by path per bundle.
|
|
- **Do not move** `src/scripts/*` — `package.json` invokes them by path.
|
|
- **Keep importers of `src/constants/universal-identifiers.ts` pointing at the root file** —
|
|
don't relocate its UUID exports into a module.
|
|
- **Never split a relation across domains with a dangling ID import.** Paired relation
|
|
fields export/import each other's field-ID constant; keep both sides of a relation in one
|
|
domain, or hoist the shared ID into `shared/`.
|
|
- One domain per commit when migrating (`git mv` only, imports fixed in the same commit).
|
|
|
|
## Application review playbook
|
|
|
|
The Matching Admin Workspace playbook is a standalone page in
|
|
`src/modules/partner/application-intake/` (front-component + page layout +
|
|
nav item). It must stay in lockstep with real behaviour. When you change any
|
|
of the following, update the playbook in the same change:
|
|
|
|
- Website form fields that land on Partner
|
|
- The Partner Applications view (filter, columns, sort)
|
|
- `validationStage` values used as the review decision
|
|
- Intake side effects the reviewer needs to know (Discord on create;
|
|
welcome email on `VALIDATED`, linked from
|
|
`WELCOME_EMAIL_WORKFLOW_URL`)
|
|
- Marketplace ranking bonuses in
|
|
`packages/twenty-website/src/partners-marketplace/completeness-score.ts`
|
|
(case study, cover, introduction, service, picture, calendar, rate,
|
|
category, ghost rule)
|
|
|
|
Facts on the page come from the same constants the view and intake use. Do
|
|
not leave stale steps. Do not keep a second copy on twenty.com or in a Note.
|
|
|
|
## Opportunity process playbooks
|
|
|
|
How to process (Matching Admin) and How to apply (Partner Workspace) are
|
|
standalone pages under `src/modules/opportunity/how-to-process/`. They must
|
|
stay in lockstep with real operator behaviour. Update the matching page and
|
|
its copy test in the same change when you change any of:
|
|
|
|
- Intake (marketplace brief already Listed + Discord; Twenty Internal
|
|
unlisted; `/twenty-lead-brief` writes Design Doc URL)
|
|
- Listing (`Listed` flip → Discord; daily digest; Open Briefs membership)
|
|
- Intro (`Intro Sent At` turns Listed off; Invited applications)
|
|
- Win (set Partner → Won / Declined; Backup stays; unassign reopens Won
|
|
to Applied; Deals stages New → Screening → Meeting → Proposal → Customer)
|
|
- Apply (Open Briefs listed-only; pitch minimum; one pitch; state meaning)
|
|
- Bundled skills (`twenty-lead-brief`, `twenty-partner-shortlist`,
|
|
`twenty-partner-intro`): local run, expected output, GitHub URL of
|
|
`SKILL.md`
|
|
- Match via MCP: the operator can ask the agent to write CRM changes
|
|
|
|
Pages stay operator language. Skill names, slash triggers, and GitHub URLs
|
|
of those bundled skills are allowed on How to process. Open Briefs and
|
|
My Applications links are required on How to apply. Do not open Apply
|
|
from the standalone page — Apply is pinned on the brief record.
|
|
|
|
## Where does new code go?
|
|
|
|
External API call → `connector/`. Raw query/mutation → `graphql/`. Any logic →
|
|
`services/`. New SDK trigger/function → a thin `*.logic-function.ts` at the domain/feature
|
|
root that calls a service. Shared across domains → `modules/shared/`.
|