# A sum between two microapps

Create an RPC client and provider with WU CLI and Astro: complete files, permissions, versions, errors, and verifiable unmounting.

<span id="lo-que-vas-a-construir"></span>

<span class="section-anchor" id="section-1"></span>

## What you will build

A form sends `7` and `8` to another microapp, which validates the numbers and returns `15`. The client displays **7 + 8 = 15**. Then you will stop the service, check the error, and start it again.

Continue in the `mi-plataforma` workspace from [Adding a second app](/docs/guides/add-an-app/). You can also apply this recipe after the [shared React and Vue counter](/docs/guides/shared-counter/): both apps retain their components. Here we add two small JavaScript apps to focus on RPC calls.

```text
Shell Astro: concede permisos y monta las dos apps
  cliente-suma → request('ejemplo:suma', { a: 7, b: 8 })
    sumador → handle('ejemplo:suma', ...) → 15
  cliente-suma → muestra «7 + 8 = 15»
```

The calculation happens in the browser. WU coordinates the request between apps; this recipe does not open an HTTP API. To study a provider with more operations and a contract discovered through `provide`/`consume`, continue afterward with [A WU-connected service](/docs/guides/browser-backend/).

**Recipe versions:** Node.js 24, WU CLI 1.2.0, and `wu-framework` 4.0.0. Use the CLI installed in [Your first microapp](/docs/guides/quickstart/).

<span id="1-añadir-el-cliente-y-el-proveedor"></span>

<span class="section-anchor" id="section-2"></span>

## 1. Add the client and provider

Stop the server with **Ctrl+C**. From the `mi-plataforma/` root beside `wu.config.json`, run:

```sh
wu add vanilla sumador --no-install
wu add vanilla cliente-suma --no-install
```

The order is framework, name. The CLI creates `mf-sumador/` and `mf-cliente-suma/`, with dependencies and HTML entrypoints, and registers both apps.

[Download all recipe files](/examples/wu-rpc-sum.zip). Copy the ZIP's three folders into the workspace, merge existing folders, and replace files with the same names. Also copy `wu.config.json`. Replace each app's `index.html` too: it must load `src/main.js`, this recipe's JavaScript entrypoint. Keep the remaining workspace files.

```text
mi-plataforma/
├── wu.config.json
├── mf-sumador/
│   ├── index.html
│   ├── package.json
│   ├── vite.config.js
│   ├── styles.css
│   ├── wu.json
│   └── src/main.js
├── mf-cliente-suma/
│   ├── index.html
│   ├── package.json
│   ├── vite.config.js
│   ├── styles.css
│   ├── wu.json
│   └── src/main.js
└── shell/
    ├── astro.config.mjs
    └── src/pages/suma.astro
```

The `suma.astro` page replaces the earlier host-only sum example if you created it. Workspace and counter pages remain available. If you customized your workspace, merge the following new entries and grants while preserving your other settings.

You can also copy the complete files below.

<span id="2-solicitar-permisos-y-concederlos"></span>

<span class="section-anchor" id="section-3"></span>

## 2. Request permissions and grant them

The provider requests `rpc.provide:ejemplo:suma`; the client requests `rpc.call:ejemplo:suma`. `required: true` makes initialization fail with diagnostics if the host does not grant that permission. The manifest describes the need; host `grants` supply authorization.
<span class="section-anchor" id="section-4"></span>

### `mf-sumador/wu.json`

[View complete file](/examples/browser/rpc-sum/mf-sumador/wu.json.txt)

```json
{
  "name": "sumador",
  "version": "1.0.0",
  "entry": "src/main.js",
  "styleMode": "isolated",
  "wu": {
    "permissions": [
      { "capability": "rpc.provide", "resource": "ejemplo:suma", "required": true }
    ]
  }
}
```

<span class="section-anchor" id="section-5"></span>

### `mf-cliente-suma/wu.json`

[View complete file](/examples/browser/rpc-sum/mf-cliente-suma/wu.json.txt)

```json
{
  "name": "cliente-suma",
  "version": "1.0.0",
  "entry": "src/main.js",
  "styleMode": "isolated",
  "wu": {
    "permissions": [
      { "capability": "rpc.call", "resource": "ejemplo:suma", "required": true }
    ]
  }
}
```

This is the complete workspace configuration after adding both apps. Counter grants preserve the `demo.counter` scope. The two new grants allow summation from generated-shell navigation.
<span class="section-anchor" id="section-6"></span>

### `wu.config.json`

[View complete file](/examples/browser/rpc-sum/wu.config.json.txt)

```json
{
  "name": "mi-plataforma",
  "version": "1.1.0",
  "shell": {
    "dir": "shell",
    "port": 4321,
    "framework": "astro",
    "dev_cmd": "npx astro dev",
    "build_cmd": "npx astro build"
  },
  "apps": [
    {
      "name": "mi-plataforma",
      "dir": "mf-mi-plataforma",
      "framework": "react",
      "port": 5001,
      "grants": [
        "store.read:demo.counter",
        "store.write:demo.counter"
      ]
    },
    {
      "name": "inspector",
      "dir": "mf-inspector",
      "framework": "vue",
      "port": 5002,
      "grants": [
        "store.read:demo.counter",
        "store.write:demo.counter"
      ]
    },
    {
      "name": "sumador",
      "dir": "mf-sumador",
      "framework": "vanilla",
      "port": 5003,
      "grants": [
        "rpc.provide:ejemplo:suma"
      ]
    },
    {
      "name": "cliente-suma",
      "dir": "mf-cliente-suma",
      "framework": "vanilla",
      "port": 5004,
      "grants": [
        "rpc.call:ejemplo:suma"
      ]
    }
  ],
  "proxy": {
    "port": 3000,
    "open_browser": true
  }
}
```

<span id="3-implementar-el-proveedor"></span>

<span class="section-anchor" id="section-7"></span>

## 3. Implement the provider

`wu.define` registers the `sumador` lifecycle. WU supplies `context.wu` on mount: that facade carries this app's identity and permissions. The handler validates input, calculates the result, and shows which client called it.

`version: '1.0.0'` is the operation version. `allow: ['cliente-suma']` adds an identity check: only that client can invoke it, even if another app has a grant for the same channel. The function returned by `handle` removes the handler; we execute it in `unmount`.
<span class="section-anchor" id="section-8"></span>

### `mf-sumador/src/main.js`

[View complete file](/examples/browser/rpc-sum/mf-sumador/src/main.js.txt)

```js
import { waitForWu } from 'wu-framework/adapters/shared';

const wu = await waitForWu();

let release;

wu.define('sumador', {
  mount(container, context) {
    container.innerHTML = `
      <link rel="stylesheet" href="/mf-sumador/styles.css">
      <section aria-labelledby="service-title">
        <h2 id="service-title">Servicio de suma</h2>
        <p role="status">Listo para recibir solicitudes.</p>
      </section>
    `;
    const status = container.querySelector('[role="status"]');

    release = context.wu.handle('ejemplo:suma', ({ a, b }, request) => {
      if (!Number.isFinite(a) || !Number.isFinite(b)) {
        throw new TypeError('Escribe dos números finitos.');
      }
      const total = a + b;
      if (!Number.isFinite(total)) {
        throw new RangeError('La suma excede el rango numérico.');
      }
      status.textContent = `Respondí a ${request.caller.appName}: ${a} + ${b} = ${total}`;
      return total;
    }, {
      version: '1.0.0',
      allow: ['cliente-suma'],
    });
  },

  unmount(container) {
    release?.();
    release = undefined;
    container.replaceChildren();
  },
});
```

<span id="4-implementar-el-cliente"></span>

<span class="section-anchor" id="section-9"></span>

## 4. Implement the client

The client does not import the provider function. It sends both numbers through `context.wu.request`, awaits the response, and updates its own DOM. It requests a version compatible with `^1.0.0` and limits waiting to 1000 ms.

An empty field is sent as `null` so you can observe provider validation rejection. `try`/`catch` presents errors without leaving the interface blocked. Unmounting the client removes its listeners and cancels any pending request.
<span class="section-anchor" id="section-10"></span>

### `mf-cliente-suma/src/main.js`

[View complete file](/examples/browser/rpc-sum/mf-cliente-suma/src/main.js.txt)

```js
import { waitForWu } from 'wu-framework/adapters/shared';

const wu = await waitForWu();

let dispose;

wu.define('cliente-suma', {
  mount(container, context) {
    container.innerHTML = `
      <link rel="stylesheet" href="/mf-cliente-suma/styles.css">
      <section aria-labelledby="client-title">
        <h2 id="client-title">Cliente de suma</h2>
        <form>
          <label>Primer número<input name="a" type="number" step="any" value="7"></label>
          <label>Segundo número<input name="b" type="number" step="any" value="8"></label>
          <button type="submit">Calcular</button>
        </form>
        <output aria-live="polite">Pulsa Calcular para solicitar una suma.</output>
      </section>
    `;
    const form = container.querySelector('form');
    const first = form.elements.namedItem('a');
    const second = form.elements.namedItem('b');
    const button = form.querySelector('button');
    const output = container.querySelector('output');
    const listeners = new AbortController();
    let pending;
    let active = true;

    form.addEventListener('submit', async (event) => {
      event.preventDefault();
      if (pending) return;
      const a = first.value === '' ? null : first.valueAsNumber;
      const b = second.value === '' ? null : second.valueAsNumber;
      pending = new AbortController();
      button.disabled = true;
      output.textContent = 'Calculando…';
      try {
        const total = await context.wu.request('ejemplo:suma', { a, b }, {
          version: '^1.0.0',
          timeout: 1000,
          signal: pending.signal,
        });
        if (active) output.textContent = `${a} + ${b} = ${total}`;
      } catch (error) {
        if (!active) return;
        output.textContent = error.code === 'WU_RPC_NO_HANDLER'
          ? 'El servicio está detenido. Inícialo y vuelve a pulsar Calcular.'
          : `No se pudo calcular: ${error.message}`;
      } finally {
        pending = undefined;
        if (active) button.disabled = false;
      }
    }, { signal: listeners.signal });

    dispose = () => {
      active = false;
      listeners.abort();
      pending?.abort();
    };
  },

  unmount(container) {
    dispose?.();
    dispose = undefined;
    container.replaceChildren();
  },
});
```

<span id="5-montar-ambas-apps-desde-astro"></span>

<span class="section-anchor" id="section-11"></span>

## 5. Mount both apps from Astro

This page configures its own apps and grants. It mounts the service first, then the client. Buttons let you unmount and mount the provider without reloading the client, so you can verify that it really depends on an available handler.
<span class="section-anchor" id="section-12"></span>

### `shell/src/pages/suma.astro`

[View complete file](/examples/browser/rpc-sum/shell/src/pages/suma.astro.txt)

```astro
<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Una suma entre dos microapps WU</title>
    <link rel="icon" href="/favicon.svg" type="image/svg+xml" />
  </head>
  <body>
    <main>
      <a href="/">Volver al workspace</a>
      <h1>Una suma entre dos microapps</h1>
      <p>El cliente pide el resultado y el servicio calcula la suma.</p>
      <div class="controls">
        <button id="start" disabled>Iniciar servicio</button>
        <button id="stop" disabled>Detener servicio</button>
      </div>
      <p id="status" role="status">Iniciando WU…</p>
      <div id="service"></div>
      <div id="client"></div>
    </main>
    <script>
      import { wu } from 'wu-framework';

      const start = document.querySelector<HTMLButtonElement>('#start')!;
      const stop = document.querySelector<HTMLButtonElement>('#stop')!;
      const status = document.querySelector<HTMLElement>('#status')!;
      let running = false;

      async function setService(enabled: boolean) {
        start.disabled = true;
        stop.disabled = true;
        try {
          if (enabled) await wu.mount('sumador', '#service');
          else await wu.unmount('sumador', { force: true });
          running = enabled;
          status.textContent = enabled
            ? 'Servicio disponible. Pulsa Calcular.'
            : 'Servicio detenido. Pulsa Calcular para comprobar el error.';
        } catch (error) {
          status.textContent = `No se pudo cambiar el servicio: ${error}`;
        } finally {
          start.disabled = running;
          stop.disabled = !running;
        }
      }

      try {
        await wu.init({
          sandbox: 'module',
          apps: [
            {
              name: 'sumador',
              url: `${location.origin}/mf-sumador`,
              grants: ['rpc.provide:ejemplo:suma'],
            },
            {
              name: 'cliente-suma',
              url: `${location.origin}/mf-cliente-suma`,
              grants: ['rpc.call:ejemplo:suma'],
            },
          ],
        });
        await wu.mount('sumador', '#service');
        running = true;
        await wu.mount('cliente-suma', '#client');
        stop.disabled = false;
        status.textContent = 'Servicio disponible. Pulsa Calcular.';
        start.addEventListener('click', () => { void setService(true); });
        stop.addEventListener('click', () => { void setService(false); });
      } catch (error) {
        status.textContent = `No se pudo iniciar WU: ${error}`;
      }
    </script>
  </body>
</html>

<style>
  :global(body) { margin: 0; background: #f4f7f6; color: #172b34; font: 1rem/1.6 system-ui, sans-serif; }
  main { max-width: 48rem; margin: 0 auto; padding: 2rem 1rem; }
  h1 { font-size: clamp(1.6rem, 5vw, 2.5rem); line-height: 1.2; }
  a { color: #175f56; }
  .controls { display: flex; flex-wrap: wrap; gap: .75rem; }
  button { border: 1px solid #536f74; border-radius: .5rem; padding: .6rem 1rem; color: #172b34; background: white; font: inherit; cursor: pointer; }
  button:disabled { opacity: .55; cursor: default; }
  :focus-visible { outline: 3px solid #174bac; outline-offset: 3px; }
  #client { margin-top: 1rem; }
</style>
```

`waitForWu` locates the shell runtime to register each app through `define`. RPC calls use the `mount` facade with identity, version and allowlist. Framework 4 also preserves these options in `wu.handle`; use the scoped facade inside a microapp.

<span id="6-conservar-la-entrada-y-el-manifest-al-construir"></span>

<span class="section-anchor" id="section-13"></span>

## 6. Preserve the entrypoint and manifest during build

Each app's `index.html` is also its Vite build entrypoint. The following two files point to `src/main.js`. They replace template files so development and production execute the same provider and client.

<span class="section-anchor" id="section-14"></span>

### `mf-sumador/index.html`

[View complete file](/examples/browser/rpc-sum/mf-sumador/index.html.txt)

```html
<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Servicio de suma · WU</title>
  </head>
  <body><script type="module" src="/src/main.js"></script></body>
</html>
```

<span class="section-anchor" id="section-15"></span>

### `mf-cliente-suma/index.html`

[View complete file](/examples/browser/rpc-sum/mf-cliente-suma/index.html.txt)

```html
<!doctype html>
<html lang="es">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Cliente de suma · WU</title>
  </head>
  <body><script type="module" src="/src/main.js"></script></body>
</html>
```

<span id="servir-los-estilos-como-archivos"></span>

<span class="section-anchor" id="section-16"></span>

### Serving styles as files

Astro generates an external stylesheet with `inlineStylesheets: 'never'`. Each microapp loads its own `styles.css` from its Shadow DOM. Styles therefore comply with `style-src 'self'` without inline styles or server CSP changes. If you already applied the counter, keep this same shell configuration option.

<span class="section-anchor" id="section-17"></span>

### `shell/astro.config.mjs`

[View complete file](/examples/browser/rpc-sum/shell/astro.config.mjs.txt)

```js
import { defineConfig } from 'astro/config';

export default defineConfig({
  server: { port: 4321 },
  build: { inlineStylesheets: 'never' },
});
```

<span class="section-anchor" id="section-18"></span>

### `mf-sumador/styles.css`

[View complete file](/examples/browser/rpc-sum/mf-sumador/styles.css.txt)

```css
:host { display: block; font: 1rem/1.6 system-ui, sans-serif; color: #172b34; }
section { padding: 1.5rem; background: #edf7f1; border-radius: 1rem; }
h2 { margin-top: 0; font-size: 1.25rem; }
p { margin-bottom: 0; }
```

<span class="section-anchor" id="section-19"></span>

### `mf-cliente-suma/styles.css`

[View complete file](/examples/browser/rpc-sum/mf-cliente-suma/styles.css.txt)

```css
:host { display: block; font: 1rem/1.6 system-ui, sans-serif; color: #172b34; }
section { padding: 1.5rem; background: white; border: 1px solid #cbd9d9; border-radius: 1rem; }
h2 { margin-top: 0; font-size: 1.25rem; }
form { display: flex; gap: 1rem; flex-wrap: wrap; align-items: end; }
label { display: grid; gap: .25rem; }
input { box-sizing: border-box; width: 7rem; padding: .65rem; font: inherit; border: 1px solid #6e838b; border-radius: .4rem; }
button { padding: .7rem 1rem; font: inherit; color: white; background: #126153; border: 0; border-radius: .4rem; cursor: pointer; }
button:disabled { opacity: .65; cursor: wait; }
:focus-visible { outline: 3px solid #174bac; outline-offset: 3px; }
output { display: block; margin-top: 1rem; font-weight: 650; }
```

There is one source `wu.json` per app, at its root. CLI native mode serves it directly. Vite must also include it in output: this plugin emits both the manifest and `styles.css` without manually maintaining extra copies. When collecting output, `wu build` preserves permissions and replaces `entry` with compiled JavaScript.

<details>
<summary>Complete dependency and build files included in the ZIP</summary>

<span class="section-anchor" id="section-20"></span>

### `mf-sumador/package.json`

[View complete file](/examples/browser/rpc-sum/mf-sumador/package.json.txt)

```json
{
  "name": "wu-sumador",
  "private": true,
  "type": "module",
  "engines": { "node": ">=24" },
  "scripts": { "dev": "vite --port 5003", "build": "vite build" },
  "dependencies": { "wu-framework": "4.0.0" },
  "devDependencies": { "vite": "7.3.6" }
}
```

<span class="section-anchor" id="section-21"></span>

### `mf-cliente-suma/package.json`

[View complete file](/examples/browser/rpc-sum/mf-cliente-suma/package.json.txt)

```json
{
  "name": "wu-cliente-suma",
  "private": true,
  "type": "module",
  "engines": { "node": ">=24" },
  "scripts": { "dev": "vite --port 5004", "build": "vite build" },
  "dependencies": { "wu-framework": "4.0.0" },
  "devDependencies": { "vite": "7.3.6" }
}
```

<span class="section-anchor" id="section-22"></span>

### `mf-sumador/vite.config.js`

[View complete file](/examples/browser/rpc-sum/mf-sumador/vite.config.js.txt)

```js
import { readFileSync } from 'node:fs';
import { defineConfig } from 'vite';

export default defineConfig({
  base: './',
  build: { target: 'esnext' },
  plugins: [{
    name: 'wu-assets',
    generateBundle() {
      for (const fileName of ['wu.json', 'styles.css']) {
        this.emitFile({
          type: 'asset',
          fileName,
          source: readFileSync(new URL(fileName, import.meta.url), 'utf8'),
        });
      }
    },
  }],
});
```

<span class="section-anchor" id="section-23"></span>

### `mf-cliente-suma/vite.config.js`

[View complete file](/examples/browser/rpc-sum/mf-cliente-suma/vite.config.js.txt)

```js
import { readFileSync } from 'node:fs';
import { defineConfig } from 'vite';

export default defineConfig({
  base: './',
  build: { target: 'esnext' },
  plugins: [{
    name: 'wu-assets',
    generateBundle() {
      for (const fileName of ['wu.json', 'styles.css']) {
        this.emitFile({
          type: 'asset',
          fileName,
          source: readFileSync(new URL(fileName, import.meta.url), 'utf8'),
        });
      }
    },
  }],
});
```

</details>

<span id="7-ejecutar-y-reconocer-el-resultado"></span>

<span class="section-anchor" id="section-24"></span>

## 7. Run and recognize the result

After copying files, from the workspace root:

```sh
wu install
npm --prefix shell run build
wu dev --native --port 3000 --no-open
```

This initial build includes `suma.astro` in `shell/dist/` after copying the files with the server stopped. If shell output already exists, CLI 1.2.0 may reuse it at startup. Once `wu dev --native` is running, the watcher rebuilds the shell when its sources change and then reloads the page; you do not need to stop it and build manually after every edit. If compilation fails, inspect the terminal error.

Open [http://127.0.0.1:3000/suma/](http://127.0.0.1:3000/suma/). You should see **Servicio de suma**, **Cliente de suma**, and the numbers `7` and `8`.

| Action | Visible result |
| --- | --- |
| Click **Calcular** | The client displays **7 + 8 = 15**; the service reports responding to `cliente-suma`. |
| Change the second number to `3` and calculate | **7 + 3 = 10**. |
| Empty the first number and calculate | **No se pudo calcular: Escribe dos números finitos.** |
| Restore `7` and `8`, click **Detener servicio**, and calculate | The client explains that the service stopped and how to retry. |
| Click **Iniciar servicio** and calculate | **7 + 8 = 15** again. |

The new apps also appear in navigation when you return to the main page. Open `sumador` before calculating from `cliente-suma`; this navigation mounts each app when visited. The `/suma/` page performs that order automatically.

To check versioning, temporarily change the handler version to `2.0.0`, reload `/suma/`, and calculate. The `^1.0.0` request is rejected as incompatible. Restore `1.0.0` afterward.

<span id="8-comprobar-la-versión-que-publicarás"></span>

<span class="section-anchor" id="section-25"></span>

## 8. Check the version you will publish

Stop development with **Ctrl+C** and run:

```sh
wu build
wu serve 4100 --mode static
```

Open [http://127.0.0.1:4100/suma/index.html](http://127.0.0.1:4100/suma/index.html). Repeat summation, service removal, and remounting; then reload that URL. The same results should work with compiled files. Publishable output is `dist/` at the workspace root.

`wu serve 4100 --mode static` resolves directory indexes and returns real 404 responses for missing routes. Use `--mode spa` only when client routes need shell fallback.

<span id="si-algo-falla"></span>

<span class="section-anchor" id="section-26"></span>

## If something fails

| Symptom | What to check |
| --- | --- |
| `/suma/` is missing or displays the old sum | If you copied the page while development was stopped, run `npm --prefix shell run build` before starting. During development, check that the automatic shell rebuild has no errors. |
| WU reports a missing required permission | Compare the `wu.json` permission with the corresponding app grant in `suma.astro`; for the main page, check `wu.config.json`. |
| The client says the service is stopped | Click **Iniciar servicio**. In main navigation, open `sumador` first. |
| The page works but loses styles | Keep `build.inlineStylesheets: 'never'` in `shell/astro.config.mjs`; check both `styles.css` files exist in `dist/mf-*/` and run `wu build`. |
| The Vanilla JS counter reappears in production | Replace both `index.html` files with this recipe's versions; their scripts must load `/src/main.js`. Run `wu build` again. |
| Works in development but fails in production | Check that you copied both `vite.config.js` files and that `dist/mf-sumador/wu.json` and `dist/mf-cliente-suma/wu.json` preserve permissions with a production `entry`. |
| The request is rejected for version mismatch | The handler publishes `1.0.0` and the client requests `^1.0.0`; they must be compatible. |

<span id="continuar-con-las-apis"></span>

<span class="section-anchor" id="section-27"></span>

## Continue with the APIs

You have used `init`, `define`, `mount`, `unmount`, `handle`, and `request` between two apps with different permissions. [Events, contracts, and RPC](/docs/framework/communication/) explains signatures and variants; [the communication reference](/docs/framework/api-events/) lists APIs. To add contract discovery, observable cancellation, and other operations, continue with [A WU-connected service](/docs/guides/browser-backend/).