Back to Activepieces

Piece Sets

brain/knowledge/pieces-engine/piece-sets.md

0.90.18.9 KB
Original Source

Piece Sets

A named, reusable piece/action/trigger visibility configuration a platform admin defines once and assigns to many projects. Visibility is derived at read time — nothing is written when a new piece or action is installed.

Model

  • PieceSetConfig{ pieces: PieceSelection, selectedActions: Record<piece, action[]>, selectedTriggers: Record<piece, trigger[]> }.
  • PieceSelection{ mode: 'include_all' | 'exclude_all', exceptions: string[] }. include_all = everything present and future except exceptions (auto-includes new pieces); exclude_all = only exceptions, hiding future pieces.
  • Selected components — a piece key present in selectedActions/selectedTriggers means "curated": only listed components visible, new ones stay hidden. Absent key = all visible incl. future.
  • Default Set — one per platform (isDefault, key: 'default'); unassigned projects resolve to it. Can't be deleted; projects reassign to it rather than being removed.
  • Shared pure resolvers isPieceVisible / isComponentVisible live in core/shared/.../ee/piece-set/ (used by both server and web).

Entities & services

  • piece_set entity — platformId (CASCADE), name, key (embed handle, unique per platform, auto kebabCase(name)-<random>), isDefault (partial unique index), config jsonb. Projects reference it via project.pieceSetId (FK SET NULL).
  • pieceSetService — CRUD + getOrCreateDefaultPieceSet (distributed lock), duplicate, assignProject(s) / removeProjectAssignment. update runs pieceSetConfig.applyUpdate (declarative merge, never touches unreferenced component keys).
  • Routes /v1/piece-sets (platformAdminOnly). Update uses ComponentIntent: { mode: 'all' } resets a piece to all; { mode: 'selected', selected } sets the allow-list (empty array = hide all).

Gotchas

  • EE/Cloud only, gated behind platform.plan.managePiecesEnabled. On CE / flag off, piece sets are inert and filtering falls back to legacy project-plan allow/block lists.

  • The whole /v1/piece-sets module is behind that flag, GET included — so on a locked plan the web list query is enabled: false, the table is simply empty, and row actions never render. Only toolbar/entry points need a UI guard. The LockedAlert + RequestTrial featureKey="ENTERPRISE_PIECES" lives once on PlatformPiecesPage, above the tabs, since the same flag gates both the Pieces and Piece Sets tabs; the details route redirects back to the tab rather than hanging on a spinner waiting for a query that will never run.

  • There is no install-time sync and no onPieceCreated hook — resolution is purely read-time. See ADR 0001 (visibility derived, not materialized).

  • Embed auth: a v4 JWT carries a pieceSet key claim; legacy v2/v3 tokens carry piecesTags (only the first tag honored, resolved to key = tag, else Default). Enforcement (applyProjectPieceAccess) runs unconditionally, not gated by the flag.

  • usePieces({ skipProjectFilter: true }) is not a caching flag — it silently turns piece-set filtering off. It drops projectId from GET /v1/pieces, and resolveVisibility (ee/pieces/filters/piece-filtering-utils.ts) bails to null the moment either platformId or projectId is nil, so the response is the unfiltered platform catalog. platformId still comes from the principal, so this is not a tenancy hole — but any surface using it advertises pieces a restricted project's flows and MCP server will not actually expose. Correct for platform-admin screens (the piece-set editor has to list pieces you have not permitted yet) and for a marketing-style showcase; wrong anywhere the list implies "what you can use here". The absence of projectId is easy to miss at the call site because the flag reads like a client-side concern. The mirror-image trap: with the flag off, usePieces scopes to authenticationSession.getProjectId() — the session's project — so a platform-admin screen inspecting some other project (the MCP Reach tab, with its project picker) must pass projectId explicitly or it will quietly render the admin's own project's pieces under another project's name.

  • Migration is three ordered steps: create table + backfill (1807...), then CREATE INDEX CONCURRENTLY (1808..., non-transactional), then the breaking drop of legacy platform piece-filter columns (1809...). Legacy tag/piece_tag tables are kept only because the backfill reads them once via raw SQL.

  • The three GET /v1/pieces* routes are securityAccess.unscoped(ALL_PRINCIPAL_TYPES) but accept a projectId query param that picks which project's piece set filters the result — the route security does not scope it. The handlers assert membership themselves via rbacService.assertPrinicpalAccessToProject (membership only, no permission, skipped for principals with no platformId since visibility is already inert for them). Any new route that takes projectId for visibility must do the same: resolvePieceSetForProject looks the project up by id alone. That assertion carries the same two carve-outs resolveVisibility needs, both pinned by tests. It is edition-gated to EE/Cloud: projectId reaches nothing but resolveVisibility (not the search or sort path), so on CE the param is inert and an ungated membership check could only turn a working 200 into a 403/404. And it skips an empty projectId as well as a nil one, because isNil('') is false and '' would otherwise reach projectService.getOneOrThrow('') and 404 — the web can produce exactly that, since qs.stringify serializes a null projectId as projectId= (so pass getProjectId() ?? undefined, never getProjectId()!). What remains: on EE/Cloud a nonexistent or soft-deleted project id answers 404 while a real project you are not a member of answers 403, which is a project-existence oracle for any authenticated user.

  • What each principal actually gets from GET /v1/pieces?projectId= (measured on all three editions, all three routes — they never diverge). A project member of any role (VIEWER included) reads its own project and is refused a sibling with 403; a platform ADMIN or OPERATOR reads every project on its platform through the implicit role projectMemberService.getRole grants; a SERVICE api key reads every project on its own platform and is refused another platform's. WORKER, UNKNOWN and unauthenticated callers are skipped and leak nothing — with any projectId they get the unfiltered platform catalogue, exactly as if the param were absent, because resolveVisibility bails on a nil platformId. So they also never receive filtering: a piece a project's set hides is still visible to them. ONBOARDING never reaches these handlers at all (401 INVALID_BEARER at authentication), so the ONBOARDING arm of getPlatformId is dead code here. ENGINE is refused anything but its own projectId, a nonexistent id included, since that arm compares ids without a lookup — no caller does this today, but it is a trap for the first one that tries.

  • Because the guard makes these routes able to fail, any surface that puts a user-controlled projectId on them has to surface the denial. The Reach tab does not yet: a ?project= the caller cannot read answers 403 (or 404 for an unknown id) and the page renders its "No pieces are reachable in this project." empty state with no error, which reads as "this project has no pieces" rather than "you have no access" — verified against a live EE server, and not a stale bundle or a missing showErrorDialog.

  • Embed tenants are isolated from each other's piece sets: a token minted through POST /v1/managed-authn/external-token reads its own project, and is refused both a sibling project and another embed user's project with 403. That endpoint is a convenient way to get a real embed principal in a test, rather than hand-rolling one.

Key files

Entry point: pieceSetService, defined in piece-set.service.ts and wired to the /v1/piece-sets routes by piece-set.controller.ts.

  • packages/server/api/src/app/ee/pieces/piece-set/ — entity, service, controller, module, and the applyUpdate config merge
  • packages/core/shared/src/lib/ee/piece-set/ — shared models, request DTOs, and the pure isPieceVisible / isComponentVisible resolvers
  • packages/server/api/src/app/ee/pieces/filters/piece-filtering-utils.ts — applies the resolved set when filtering pieces and components
  • packages/server/api/src/app/ee/managed-authn/managed-authn-service.ts — embed token enforcement via applyProjectPieceAccess
  • packages/server/api/src/app/ee/projects/ee-project-hooks.ts — assigns the Default set on project postCreate
  • packages/web/src/features/piece-sets/ — client api and hooks
  • packages/web/src/app/routes/platform/setup/pieces/piece-sets/ — management UI, tabs and dialogs
  • brain/decisions/000007-piece-set-visibility-is-derived-at-read-time.md — why visibility is derived rather than materialized

Paths verified 2026-07-17.