Akapulu Labs logo Akapulu Labs Blog

How Simam Digital Gave a Hospital Ops Digital Twin a Voice with Akapulu Labs

Junaid Malik built Wren, a live avatar operations assistant, into an NHS hospital-ops digital twin prototype on the Akapulu Web SDK. Here's how the integration went, what he told us was rough, and what we shipped the same day because of it.

How Simam Digital Gave a Hospital Ops Digital Twin a Voice with Akapulu Labs

"A dashboard can tell you the state of the world. It can't answer a question." That is how Junaid Malik, founder of Simam Digital, opens his write-up of Wren, a live avatar operations assistant he built into a hospital operations digital twin on the Akapulu Web SDK. This is our side of the story: what Simam built, how the integration went, and what we changed because of it.

A hospital ops twin you can talk to

Simam's NHS Digital Twin is a browser-based operations platform. It opens on a photoreal 3D map of a trust's estate, with every site pinned by occupancy, and drills all the way down to a live, room-level floor plan with beds, staff and assets moving through it. Underneath sits a patient-flow and escalation model: bed occupancy, A&E queues, OPEL levels, staffing gaps.

Junaid is clear about what it is, and so are we. It is a concept prototype, built as a research project for a client, and every number in it is simulated. No real patient or NHS data goes anywhere near it. That disclosure sits on the entry screen, the footer and every modelling panel. What is real is the interaction pattern and the architecture around it.

The Wren operations assistant panel open beside a photoreal 3D view of a hospital campus in the NHS Digital Twin prototype
Wren open at site level in the twin. Screenshot: Simam Digital, simulated data.

Wren is the assistant inside the twin. Simam used the name of one of the stock faces in our avatar catalogue. It sits behind an "Ask Wren · Operations assistant" launcher, and when you open it you get a compact, circular, avatar-first stage rather than a chat window bolted onto the side. Ask "which sites have A&E queues over three hours?" and it answers in speech and flies the 3D camera to the site it is talking about.

"Wren doesn't just talk, it acts on the model: pulling live figures, and flying the 3D camera to a site when you ask to see it. That's the difference between a chatbot in a corner and an assistant that operates the tool with you."

How Simam built it

The REST contract

Simam's server calls POST /api/conversations/connect/ with a scenario and an avatar and gets three things back: a room_url, a token and a conversation_session_id. The browser joins the room, and updates are polled by that id until the call is ready. Junaid's read: "clean and predictable... We understood it within minutes, which is rarer than it should be."

Their UI, our plumbing

Simam did not use the prebuilt AkapuluConversation component from @akapulu/react-ui. It is a full-page, two-column layout, and Wren needed to be a small circular stage in the NHS design language. So they dropped down to the hooks in @akapulu/react (useAkapuluSession, useAkapuluDailyCall, useAkapuluParticipantRoles) and rendered their own stage with Daily's video primitive. The SDK kept the hard parts: WebRTC transport via Daily, media handling, live transcripts, and the speaking state that drives Wren's listening and speaking ring.

"Akapulu's lower-level hooks let us throw away the prebuilt conversation UI entirely and build our own compact stage in the NHS design language, while the SDK still owned the hard parts underneath. WebRTC transport via Daily, media handling, live transcripts and speaking-state all came out of the box. That's exactly the right seam to expose."

That seam is deliberate. @akapulu/react-ui is the fast path for a demo. The hooks are the path for a product that already has a design system.

Keeping it grounded

"In an operational setting, a confidently wrong answer is worse than no answer." Wren is constrained to the twin's own data and declines when a question falls outside it. In Junaid's words, "Constraining the model to the twin's own data was a bigger part of the effort than the wiring."

"It came together fast and the result feels genuinely alive."

Room-level floor plan in the NHS Digital Twin prototype showing wards, beds, staff and asset positions
Room level: wards, beds, staff and tracked assets. Screenshot: Simam Digital, simulated data.

Security from the first line, not the last

The Akapulu API key never ships to the browser. Every call from the twin goes through a server-side proxy, a Firebase Function behind /api/akapulu/**. It verifies the signed-in user, checks them against an allowlist, calls Akapulu with the server-held key, and returns only the room URL and session token. The browser never sees the credential.

That is the pattern we want every production integration to use, and Junaid built it before we had a doc page telling him to. Here is the shape, as a connect route using @akapulu/server 1.0.3:

// app/api/akapulu/connect/route.ts
import { AkapuluApiError, createAkapuluServerClient } from "@akapulu/server";

const akapulu = createAkapuluServerClient(); // reads AKAPULU_API_KEY from server env

export async function POST(request: Request) {
  // 1. Verify the caller. Simam used a Firebase ID token.
  const user = await verifyIdToken(request.headers.get("authorization"));
  if (!user) return Response.json({ error: "Unauthorized" }, { status: 401 });

  // 2. Check them against an allowlist.
  if (!isAllowed(user)) return Response.json({ error: "Forbidden" }, { status: 403 });

  // 3. Mint the session with the server-held key.
  //    The browser only ever gets room_url, token and conversation_session_id.
  try {
    return Response.json(
      await akapulu.connectConversation({ scenario_id: SCENARIO_ID, avatar_id: WREN_AVATAR_ID })
    );
  } catch (error) {
    if (error instanceof AkapuluApiError) {
      return Response.json(error.details ?? { error: error.message }, { status: error.status });
    }
    throw error;
  }
}

On the client, AkapuluProvider points at that route and sends the user's token with every request. headers can be a function, so a short-lived token is read fresh each time instead of being captured once at mount:

<AkapuluProvider
  config={{
    endpoints: { connectPath: "/api/akapulu/connect", updatesPath: "/api/akapulu/updates" },
    headers: async () => ({ Authorization: `Bearer ${await getIdToken()}` }),
  }}
>
  <WrenStage />
</AkapuluProvider>

What he told us was rough, and what we shipped

Along the way, Junaid sent us a set of honest integration notes, friction and all, with a concrete suggestion against each one. Two of them cost Simam real time.

First, @akapulu/server would not import under Node's native ESM loader. The built package used extensionless relative imports, so Simam could not load it inside their Firebase Function and reimplemented the two REST calls by hand. Second, the secure setup above was the undocumented path. The quickstart called connect from the client, and every real product had to work out the proxy on its own.

Those notes landed on the morning of August 27. By that evening we had shipped:

  • @akapulu/server 1.0.3: .js extensions on relative imports, a proper exports map, and a CI job that installs the built tarball and imports it from a plain Node script so it cannot regress.
  • @akapulu/react 1.0.4: a headers option on your connect POST and updates GET. Pass an object or an async function, so a Firebase ID token stays current for the whole session.
  • A machine-readable error_code (for example QUOTA_EXCEEDED) next to the human error string, surfaced as error.code, so UI branches stop depending on regexing a message.
  • Updates polling that runs at 200ms only until the call is ready, then stops.
  • Docs that say it plainly: keep the key on the server, authenticate users on your connect route before starting a billed call, pass user auth through config.headers. Plus llms.txt and an agent-instructions preamble on every doc page, because integration guides now get read by coding agents before they get read by people.

One item is still open. Junaid asked for something between the full-page prebuilt component and raw hooks: styleable primitives for a compact stage like Wren's. We are building a bottom-right avatar example for exactly that.

"We sent Akapulu Labs a set of honest integration notes along the way, friction and all, in the spirit of the collaboration. The team was generous with the exchange and clearly glad to see the tool put to real use. That kind of openness is worth calling out on its own."

If you are shipping into healthcare

Junaid's line on the economics is the one to keep: "A voice layer like this is cheap to bolt on badly and expensive to bolt on well, and healthcare only buys the second kind." The secret boundary, the allowlist gate and the grounded-answers constraint are not tidiness. They are the questions a procurement or information-governance process asks. As he puts it, "The same architecture that keeps one API key safe is the architecture that lets you say yes to a security questionnaire."

That is why the SDK exposes hooks and not just a widget, and why the server package exists at all. Your UI, your auth, your data boundary. Our conversational loop underneath.

Where this goes next

Junaid names two moves: point Wren at a genuinely live operational model instead of a simulated one, and go from answering to acting, so the assistant can kick off discharge coordination or flag a reallocation instead of only reporting the state. He is careful to call those hypotheses to test, not outcomes to claim from a prototype. We would say the same.

Simam Digital built this. Read Junaid's full write-up on LinkedIn or the version on Simam's site, try the prototype at nhs-ops.simamdigital.com, and see more of their work at simamdigital.com.

If your users would rather ask than hunt, the pieces Simam used are all in our docs: the Customized UI example, the Server SDK page, and llms.txt if you are building with a coding agent.