Intro
Webxdc is a new way to create and share web apps in messenger chat groups. Anyone can build HTML5 apps, package it as a .xdc
file, and drop it in a chat to share with friends. Once shared, chat participants can simply click "Start" to run the app and interact with each other through the app.
Webxdc apps can only send and receive messages in the chat via the messenger's Javascript API. Thus they automatically get end-to-end encryption and peer discovery for free.
Webxdc is specified through its file format for packaging as a .xdc
file, Javascript API for developing web apps, and Messenger implementation for running and sharing Webxdc apps in chats. Webxdc is supported by and evolving with the Delta Chat e-mail messenger, which serves as the default reference.
See the webxdc GitHub organization for further developments and feel free to post questions or suggestions in the Webxdc support forum.
Getting Started
A simple example
The following index.html
shows a complete webxdc app, with an input field shown on all peers. Data submitted from the input is delivered to all members of the chat.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<script src="webxdc.js"></script>
</head>
<body>
<input id="input" type="text"/>
<a href="" onclick="sendMsg(); return false;">Send</a>
<p id="output"></p>
<script>
function sendMsg() {
msg = document.getElementById("input").value;
window.webxdc.sendUpdate({payload: msg}, 'Someone typed "'+msg+'".');
}
function receiveUpdate(update) {
document.getElementById('output').innerHTML += update.payload + "<br>";
}
window.webxdc.setUpdateListener(receiveUpdate, 0);
</script>
</body>
</html>
To package the app as a .xdc
file, zip the directory containing the index.html
:
zip -9 --recurse-paths "myapp.xdc" PATH_TO_DIR
Now it's possible to share the myapp.xdc
file in any chat: recipients can hit "Start" to begin using the app to enter text in this input field and send updates to each other.
To simulate multiple chat participants in the browser, try Hello as a minimal example; it includes everything needed to run the app and requires no build systems.
More examples
Source code for apps referenced on the webxdc home page can be found in the Webxdc GitHub organization.
Participating in developments
-
Webxdc GitHub organization: contains many example app repositories. Please tag your own app experiments with "webxdc" so that others can find it via the
#webxdc
Topic on GitHub. -
Support Forum: the webxdc category on the DeltaChat forum is a space to ask questions and announce your app projects. Log into the forum via DeltaChat, Github, or by creating a username and password there.
-
Announcements: Delta Chat and Webxdc-related developments can be followed on Fediverse or Twitter.
Webxdc Specification
Webxdc is a fresh and still evolving way of running web apps in chat messengers.
This document is for app developers and outlines the webxdc API and .xdc
file format. It also describes the constraints for a messenger implementation when running webxdc apps.
Webxdc API
Webxdc apps are shared in a chat and each device runs its own instance on the recipients device when they click "Start". The apps are network-isolated but can share state via sendUpdate()
and setUpdateListener()
.
Messenger implementations expose the API through a webxdc.js
module. To activate the webxdc API you need to use a script reference for webxdc.js
in your HTML5 app:
<script src="webxdc.js"></script>
webxdc.js
must not be added to your .xdc
file as they are provided by the messenger. To simulate webxdc in a browser, use the webxdc.js
file from Hello.
sendUpdate()
window.webxdc.sendUpdate(update, descr);
-
update
: an object with the following properties:update.payload
: any javascript primitive, array or object.update.info
: optional, short, informational message that will be added to the chat, e.g. "Alice voted" or "Bob scored 123 in MyGame". usually only one line of text is shown and if there are series of info messages, older ones may be dropped. use this option sparingly to not spam the chat.update.document
: optional, name of the document in edit, must not be used e.g. in games where the webxdc does not create documentsupdate.summary
: optional, short text, shown beside the app icon; it is recommended to use some aggregated value, e.g. "8 votes", "Highscore: 123"
-
descr
: short, human-readable description what this update is about. this is shown e.g. as a fallback text in an e-mail program.
All peers, including the sending one,
will receive the update by the callback given to setUpdateListener()
.
There are situations where the user cannot send messages to a chat,
e.g. if the webxdc instance comes as a contact request or if the user has left a group.
In these cases, you can still call sendUpdate()
,
however, the update won't be sent to other peers
and you won't get the update by setUpdateListener()
.
setUpdateListener()
let promise = window.webxdc.setUpdateListener((update) => {}, serial);
With setUpdateListener()
you define a callback that receives the updates
sent by sendUpdate()
. The callback is called for updates sent by you or other peers.
The serial
specifies the last serial that you know about (defaults to 0).
The returned promise resolves when the listener has processed all the update messages known at the time when setUpdateListener
was called.
Each update
which is passed to the callback comes with the following properties:
-
update.payload
: equals the payload given tosendUpdate()
-
update.serial
: the serial number of this update. Serials are larger0
and newer serials have higher numbers. There may be gaps in the serials and it is not guaranteed that the next serial is exactly incremented by one. -
update.max_serial
: the maximum serial currently known. Ifmax_serial
equalsserial
this update is the last update (until new network messages arrive). -
update.info
: optional, short, informational message (seesendUpdate()
) -
update.document
: optional, document name as set by the sender, (seesendUpdate()
), implementations show the document name e.g. beside the app icon or in the title bar -
update.summary
: optional, short text, shown beside icon (seesendUpdate()
)
selfAddr
window.webxdc.selfAddr
Email address of the current account.
Especially useful if you want to differentiate between different peers -
just send the address along with the payload,
and, if needed, compare the payload addresses against selfAddr
later on.
selfName
window.webxdc.selfName
Name of the current account, as defined in settings. If empty, this defaults to the peer's address.
Other APIs and Tags Usage Hints
Webxdc apps run in a restricted environment, but the following practices are permitted:
localStorage
,sessionStorage
,indexedDB
visibilitychange
eventswindow.navigator.language
- internal links, such as
<a href="localfile.html">
mailto
links, such as<a href="mailto:addr@example.org?body=...">
<meta name="viewport" ...>
is useful especially as webviews from different platforms have different defaults
Discouraged Practises
document.cookie
is known not to work on desktop and iOS—uselocalStorage
insteadunload
,beforeunload
andpagehide
events are known not to work on iOS and are flaky on other systems (also partly discouraged by mozilla)—usevisibilitychange
instead<title>
anddocument.title
is ignored by Webxdc; use thename
property frommanifest.toml
instead- the latest JavaScript features may not work on all webviews, you may want to transpile your code down to an older js version e.g. with https://babeljs.io
<a href="https://example.org/foo">
and other external links are blocked by definition; instead, embed content or usemailto:
link to offer a way for contact<input type="file">
is currently discouraged; this may change in future
Webxdc File Format
- a Webxdc app is a ZIP-file with the extension
.xdc
- the ZIP-file MUST use the default compression methods as of RFC 1950, this is "Deflate" or "Store"
- the ZIP-file MUST contain at least the file
index.html
- the ZIP-file MAY contain a
manifest.toml
andicon.png
oricon.jpg
files - if the webxdc app is started,
index.html
MUST be opened in a restricted webview that only allows accessing resources from the ZIP-file.
The manifest.toml File
If the ZIP-file contains a manifest.toml
in its root directory,
the following basic information MUST be read from it:
name = "My App Name"
source_code_url = "https://example.org/orga/repo"
-
name
- The name of the webxdc app. If no name is set or if there is no manifest, the filename is used as the webxdc name. -
source_code_url
- Optional URL where the source code of the webxdc and maybe other information can be found. Messenger implementors may make the url accessible via a "Help" menu in the webxdc window.
Icon Files
If the ZIP-root contains an icon.png
or icon.jpg
,
these files are used as the icon for the webxdc.
The icon should be a square at reasonable width/height.
Round corners, circle cut out etc. will be added by the implementations as needed;
do not add borders or shapes to the icon therefore.
If no icon is set, a default icon will be used.
Messenger Implementation
Webview Constraints for Running Apps
When starting a web view for a webxdc app to run, messenger implementors:
-
MUST deny all forms of internet access. If you don't do this unsuspecting users may leak data of their private interactions to outside third parties. You do not need to offer "privacy" or "cookie" consent screens as there is no way the app can transfer user data to anything outside the chat.
-
MUST allow unrestricted use of DOM storage (local storage, indexed db and co), but make sure it is scoped to each webxdc app so they can not delete or modify the data of other webxdc content.
-
MUST inject
webxdc.js
and implement the Webxdc API so that messages are relayed and shown in chats. -
MUST make sure the standard JavaScript API works as described at Other APIs and Tags Usage Hints.
UI Interactions in Chats
-
Text from
update.info
should be shown in the chats and tapping them should jump to their webxdc message -
The most recent text from
update.document
andupdate.summary
should be shown inside the webxdc message, together with name and icon.
Only one line of text should be shown and truncation is fine as webxdc devs should not be encourated to send long texts here. -
A "Start" button should run the webxdc app
Example Messenger Implementations
Tips and Tricks
Typescript support
How to get autocompletion for window.webxdc api in your IDE via Typescript.
Get the Typescript Definitions
Just copy webxdc.d.ts
into your source dir:
type SendingStatusUpdate<T> = {
/** the payload, deserialized json:
* any javascript primitive, array or object. */
payload: T;
/** optional, short, informational message that will be added to the chat,
* eg. "Alice voted" or "Bob scored 123 in MyGame";
* usually only one line of text is shown,
* use this option sparingly to not spam the chat. */
info?: string;
/** optional, if the Webxdc creates a document, you can set this to the name of the document;
* do not set if the Webxdc does not create a document */
document?: string;
/** optional, short text, shown beside the icon;
* it is recommended to use some aggregated value,
* eg. "8 votes", "Highscore: 123" */
summary?: string;
};
type ReceivedStatusUpdate<T> = {
/** the payload, deserialized json */
payload: T;
/** the serial number of this update. Serials are larger than 0 and newer serials have higher numbers */
serial: number;
/** the maximum serial currently known */
max_serial: number;
/** optional, short, informational message. */
info?: string;
/** optional, if the Webxdc creates a document, this is the name of the document;
* not set if the Webxdc does not create a document */
document?: string;
/** optional, short text, shown beside the webxdc's icon. */
summary?: string;
};
interface Webxdc<T> {
/** Returns the peer's own address.
* This is esp. useful if you want to differ between different peers - just send the address along with the payload,
* and, if needed, compare the payload addresses against selfAddr() later on. */
selfAddr: string;
/** Returns the peer's own name. This is name chosen by the user in their settings, if there is nothing set, that defaults to the peer's address. */
selfName: string;
/**
* set a listener for new status updates.
* The "serial" specifies the last serial that you know about (defaults to 0).
* Note that own status updates, that you send with {@link sendUpdate}, also trigger this method
* @returns promise that resolves when the listener has processed all the update messages known at the time when `setUpdateListener` was called.
* */
setUpdateListener(cb: (statusUpdate: ReceivedStatusUpdate<T>) => void, serial?: number): Promise<void>;
/**
* WARNING! This function is deprecated, see setUpdateListener().
*/
getAllUpdates(): Promise<ReceivedStatusUpdate<T>[]>;
/**
* Webxdc are usually shared in a chat and run independently on each peer. To get a shared status, the peers use sendUpdate() to send updates to each other.
* @param update status update to send
* @param description short, human-readable description what this update is about. this is shown eg. as a fallback text in an email program.
*/
sendUpdate(update: SendingStatusUpdate<T>, description: string): void;
}
////////// ANCHOR: global
declare global {
interface Window {
webxdc: Webxdc<any>;
}
}
////////// ANCHOR_END: global
export { SendingStatusUpdate, ReceivedStatusUpdate, Webxdc };
you can find it also on https://github.com/webxdc/webxdc_docs/blob/master/webxdc.d.ts or just copy the code block above.
In the future this might become an @types npm module, but for now it is what it is: a simple file copy with no automatic updates.
Using types
Start by importing the file.
In Typescript:
import type { Webxdc } from './webxdc.d.ts'
In Javascript:
/**
* @typedef {import('./webxdc').Webxdc} Webxdc
*/
This works in VS Code nicely together with the //@ts-check
comment on top of your source file.
If you want you can also type your own functions using JSDoc comments.
If you don't use VS Code you can still make use of the type checking with the Typescript compiler:
npm i -g typescript # -g stands for global installation tsc --noEmit --allowJs --lib es2015,dom *.js
Own Payload Type
If you have a type for your state update payloads, replace the any
in Webxdc<any>
with your own payload type:
declare global {
interface Window {
webxdc: Webxdc<any>;
}
}
Transpile Newer Javascript With Babel.js
Older devices might not have the newest javascript features/syntax in their webview, you may want to transpile your code down to an older JavaScript version eg. with Babel.
Targets:
- Desktop (electron -> is chrome 91)
- iOS (iOS 11 -> webkit 604.1.38)
- android (android 5 -> the webview system component can be updated by the user: https://play.google.com/store/apps/details?id=com.google.android.webview)
If you want to use a newer API make sure to check on https://caniuse.com. If you just want to use newer JavaScript syntax, babel.js is the right tool for you - it translates new JS into older JS, that can be interpreted.
Debugging With eruda.js
When you can not use debugging inside Deltachat, either because you have no computer to connect to or are on iOS, you can use eruda.js as an alternative to the native dev tools.
Installing eruda
Get eruda.js
from https://github.com/liriliri/eruda, copy it next to your index.html
and then add this snippet into the head section of your index.html, before all other scripts:
<script src="eruda.js"></script>
<script>
eruda.init();
</script>
Using eruda
When you open the webxdc a floating button will appear in a corner, tap it to see the developer tools.
Debugging Inside Deltachat
Debug Your webxdc Content in Android via Chrome DevTools
- enable webView debugging in delta chat settings
Settings
>Advanced
>Developer Mode
: - enable developer mode and ADB debugging on your device (go to system settings, device info, spam click on build number until there is a toast telling you that you are now a "Developer", then go into the developer menu that just appeared and enable "ADB debugging", see also android docs: Enable ADB debugging on your device).
- connect your device via USB to your computer
- open chromium (or google chrome) and go to
chrome://inspect/#devices
- start your webxdc that you want to debug
- click on
inspect
:
Inpect HTML | Javascript Console |
---|---|
![]() | ![]() |
Make sure to disable adb debugging again after you are done with debugging!
Debug Your webxdc Content in DeltaChat Desktop
Start the webxdc you want to debug and press F12
to open the developer tools:
A bit small isn't it? fix it either by resizing the window's width or undock the developer tools:
Optimizing Your App Icon
There are several things you can do to shrink down the size of your icon:
- save without thumbnail image (in gimp it can be done in the export dialog)
- shrink the image resolution (
256px
are enough, in some cases128px
or even lower like64px
can sufice) - change your PNG colors from
RGB
toIndexed
(in gimpImage
->Mode
->Indexed
, see https://docs.gimp.org/en/gimp-image-convert-indexed.html)
For
png
you can also use theoxipng
tool (https://github.com/shssoichiro/oxipng), which automagically optimizes your icon's file size without quality loss:oxipng icon.png -s -o max
If you have png files in your project, you should also do this them to safe even more bytes.
Noteworthy parameters:
--pretend
only calculates gains-Z
even more compression, but takes longer- for more info see
oxipng --help
# Troubleshooting
I Cannot Share Variables on iOS Between Scripts!
Your code:
a.js
const CONFIG = { difficulty: "hard", hasCoins: true };
b.js
if (CONFIG.difficulty == "hard") {
/** make the game harder **/
}
index.html
<html>
<head>
<!-- ... -->
</head>
<body>
<!-- ... -->
<script src="a.js"></script>
<script src="b.js"></script>
</body>
</html>
Basically you get many errors in your JS console like this:
Can't find variable: CONFIG
There are a few ways to solve this:
- use a bundle to bundle all your JS to one file (some bundlers: parcel, webpack, esbuild)
- use esm modules (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules)
- define your variables as inline script in your HTML file. (inline script means that the script is in the HTML file between the
<script>
tags:<script>my code</script>
) - append your global variables to the window object:
window.myVar = 1;
and use them likeconsole.log(window.myVar)