1. What is an iCanText Micro-App?

A Micro-App is a lightweight, pure-frontend web application (HTML, CSS, JS) that runs directly inside the iCanText user interface. It is not a backend bot, a server-side plugin, or a webhook service.

Instead, it lives in a secure, sandboxed iframe and uses the iCanText App Bridge to turn the P2P mesh network into a decentralized, serverless back-end. Whether you are building a multiplayer game, a collaborative whiteboard, or a secure voting system, the app relies entirely on the users' devices to process and synchronize data.

Zero Back-end Required: Host your HTML/JS on any static host (GitHub Pages, Vercel, S3). iCanText handles the user identity, network discovery, and encrypted transport layer automatically.

The Design Framework: Plan Before You Code

Before writing code (or prompting an AI), structure your app by answering three fundamental questions:

Q1: What is the app's purpose?

Are you building a static dashboard, a turn-based game (like Tic-Tac-Toe), a real-time collaborative tool (like Sticky Notes), or a consensus tool (like a Poll)? This dictates the next two choices.

Q2: Where does it live? (Display Mode)

  • MESSAGE (Chat Widget): The app is posted as an interactive bubble in the chat timeline. Best for polls, specific tasks, or games linked to a conversation context.
  • HEADER (Channel Tool): The app is pinned to the top of the channel. Best for persistent utilities, dashboards, or global channel settings.
  • POPUP (Standalone): Launched in a separate window or fullscreen. Best for immersive, complex applications.

Q3: How does it sync data? (Collaboration Model)

Depending on your app's purpose, choose how state is managed across the mesh network:

Method Type Best For...
updateAppContext
(OWNER_ONLY)
Private State Only the creator can modify the parent state. Ideal to protect the setup configuration of a Poll or the rules of a Game.
updateAppContext
(COLLABORATIVE)
Shared State Anyone can overwrite the main state. "Last-writer-wins". Ideal for global settings or simple shared text editing.
emitAppEvent Event Sourcing Everyone appends actions to a Universal Ledger. Perfect for votes, game moves, or collaborative whiteboards where no data should be lost to race conditions.
sendData Volatile P2P Fire-and-forget ephemeral signals. Lost if the receiver is offline. Ideal for live mouse cursors, WebRTC signaling, or typing indicators.

🔄 The "Late Joiner" Pattern (Volatile Sync)

If you rely on sendData (Volatile P2P) instead of the Persistent Ledger (e.g., for a live drawing app), you must handle new peers joining the channel after the app has started.

The Strategy: When a new peer opens the app, they broadcast a request, and one existing peer replies with the current RAM state.

// 1. When a new peer opens the app, they request the state
sdk.sendData(peerIds, { type: 'sync_request' });

// 2. Existing peers listen for requests and share their RAM state
sdk.onData((fromId, payload) => {
if (payload.type === 'sync_request') {
    sdk.sendData([fromId], { 
        type: 'sync_response', 
        data: myLocalRamState 
    });
}
if (payload.type === 'sync_response' && !amISyncedYet) {
    myLocalRamState = payload.data;
    amISyncedYet = true;
    renderUI();
}
});

Hyper-Privacy: Every data packet sent via the bridge is End-to-End Encrypted (E2EE) and signed by the user. Intermediate routing nodes only see a Sealed Forward envelope, keeping your application data completely invisible to the network.

How it works

Local iCanText Client iCanText Core Engine • Identity & E2EE Crypto • Universal Ledger & Routing ICTBridgeSDK (postMessage) Sandboxed Iframe Your Micro-App (HTML/JS/CSS) RAM State Only (Origin: null) 🚫 No LocalStorage Remote Peer Client Remote Core Engine ICTBridgeSDK Remote Micro-App Instance WebRTC Mesh E2EE JSON Payloads

The iCanText App Bridge is a versatile integration engine that turns iCanText into a Decentralized Operating System. It allows third-party web applications to run seamlessly within the communication flow, leveraging the P2P network as a secure, serverless back-end.

Universal Integration: Apps can be deployed as persistent tools in a channel's header, interactive widgets within the chat timeline, or standalone windows. iCanText handles the identity, discovery, and encrypted transport layer for all of them.

Decentralized Persistence: Apps can now choose how to manage shared data and synchronization across the mesh network.

Interaction & Collaboration Models

Integration Modes

Choosing the Right Communication Flow

The SDK offers three distinct ways to exchange data. Choose based on your persistence requirements:

Method Type Retention Best For...
sendData P2P Signal Volatile: Lost if the receiver's app is not currently open. Real-time cursors, video signaling, ephemeral game moves.
updateAppContext Message Edit Persistent: Updates the single "State" of the app message. Title of a project, global settings, "Last-writer-wins" data.
emitAppEvent Event Sourcing Persistent: Stored in the Universal Ledger. Can be replayed. Poll votes, document history, audit trails, collaborative pads.

Hyper-Privacy: Every data packet sent via the bridge is End-to-End Encrypted (E2EE) and signed by the user. Intermediate nodes only see a Sealed Forward envelope, keeping your application data and metadata invisible to the network.

🔄 The "Late Joiner" Pattern (Volatile Sync)

If you choose to use sendData (Volatile P2P) instead of the Persistent Ledger (like for a real-time whiteboard or sticky notes), you must handle new peers joining the channel after the app has started.

The Strategy: When a new peer appears, they must request the current state, and one existing peer must reply.

// 1. When a new peer opens the app, they request the state
			sdk.sendData(peerIds, { type: 'sync_request' });

			// 2. Existing peers listen for requests and share their RAM state
			sdk.onData((fromId, payload) => {
			    if (payload.type === 'sync_request') {
				sdk.sendData([fromId], { 
				    type: 'sync_response', 
				    data: myLocalRamState 
				});
			    }
			    if (payload.type === 'sync_response' && !amISyncedYet) {
				myLocalRamState = payload.data;
				amISyncedYet = true;
				renderUI();
			    }
			});

Understanding interactionModel

The interactionModel option in postAppContent defines who has the right to modify the original message instance itself:

Pro Tip: Even if an app is OWNER_ONLY, the Universal Ledger remains open. Any member can send app-event messages to it. iCanText ensures these events are cryptographically linked to the parent app message.

App Architecture & UI Guidelines

To ensure your micro-app works flawlessly across all iCanText clients and is strictly secure, you must adhere to the following architectural rules:

1. Zero Dependency & Asset Paths

Your application is rendered inside a strict iframe. It does not inherit any CSS or scripts from the iCanText parent.

2. Strict Sandbox Constraints (No LocalStorage)

To protect user privacy, micro-apps are sandboxed without the allow-same-origin directive.

3. Visual Routing via Display Modes

A single HTML file acts as the configuration screen, the chat widget, and the standalone app. Use sdk.getDisplayMode() and connection.status to conditionally hide/show HTML containers:

const connection = await sdk.init();

if (connection.status === 'STANDALONE') {
    // 1. User opened the app URL directly in a browser
    document.getElementById('standalone-config-ui').style.display = 'block';
} else if (sdk.getDisplayMode() === 'MESSAGE') {
    // 2. App is embedded inside the chat timeline
    document.getElementById('chat-interactive-ui').style.display = 'block';
    // Fetch the state specific to this message bubble
    const state = await sdk.getAppContext(); 
} else {
    // 3. App is in HEADER or POPUP mode (usually for config or tools)
    document.getElementById('full-tool-ui').style.display = 'block';
}

4. The "Local Echo" Trap (Event Sourcing)

When you call sdk.emitAppEvent(payload), iCanText immediately processes the event locally and triggers your onAppEvent callback for the sender too. This ensures perfect UI synchronization.

Crucial: Do not update your UI optimistically AND via the event listener, or actions will happen twice. Either rely entirely on onAppEvent to update the UI, or generate a unique actionId in your payload to ignore your own echoed events.

5. Responsive Design for Chat Bubbles

When your app runs in MESSAGE mode, it is displayed inside a chat timeline. It could be viewed on a large desktop monitor or a narrow mobile screen.

🤖 Prompting Rules for AI Assistants

If you are an AI generating an iCanText Micro-App, strictly follow these golden rules:

  1. Design First: Begin your code with a JS comment block explicitly answering the 3 framework questions: App Purpose, Display Mode, and Collaboration Model. Plan before you code.
  2. Single File: Output a single HTML file containing all JS and CSS.
  3. Initialization: Always wrap the startup logic in const connection = await sdk.init();.
  4. Routing: Check sdk.getDisplayMode(). Render a compact collaborative UI for 'MESSAGE', and a full configuration UI for 'STANDALONE' or 'HEADER'.
  5. History Replay: In collaborative apps, always call await sdk.getAppEvents() at startup to replay the history and reconstruct the current RAM state before listening to new live events.
  6. Late Joiners: If using sendData (volatile), always implement a sync_request / sync_response handshake so new users can ask existing users for the current RAM state.

Unified Security Handshake

Whether your app is running as a header icon, a chat widget, or an external window, it must perform a rigorous handshake to establish a secure link with iCanText:

  1. Initiation: The app is rendered by iCanText. This happens when:
    • A member clicks a Header Icon.
    • An Interactive Message appears in the chat timeline.
    • A user manually types /app https://your-app.com.
  2. Hello: Your application script starts and automatically sends an ICT_HELLO signal via postMessage to the parent window.
  3. Verification: iCanText identifies the source window, verifies it against its internal registry, and retrieves its specific context (active workspace, permissions).
  4. Init: iCanText returns an ICT_INIT payload containing the instanceId, the user's localPeerId, localPseudonym, and its Display Mode.
  5. Ready: The bridge is now READY. All subsequent P2P operations are cryptographically locked to this specific app instance.

ICTBridgeSDK.js

We provide a lightweight, promise-based SDK to handle the complexity of postMessage communication and handshaking.

Include it in your HTML:

<script src="https://icantext.com/ICTBridgeSDK.js"></script>

API Reference

sdk.init(timeoutMs): Starts the security handshake. Returns a Promise resolving to the connection state.

const connection = await sdk.init();
if (connection.status === 'READY') {
    console.log("Connected as:", connection.localPseudonym);
}

sdk.getPeers(): Returns an array of available peers in the current workspace. Use this to map PeerIDs to human-readable names.

const peers = await sdk.getPeers();
// peer object: { id: string, pseudonym: string, verified: boolean, isActiveOnApp: boolean }

const getAuthorName = (peerId) => {
    if (peerId === sdk.localPeerId) return sdk.localPseudonym;
    const peer = peers.find(p => p.id === peerId);
    return peer ? peer.pseudonym : peerId.substring(0, 8);
};

sdk.onPeersUpdate(callback, options): Subscribes to real-time updates of the peers list. This enables a decentralized Gossip protocol where app instances monitor each other without a central server.

  • presenceState: 0 (ALIVE), 1 (SUSPECT), 2 (OFFLINE).
  • isActuallyOnline: Boolean helper. false if the peer is confirmed offline by the network.
sdk.onPeersUpdate((peers) => {
    const onlinePeers = peers.filter(p => p.isActuallyOnline);
    updatePresenceDots(onlinePeers);
}, { cycleMs: 3000 }); // Optional: check every 3s

sdk.sendData(targetIds, payload): Transmits a JSON payload to specific PeerIDs via E2EE transport.

const targets = ['peer_xyz...'];
const data = { action: 'move', x: 10, y: 20 };
sdk.sendData(targets, data);

sdk.onData(callback): Registers a listener for incoming P2P data packets from other instances of your app.

sdk.onData((fromPeerId, payload) => {
    console.log(`Message from ${fromPeerId}:`, payload);
});

sdk.onProfileUpdate(callback): Listens for local pseudonym changes in iCanText to keep your UI in sync.

sdk.onProfileUpdate((newPseudonym) => {
    document.getElementById('display-name').textContent = newPseudonym;
});

sdk.getServerTime(): Fetches a synchronized timestamp from the iCanText server to prevent clock drift issues.

const timeStr = await sdk.getServerTime(); 
// Returns string format: YYYYMMDDHHMMSS (e.g. "20260202101030")

sdk.closeApp(): Requests iCanText to close the current application session (works for both iframes and popups).

// Close the app when the user clicks a "Quit" button
document.getElementById('quit-btn').onclick = () => sdk.closeApp();

sdk.postMessage(payload): Posts a message or a file to the channel under the user's identity. Returns the new messageId.

// Post Text
const id = await sdk.postMessage({ text: "Hello!" });

// Post Image/File
sdk.postMessage({ 
    file: { 
        name: "image.png", 
        type: "image/png", 
        data: "data:image/png;base64,..." 
    } 
});

sdk.editMessage(messageId, newText): Updates the content of a previously posted text message. Only works for messages owned by the user.

await sdk.editMessage("msg_123...", "Updated text content");

sdk.postAppContent(url, initialContext, options): Creates a new interactive block in the channel.
The initialContext is a JSON object representing the app's state (e.g., poll questions).
The options object can specify the interactionModel ('OWNER_ONLY' or 'COLLABORATIVE').

// From a configuration popup:
sdk.postAppContent("https://vote.app/", { 
    question: "Pizza or Tacos?", 
    votes: {} 
}, { 
    interactionModel: 'COLLABORATIVE' 
});

sdk.getAppContext(): Used by the app running inside a message. Fetches the current JSON state associated with this specific message instance.

// On app startup inside the chat:
const state = await sdk.getAppContext();
console.log("Question:", state.question);

sdk.updateAppContext(newContext): Updates the shared state for everyone. This triggers a signed P2P update. Perfect for real-time collaboration (votes, counters, etc.).

// When a user interacts (e.g. votes):
state.votes[sdk.localPeerId] = "Pizza";
sdk.updateAppContext(state); 
// Result: All channel members see the update instantly.

sdk.getDisplayMode(): Identifies where the app is currently rendered. Returns 'HEADER', 'POPUP', or 'MESSAGE'.

const mode = sdk.getDisplayMode();

if (mode === 'MESSAGE') {
    renderCompactView(); // Simplified UI for the chat timeline
} else {
    renderFullEditor();  // Full UI for configuration or popups
}

sdk.onContextUpdate(callback): Essential for real-time apps. Registers a listener that triggers whenever another user updates the context of this message (e.g., someone else voted).

sdk.onContextUpdate((updatedContext) => {
    console.log("New votes received!");
    refreshUI(updatedContext.votes);
});

sdk.getMessageId(): Returns the unique iCanText ID of the message containing the app. Useful for advanced local caching or logging.

const mid = sdk.getMessageId();
console.log("This app instance belongs to message:", mid);

sdk.emitAppEvent(eventData): Emits a persistent, cryptographically signed event linked to this message. Use this for cumulative data like votes or chat-within-an-app.

// Example: Cast a vote in a poll
sdk.emitAppEvent({ vote: 'Option A' });

sdk.onAppEvent(callback): Registers a real-time listener for app-specific events emitted by other users in the channel.

sdk.onAppEvent((senderId, data, timestamp) => {
    console.log(`User ${senderId} voted for:`, data.vote);
    recalculateUI();
});

sdk.getAppEvents(): Fetches the complete history of events for this specific app instance from the P2P Universal Ledger. Returns a Promise resolving to an array of event objects.

// On startup: Replay history to show the current score
const events = await sdk.getAppEvents();
events.forEach(e => processVote(e.senderId, e.data.vote));

sdk.setFullscreen(scope): Requests iCanText to expand the application's display area for an immersive experience. In STANDALONE mode, this method automatically triggers the browser's native Fullscreen API.

  • scope: 'VIEWPORT' (App covers the entire browser window, over iCanText UI) or 'CHANNEL' (App covers only the chat area, keeping the channel header visible).
// Switch the app to total fullscreen
sdk.setFullscreen('VIEWPORT');

sdk.exitFullscreen(): Requests iCanText to return the application to its original size (back to the message bubble in the timeline or the reduced panel in the header). Reverts native fullscreen if the app is in STANDALONE mode.

await sdk.exitFullscreen();

sdk.getIsAdmin(): Returns a boolean indicating whether the local user has administrative rights on the current channel. This includes both CHANNEL_ADMIN and WORKSPACE_ADMIN roles.

const isAdmin = sdk.getIsAdmin();

if (isAdmin) {
    renderAdminControls(); // Enable configuration or moderation tools
} else {
    renderUserView();      // Restrict UI to standard participation
}

Security Reminder: This method is intended for UI adaptation purposes (UX). Because iCanText is decentralized, actual authorization is enforced at the protocol level. Any restricted action (like updating a shared context or emitting administrative events) will be cryptographically verified and rejected by other peers if the user lacks the required proofWallet certificates, even if the local UI is modified.

Publishing & Deployment

Once your micro-app is built and hosted on your servers, you have two ways to bring it into iCanText:

1. Sideloading (For Testing & Private Tools)

Any user can manually launch a web-hosted app by typing /app https://your-domain.com in the chat. This is perfect for local development, private testing, or internal company tools.

Note on Sideloaded Apps: To protect the P2P network, unofficial apps launched via the /app command are subject to strict Anti-Flood limits (max 3 messages per 10 seconds, 6 per minute). If your app requires intense real-time syncing, you must publish it to the Store.

2. The Official iCanText Store

For public distribution, you can submit your application to the iCanText Store. Apps installed via the official store benefit from major advantages:

SDK License & Source Code

MIT License: The ICTBridgeSDK.js file is open-source and free to use. You are completely free to embed, modify, and distribute it within your own commercial or open-source applications without any restrictions.

The SDK is a zero-dependency JavaScript class. It is designed to be robust against standalone execution and elegantly handles the multiplexing of postMessage signals between iCanText and your application logic.

ICTBridgeSDK.js (Full Implementation)

/* SDK code loading... */

Usage Example: Shared Sticky Notes

Below is a minimal example of a collaborative app where users send notes to each other.

Implementation Example

const sdk = new ICTBridgeSDK('my_collab_app');

async function startApp() {
    const connection = await sdk.init();
    
    if (connection.status === 'READY') {
        console.log(`Connected as: ${connection.localPseudonym}`);

        // Listen for incoming notes
        sdk.onData((from, payload) => {
            alert(`Note from ${from}: ${payload.text}`);
        });

        // Send a note to all known peers
        const peers = await sdk.getPeers();
        const peerIds = peers.map(p => p.id);
        sdk.sendData(peerIds, { text: 'Hello decentralized world!' });
    }
}

startApp();

Complete Usage Example

This example shows a simple "Status Update" app. When launched through iCanText, it fetches your contacts and allows you to broadcast your current activity to them.

index.html (Application Logic)

// 1. Initialize the SDK with a unique Application ID
		const sdk = new ICTBridgeSDK('p2p_status_tracker');

		async function setup() {
		    // 2. Start the handshake with the parent iCanText window
		    const connection = await sdk.init();

		    if (connection.status === 'STANDALONE') {
			showUIError('Please launch this app from within iCanText using /app');
			return;
		    }

		    // 3. Handle incoming data from other peers
		    sdk.onData((senderId, payload) => {
			console.log(`Peer ${senderId} is now: ${payload.status}`);
			updateStatusBoard(senderId, payload.status);
		    });

		    // 4. Discovery: Fetch the list of peers in the current Workspace
		    const peers = await sdk.getPeers();
		    renderContactList(peers);
		}

		// 5. Sending Data: Encrypted P2P transmission
		function broadcastMyStatus(newStatus) {
		    sdk.getPeers().then(peers => {
			const targetIds = peers.map(p => p.id);
			
			// This JSON object will be E2EE encrypted and routed via Sealed Forward
			const packet = { 
			    type: 'STATUS_UPDATE', 
			    status: newStatus,
			    timestamp: Date.now() 
			};

			sdk.sendData(targetIds, packet);
		    });
		}

		setup();

Example: Self-Editing Bot

Micro-apps can act as assistants. This function posts a message and updates it after a short delay.

Implementation

async function sayHello() {
    // 1. Create the message
    const msgId = await sdk.postMessage({ 
        text: "I am computing the results..." 
    });
    
    // 2. Simulate work and update the same message
    setTimeout(async () => {
        await sdk.editMessage(msgId, "Computation complete: 42 âś…");
    }, 3000);
}

Example: Decentralized Poll (Event Sourcing)

In this example, the parent message is OWNER_ONLY to prevent others from changing the poll question. However, everyone uses emitAppEvent to cast their individual votes.

1. Creating the Poll (Configuration UI)

// The author creates the poll message
sdk.postAppContent("https://poll.app/", 
    { question: "Which consensus is best?" }, 
    { interactionModel: 'OWNER_ONLY' } // Protect the question
);

2. Participating (Inside the Chat Timeline)

const voteTally = {};

async function start() {
    await sdk.init();

    // Replay all votes from the Ledger (even if the author is offline)
    const history = await sdk.getAppEvents();
    history.forEach(evt => countVote(evt.senderId, evt.data.choice));

    // Listen for incoming votes from peers
    sdk.onAppEvent((senderId, data) => {
        countVote(senderId, data.choice);
        updateCharts();
    });
}

function castVote(myChoice) {
    // Anyone can emit an event, even if they don't own the parent message
    sdk.emitAppEvent({ choice: myChoice });
}

Network Requests (CORS)

For security reasons, iCanText renders micro-apps in a strict sandbox without the allow-same-origin directive. This means your app has an opaque origin (null).

If your app needs to fetch() data from its own server (like a JSON list or a database), your server must explicitly authorize the null origin via CORS headers.

PHP Implementation Example (Selective CORS)

// list.php
<?php
$allowed_origins = [
    "https://icantext.com",
    "null" // Required for sandboxed iframes
];

if (isset($_SERVER['HTTP_ORIGIN'])) {
    $origin = $_SERVER['HTTP_ORIGIN'];
    if (in_array($origin, $allowed_origins)) {
        header("Access-Control-Allow-Origin: " . $origin);
        header("Access-Control-Allow-Credentials: true");
    }
}
// ... your logic here

Security Tip: Avoid using Access-Control-Allow-Origin: * in production. Always use a whitelist to ensure only iCanText and your official domain can access your API.

Standalone User Experience

Since micro-apps are standard web pages, users can navigate to your URL directly. The sdk.init() promise helps you detect this state via the STANDALONE status.

Best Practice: Conditional Rendering

Unless your application is designed to work fully without a P2P connection, you should display a landing page or a user manual when in standalone mode. This informs the user that the app is intended to be used within iCanText.

UX Logic Example

const connection = await sdk.init();

if (connection.status === 'STANDALONE') {
    // Show the "How to install" guide or app description
    renderLandingPage(); 
} else {
    // Start the collaborative real-time logic
    initializeP2PLogic();
}

Pro Tip: Use the standalone page to showcase your app's features and provide the specific /app command for users to copy and paste into their iCanText channels.

Best Practices