FabricFabricPlatform
Platform referenceAction pipeline

Triggers — three valid entry paths

Every domain mutation enters via one of three call sites into invokeAction. There are no others.

Triggers

Every domain mutation in the platform flows through invokeAction (packages/api/modules/platform/lib/invoke-action.ts). There are exactly three valid call paths to it. Anything else is a bug.

The three paths

1. Authenticated session

A logged-in human in the SaaS UI calls platform.actions.invoke (packages/api/modules/platform/procedures/invoke-action.ts). The procedure:

  • Hard-codes actorType: "natural_person". Clients cannot override.
  • Verifies the tenant Member row.
  • Checks requiredPermissions / requiredRoles on the action.
  • Checks module entitlement.

This is the only path with a permission check. The other two are pre-authorized.

2. Verified external token

External callers (e.g. a borrower clicking an offer-acceptance link) call a public oRPC procedure that first verifies a signed token (borrowerPortal.offer.accept, borrowerPortal.offer.decline). Only after the token is verified does the procedure call invokeAction with:

invokeAction({
  actionId: "lending.accept_offer",
  actorType: "external_system",
  actorId: tokenPayload.partyId,
  parameters: { offerId, acceptanceSource: "borrower_portal_token", ... },
});

The permission check is skipped because the verified token is the proof of authorization. The signed-token verification must happen before invokeAction — the procedure cannot legitimately pass external_system without it.

3. System / scheduled / agent

Internal callers (Temporal-scheduled workflows, retry workers, the offer-expiration sweep, the MCP agent middleware) pass actorType: "system" or "agent":

invokeAction({
  actionId: "lending.expire_stale_offer",
  actorType: "system",
  actorId: "system:offer-expiration-sweep",
  parameters: { offerId },
});

The permission check is skipped because the worker is inside the trust boundary. The actorId is stamped with a stable identifier for traceability — auditors see "the offer-expiration sweep did this" rather than an opaque service account.

For agents, the upstream agentProcedure middleware validates the credential and the action-scope list before calling invokeAction. See agents within policy bounds.

Forbidden patterns

The boundary tests in packages/api/modules/{your-application,borrower-portal}/boundary.test.ts enforce these prohibitions:

  1. No direct handler imports. import { acceptOfferHandler } from "@repo/lending/actions/handlers/accept-offer" from a procedure is rejected. The runtime is the only legal way to invoke a handler.
  2. No domain mutations outside invokeAction. No db.X.create | update | delete | upsert in API procedures or UI code. Read queries (findFirst, findMany) are fine.
  3. No external_system without a verified token. Any caller passing external_system must first verify a signed proof. Public callers without a verified token must use the authenticated path.

The seed scripts in tooling/scripts/ are the only documented exception — they bypass the runtime by design for fixture creation.

Dispatching is asynchronous

invokeAction returns immediately with { status: "pending", actionInvocationId, workflowId }. The work runs in the worker (Temporal). For example, this means returning a "Processing…" interstitial and polling borrowerPortal.offer.get until the offer reaches its terminal state.

See also

On this page