WU / DOCSUNIVERSAL MICROFRONTENDS
EN

A service connected through WU

Publish a contract, authorize a client, and follow RPC requests through versioning, validation, cancellation, and cleanup.

From client to response

If this is your first RPC integration, start with Adding numbers between applications. That recipe includes every file for your workspace. This chapter extends the pattern with contract discovery, cancellation, and the laboratory’s runnable service.

In Services and RPC, two WU applications collaborate with different permissions. The provider publishes the service it offers and handles a channel; the client consumes that contract and requests a response. The walkthrough lets you test valid data, errors, version incompatibility, cancellation, and provider removal.

Cliente WU
  → consume('docs:backend:service', '^1.0.0')
  → request('docs:backend:request', solicitud)
  → handler del proveedor WU
  → respuesta { status, headers, body, execution }

Requests travel through WU RPC within the runtime. status, headers, and body are members of the response contract; they do not mean the portal has opened an HTTP connection or a server port.

Request and grant the required authority

Each manifest declares the capabilities its app requests. The provider needs rpc.provide:docs:backend:*; the client needs rpc.call:docs:backend:*. The shell grants those scopes when initializing both applications:

await wu.init({
  sandbox: 'module',
  apps: [
    {
      name: 'backend-service',
      url: '/microfrontends/backend-service',
      grants: ['rpc.provide:docs:backend:*'],
    },
    {
      name: 'backend-client',
      url: '/microfrontends/backend-client',
      grants: ['rpc.call:docs:backend:*'],
    },
  ],
});

Manifest requests and shell grants must match. Effective permission comes from their intersection. App code uses context.wu, the scoped facade received in mount; it does not need to capture the host’s privileged singleton. The permissions reference explains scopes and revocation.

Publish the channel and contract

Within the provider’s mount, appWu is that scoped facade and handleRequest implements the service:

const release = appWu.handle(
  'docs:backend:request',
  handleRequest,
  { version: '1.0.0', allow: ['backend-client'] },
);

const contract = appWu.provide(
  'docs:backend:service',
  { channel: 'docs:backend:request', method: 'POST' },
  {
    version: '1.0.0',
    shape: { channel: 'string', method: 'string' },
  },
);

handle registers who responds on the channel. provide publishes the data the consumer needs to discover it. The handler’s allowlist limits callers to backend-client; having a grant does not remove this additional check. shape checks contract members and their types, while body validation belongs to the service.

In WU Framework 4.0.0, wu.handle(channel, handler, options) and wu.eventBus.handle(channel, handler, options) preserve version and allowlist. Inside a microapp use context.wu.handle: in addition to those options, it applies the app identity and grants. See the versions page to identify the laboratory runtime.

The name published with provide belongs to the first provider. Another app, or the host attempting to replace it, receives WU_CAPABILITY_OWNED until the owner calls revoke or is unmounted with unmount. The contract version does not bypass that ownership.

Consume and call from the client

The client obtains a contract proxy and requests a compatible range. Accessing api.channel resolves the available provider and checks that range:

const api = appWu.consume('docs:backend:service', '^1.0.0');
const channel = api.channel;
const controller = new AbortController();

const pending = appWu.request(channel, {
  method: api.method,
  path: '/validate',
  body: JSON.stringify({ name: 'catalog', entry: 'entry.js' }),
}, {
  version: '^1.0.0',
  timeout: 3500,
  signal: controller.signal,
});

const response = await pending;
console.log(response.status, response.body);

There are two compatibility checks: one for the consumed contract and one for the RPC handler. The incompatible-version scenario requests ^2.0.0 from the 1.0.0 contract; resolution fails before any service work starts. A removed provider cannot handle a new request either. See events, RPC, and contracts for their signatures and errors.

Distinguish a response from a rejection

The service expects POST, a known route, and a string body of at most 4096 UTF-8 bytes.

Request Service response
/validate with valid JSON and both name/entry present 200 with ok: true
/validate with a manifest that does not meet those rules 422 with validation errors
/uppercase 200 with the ASCII text converted to uppercase
/failure 500 for the service’s controlled failure
/slow without cancellation 504 when the 2500 ms work limit expires
Unknown route 404 without starting work
Method other than POST 405
Invalid request structure 400
Body exceeds the limit 413

These responses resolve the RPC promise with an object: the client must check response.status. Contract incompatibility, a missing provider, or cancellation are handled as rejections using try/catch.

The manifest recipe validates only name and entry. Its result declares scope: "name+entry" and completeValidation: false; it does not check the entire manifest, permissions, published routes, or whether the entry exists. See configuration and manifests and WU doctor to continue diagnosing a project.

Cancel and unmount without leaving work active

While the request remains pending, the client can run:

controller.abort();

The handler receives the RPC context, which includes signal. In this example, the provider connects that signal to interruption of its executor. A handler that ignores signal can keep working after the client stops waiting.

The 3500 ms RPC timeout and the 2500 ms work limit have different effects: the first limits how long WU waits; the second lets the service stop work and return 504. The interface includes a slow recipe to observe cancellation before either deadline.

When the provider unmounts, the implementation stops its active requests and removes what it published:

backend.dispose();
release();
contract.revoke();

WU also revokes the authority associated with the app. Mounting again registers a new handler and contract. Each request releases its abort listener, timer, and executor on completion, failure, or cancellation. The lifecycle guide explains how to connect that resource ownership to mount and unmount.

Read and verify the example

The provider code shows request handling and cleanup. To run the checks from the portal repository:

npm ci
npm run build
npm run dev:shell
# En otra terminal:
npm run test:backend

The test covers responses, incompatible versions, revocation, remounting, timeout, cancellation, and unmounting during a request. It also verifies that no Workers remain after execution and that the walkthrough produces no page errors.

Example implementation

The provider uses a compiled WASI program included in the project, running in a Worker. This lets it process real data and stop work from the browser. The host and virtual files belong to the service implementation; the public contract studied here is WU. The framework does not include a general-purpose WASI engine.

You can replace that implementation with an authorized service from your application while keeping a compatible contract and testing its responses, errors, and cleanup. A remote service must validate identity, permissions, and data again. Rendering pages in Node or connecting an external MCP client still requires their processes and transports: see SSR and AI/MCP.

BUILD WU WITH US

Found a bug? Have an idea?

Your experience improves WU. Tell us what went wrong or help support its development.