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 messages
  • type: 'init' - Message type
  • idTag - User’s identity
  • tnId - Tenant ID
  • roles - User roles
  • theme - UI theme variant
  • darkMode - Dark mode preference
  • token - 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-app

Complete 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:

  1. Open DevTools
  2. Select iframe context in console dropdown
  3. 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