Microfrontend Integration
Microfrontend Integration
Cloudillo uses a microfrontend architecture where applications run as sandboxed iframes and communicate with the shell via postMessage.
Overview
Benefits:
- Isolation - Apps are sandboxed for security
- Technology agnostic - Use any framework (React, Vue, vanilla JS)
- Independent deployment - Update apps without redeploying the shell
- Resource sharing - Share authentication and state via the shell
Communication Protocol
Shell → App (Init Message)
When an app loads, the shell sends an init message:
{
cloudillo: true,
type: 'init',
idTag: 'alice@example.com',
tnId: 12345,
roles: ['user', 'admin'],
theme: 'glass',
darkMode: false,
token: 'eyJhbGciOiJFUzM4NCIsInR5cCI6IkpXVCJ9...'
}Fields:
cloudillo: true- Identifies Cloudillo messagestype: 'init'- Message typeidTag- User’s identitytnId- Tenant IDroles- User rolestheme- UI theme variantdarkMode- Dark mode preferencetoken- Access token for API calls
App → Shell (Init Request)
Apps request initialization by sending:
{
cloudillo: true,
type: 'initReq'
}Bidirectional Communication
Apps and shell can send requests and replies:
// Shell → App (request)
{
cloudillo: true,
type: 'request',
id: 123,
method: 'getData',
params: { key: 'value' }
}
// App → Shell (reply)
{
cloudillo: true,
type: 'reply',
id: 123,
data: { result: 'success' }
}Basic App Setup
Step 1: Create index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>My Cloudillo App</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div id="app">Loading...</div>
<script type="module" src="./app.js"></script>
</body>
</html>Step 2: Initialize in app.js
import * as cloudillo from '@cloudillo/base'
async function main() {
// Request init from shell
const token = await cloudillo.init('my-app')
// Now you have access to:
console.log('User:', cloudillo.idTag)
console.log('Tenant:', cloudillo.tnId)
console.log('Roles:', cloudillo.roles)
console.log('Token:', token)
// Create API client
const api = cloudillo.createApiClient()
// Your app logic here...
initializeApp(api)
}
main().catch(console.error)Step 3: Build and Deploy
# Build your app (example with Rollup)
rollup -c
# Deploy to shell's apps directory
cp -r dist /path/to/cloudillo/shell/public/apps/my-appComplete Example
Here’s a complete microfrontend app:
import * as cloudillo from '@cloudillo/base'
import * as Y from 'yjs'
// Initialize
async function init() {
const token = await cloudillo.init('quillo')
// Check dark mode
if (cloudillo.darkMode) {
document.body.classList.add('dark-theme')
}
// Create API client
const api = cloudillo.createApiClient()
// Get document ID from URL
const params = new URLSearchParams(window.location.search)
const docId = params.get('docId') || 'default-doc'
// Open collaborative document
const yDoc = new Y.Doc()
const { provider } = await cloudillo.openYDoc(yDoc, docId)
// Initialize editor
initEditor(yDoc, provider)
// Handle save
document.getElementById('save-btn')?.addEventListener('click', async () => {
await saveDocument(api, docId, yDoc)
})
}
function initEditor(yDoc, provider) {
const yText = yDoc.getText('content')
// Simple textarea editor
const textarea = document.getElementById('editor')
// Load content
textarea.value = yText.toString()
// Listen for remote changes
yText.observe(() => {
if (document.activeElement !== textarea) {
textarea.value = yText.toString()
}
})
// Send local changes
textarea.addEventListener('input', (e) => {
yDoc.transact(() => {
yText.delete(0, yText.length)
yText.insert(0, e.target.value)
})
})
}
async function saveDocument(api, docId, yDoc) {
const content = yDoc.getText('content').toString()
await api.action.post({
type: 'POST',
content: {
text: content,
docId: docId
}
})
alert('Saved!')
}
// Start app
init().catch(console.error)React Microfrontend
For React apps, use CloudilloProvider:
import React from 'react'
import ReactDOM from 'react-dom/client'
import { CloudilloProvider, useAuth, useApi } from '@cloudillo/react'
function App() {
return (
<CloudilloProvider appName="my-react-app">
<AppContent />
</CloudilloProvider>
)
}
function AppContent() {
const auth = useAuth()
const api = useApi()
if (!auth.idTag) {
return <div>Loading...</div>
}
return (
<div>
<h1>Welcome, {auth.name}!</h1>
<YourComponents />
</div>
)
}
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)Loading Apps in the Shell
Use MicrofrontendContainer to load apps:
import { MicrofrontendContainer } from '@cloudillo/react'
function Shell() {
const [currentApp, setCurrentApp] = useState('quillo')
const [docId, setDocId] = useState('doc_123')
return (
<div className="shell">
<nav>
<button onClick={() => setCurrentApp('quillo')}>Quillo</button>
<button onClick={() => setCurrentApp('prello')}>Prello</button>
<button onClick={() => setCurrentApp('todollo')}>Todollo</button>
</nav>
<main>
<MicrofrontendContainer
appName={currentApp}
src={`/apps/${currentApp}/index.html`}
docId={docId}
/>
</main>
</div>
)
}URL Parameters
Pass data to apps via URL parameters:
// Shell passes docId
<MicrofrontendContainer
src={`/apps/quillo/index.html?docId=${docId}`}
/>
// App reads docId
const params = new URLSearchParams(window.location.search)
const docId = params.get('docId')Styling
Apps should be responsive and adapt to the shell’s theme:
/* Base styles */
body {
margin: 0;
padding: 16px;
font-family: system-ui, sans-serif;
background: var(--bg-color, #fff);
color: var(--text-color, #000);
}
/* Dark mode */
body.dark-theme {
--bg-color: #1a1a1a;
--text-color: #fff;
}
/* Responsive */
@media (max-width: 768px) {
body {
padding: 8px;
}
}Security Considerations
1. Sandbox Attributes
The shell loads apps with these sandbox attributes:
<iframe
sandbox="allow-scripts allow-same-origin allow-forms"
src="/apps/my-app/index.html"
></iframe>This prevents:
- Navigation of the top frame
- Popup windows
- Download without user gesture
2. Content Security Policy
Apps should set a strict CSP:
<meta http-equiv="Content-Security-Policy"
content="default-src 'self'; connect-src 'self' wss:">3. Token Handling
Never store tokens in localStorage (vulnerable to XSS):
// ✅ Good - token in memory only
const token = await cloudillo.init('my-app')
// ❌ Bad - vulnerable to XSS
localStorage.setItem('token', token)4. Message Validation
Validate all postMessage events:
window.addEventListener('message', (event) => {
// Verify origin
if (event.origin !== window.location.origin) {
return
}
// Verify message structure
if (!event.data?.cloudillo) {
return
}
// Handle message
handleCloudilloMessage(event.data)
})Debugging
Console Logging
// Enable debug logging
cloudillo.debug = true
// All postMessage events will be logged
await cloudillo.init('my-app')DevTools
Use browser DevTools to inspect iframe:
- Open DevTools
- Select iframe context in console dropdown
- Inspect app state
Error Handling
try {
const token = await cloudillo.init('my-app')
} catch (error) {
console.error('Init failed:', error)
// Check if running in shell
if (window.parent === window) {
console.error('App must run inside Cloudillo shell')
}
}Example Apps
Cloudillo includes several example apps:
Quillo - Rich Text Editor
- Location:
/apps/quillo - Tech: Quill + Yjs
- Features: Collaborative rich text editing
Prello - Presentations
- Location:
/apps/prello - Tech: Custom drag-drop + Yjs
- Features: Slides, drawings, animations
Sheello - Spreadsheet
- Location:
/apps/sheello - Tech: Fortune Sheet + Yjs
- Features: Excel-like spreadsheets
Formillo - Forms
- Location:
/apps/formillo - Tech: React + RTDB
- Features: Form builder and responses
Todollo - Tasks
- Location:
/apps/todollo - Tech: React + RTDB
- Features: Task management
All use the same patterns described in this guide.
Best Practices
1. Request Init Immediately
// ✅ Initialize on load
async function main() {
const token = await cloudillo.init('my-app')
// Continue...
}
main()
// ❌ Delay init
setTimeout(() => {
cloudillo.init('my-app')
}, 1000)2. Handle Theme Changes
// Listen for theme changes from shell
window.addEventListener('message', (event) => {
if (event.data?.cloudillo && event.data.type === 'themeChange') {
document.body.classList.toggle('dark-theme', event.data.darkMode)
}
})3. Clean Up on Unload
window.addEventListener('beforeunload', () => {
// Close WebSocket connections
provider?.destroy()
// Cancel pending requests
abortController.abort()
})4. Progressive Enhancement
// Show loading state while initializing
document.getElementById('app').innerHTML = 'Loading...'
await cloudillo.init('my-app')
// Show app content
document.getElementById('app').innerHTML = '<div>App ready!</div>'See Also
- Getting Started - Build your first app
- @cloudillo/base - Core SDK
- @cloudillo/react - React integration
- CRDT - Collaborative editing