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.
updateAppContext.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 interactionModel option in postAppContent defines who has the right to modify the original message instance itself:
updateAppContext. Other members can still contribute via emitAppEvent. This is the safest mode for Polls.updateAppContext to overwrite the shared state.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.
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:
/app https://your-app.com.ICT_HELLO signal via postMessage to the parent window.ICT_INIT payload containing the instanceId, the user's localPeerId, localPseudonym, and its Display Mode.READY. All subsequent P2P operations are cryptographically locked to this specific app instance.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>
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.
const peers = await sdk.getPeers();
// peer: {id, pseudonym, verified, isActiveOnApp, presenceState, isActuallyOnline}
const activePeers = peers.filter(p => p.isActiveOnApp);
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[myId] = "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.
The ICTBridgeSDK is a zero-dependency JavaScript class. It is designed to be robust against standalone execution and handles the multiplexing of messages between iCanText and your application logic.
ICTBridgeSDK.js (Full Implementation)
/* SDK code loading... */
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();
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();
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);
}
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 });
}
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.
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.
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.
window.opener before sending data; if the user closes iCanText, the bridge will break.payload received in onData as it comes from a remote peer's application instance.Content-Security-Policy does not have a restrictive frame-ancestors directive, and avoid the X-Frame-Options: DENY header.
updateAppContext for small, non-conflicting metadata (like the name of a room). Use Event Sourcing (emitAppEvent) for collaborative tools where multiple users act simultaneously to avoid data loss.app-event is emitted, it is stored by all peers in a dedicated memory space called the Ledger. This ledger is not limited by the standard 50-message chat history. However, to prevent memory leaks, iCanText automatically wipes the entire ledger of an app as soon as its parent message is deleted or expires.onPeersUpdate, presence is reached via consensus.
If a user closes their tab without signaling, they will transition to SUSPECT then OFFLINE
within 3 cycles. This prevents "ghost" users in the UI while keeping network traffic minimal.
VIEWPORT fullscreen immediately upon app launch. Let the user trigger the expansion via a dedicated button to avoid breaking their reading flow.