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
- Getting started – build your first Cloudillo app
- @cloudillo/core – core SDK reference
- @cloudillo/react – React hooks and components