Microfrontend Integration

Cloudillo uses a microfrontend architecture where apps run as sandboxed iframes and communicate with the shell via a typed postMessage protocol.

Architecture overview

Apps are loaded into the Cloudillo shell as iframes with opaque origins (no allow-same-origin), ensuring strong isolation between apps and the shell. All communication flows through a message bus layer provided by @cloudillo/core.

  • Isolation – apps are sandboxed, preventing access to the shell’s DOM, cookies, or service worker keys
  • Technology agnostic – use any framework (React, Vue, vanilla JS)
  • Independent deployment – update apps without redeploying the shell
  • Shared authentication – tokens are managed by the shell and pushed to apps via the message bus

Getting started

Initialization with @cloudillo/core

The getAppBus() singleton provides the main API for apps to communicate with the shell:

import { getAppBus } from '@cloudillo/core'

async function main() {
  const bus = getAppBus()
  const state = await bus.init('my-app')

  // state contains: idTag, tnId, roles, accessToken, access, darkMode, theme, ...
  console.log('User:', state.idTag)
  console.log('Access:', state.access) // 'read' | 'write'

  // Token is also available as bus.accessToken
  // bus.init() automatically calls notifyReady('auth')
}

main().catch(console.error)

AppState fields

The init() call returns an AppState object:

Field Type Description
idTag string? User’s identity tag
tnId number? Tenant ID
roles string[]? User roles
accessToken string? JWT access token for API calls
access 'read' | 'write' Access level for the current resource
darkMode boolean Dark mode preference
theme string UI theme variant (e.g. 'glass')
tokenLifetime number? Token lifetime in seconds
displayName string? Display name (for guest users via share links)
navState string? Navigation state passed from the shell

Lifecycle notifications

Apps signal their readiness to the shell in stages using bus.notifyReady(stage):

Stage When to call Notes
'auth' After authentication completes Called automatically by bus.init()
'synced' After CRDT/data sync completes Call manually when your data is loaded
'ready' When the app is fully interactive Call when UI is ready for user interaction

The shell shows a loading indicator until the app signals 'ready'. You can also report errors with bus.notifyError(code, message).

React integration

useCloudillo hook

The primary hook for React apps. It calls bus.init() internally and provides the app state:

import { useCloudillo, useAuth, useApi } from '@cloudillo/react'

function MyApp() {
  const { token, ownerTag, fileId, idTag, roles, access, displayName } = useCloudillo('my-app')
  const [auth] = useAuth()       // [AuthState | null, setter] tuple
  const { api } = useApi()       // { api: ApiClient | null, authenticated, setIdTag }

  if (!auth) return <div>Loading...</div>

  return <div>Hello, {auth.idTag}</div>
}
Info

useCloudillo extracts ownerTag and fileId from the URL hash (#ownerTag:fileId). The hash is how the shell passes resource context to apps.

useCloudilloEditor hook

For collaborative document apps using CRDT:

import { useCloudilloEditor } from '@cloudillo/react'

function Editor() {
  const { yDoc, provider, synced, error } = useCloudilloEditor('quillo')

  if (error) return <div>Error: {error.code}</div>
  if (!synced) return <div>Syncing...</div>

  // yDoc is a Y.Doc connected to the collaborative backend
  return <MyEditor yDoc={yDoc} />
}

This hook handles the full lifecycle: initialization, WebSocket connection, CRDT persistence, and cleanup on unmount. It automatically calls notifyReady('synced') when the document is synchronized.

Communication protocol

All messages follow the envelope format:

{ cloudillo: true, v: 1, type: 'category:action.verb', ... }

Message categories

Category Direction Purpose
auth:init.req app → shell Request initialization
auth:init.res shell → app Respond with state and token
auth:init.push shell → app Proactively push init (theme/state changes)
auth:token.push shell → app Push refreshed token
auth:token.refresh.req/res both Manual token refresh
app:ready.notify app → shell Signal readiness stage
app:error.notify app → shell Report error to shell
storage:op.req/res both Key-value storage operations
settings:get.req/res both App settings access
media:pick.req/result both Media picker dialog
doc:pick.req/result both Document picker dialog
crdt:* both CRDT cache operations
sensor:compass.* both Device sensor subscriptions
Note

Apps don’t need to handle the protocol directly. The AppMessageBus class (via getAppBus()) provides typed methods for all operations. The protocol details are mainly useful for debugging or building non-JS integrations.

Storage and settings

Storage API

Apps have access to namespaced key-value storage via the message bus:

const bus = getAppBus()

// Store and retrieve data
await bus.storage.set('my-app', 'preferences', { theme: 'dark' })
const prefs = await bus.storage.get<{ theme: string }>('my-app', 'preferences')

// List keys and check quota
const keys = await bus.storage.list('my-app', 'cache:')
const { limit, used } = await bus.storage.quota('my-app')
Method Signature Description
get get<T>(ns, key): Promise<T?> Get a value by key
set set(ns, key, value): Promise<void> Set a value
delete delete(ns, key): Promise<void> Delete a key
list list(ns, prefix?): Promise<string[]> List keys with optional prefix
clear clear(ns): Promise<void> Clear all data in namespace
quota quota(ns): Promise<{limit, used}> Get storage quota info

Settings API

Apps can read and write server-side settings scoped to app.<appName>.*:

Method Signature Description
get get<T>(key): Promise<T?> Get a setting value
set set(key, value): Promise<void> Set a setting value
list list(prefix?): Promise<Array<{key, value}>> List settings

Security

Warning

The shell loads app iframes with sandbox="allow-scripts allow-forms allow-downloads". The allow-same-origin attribute is deliberately excluded to create opaque origins, which prevents apps from accessing the shell’s service worker registration keys or cookies. This is a critical security boundary.

Token handling: Access tokens are held in memory only (inside the AppMessageBus instance). Never store tokens in localStorage or sessionStorage – even with opaque origins, this would create unnecessary persistence of credentials.

Message validation: The SDK validates all incoming messages automatically (protocol version, envelope structure, message type). Apps using getAppBus() do not need to implement manual postMessage validation.

Token refresh: The shell proactively pushes refreshed tokens via auth:token.push messages. Apps can also request a refresh manually with bus.refreshToken().

Debugging

Enable debug logging by passing a config to getAppBus():

const bus = getAppBus({ debug: true })
await bus.init('my-app')
// All message bus traffic will be logged to the console

To inspect an app’s iframe context in DevTools, use the console’s context selector dropdown to switch to the iframe’s execution context.

Example apps

Cloudillo includes several built-in apps that use these patterns:

App Tech Features
Quillo Quill + Yjs Collaborative rich text editor
Prezillo Custom + Yjs Presentation slides and animations
Calcillo Fortune Sheet + Yjs Excel-like spreadsheets
Formillo React + RTDB Form builder and response collection
Taskillo React + RTDB Task management
Notillo React + Yjs Note-taking
Scanillo React Document scanning
Mapillo React Map visualization
Ideallo React + Yjs Collaborative ideation board

See also