diff --git a/_includes/live-demos/comments-callback/index.js b/_includes/live-demos/comments-callback/index.js
new file mode 100644
index 000000000..26d90e10a
--- /dev/null
+++ b/_includes/live-demos/comments-callback/index.js
@@ -0,0 +1,686 @@
+tinymce.ScriptLoader.loadScripts(
+ [
+ '//unpkg.com/@pollyjs/core@5.1.1',
+ '//unpkg.com/@pollyjs/adapter-fetch@5.1.1',
+ '//unpkg.com/@pollyjs/persister-local-storage@5.1.1',
+ ],
+ () => {
+ /******************************
+ * Mock server implementation *
+ ******************************/
+
+ let { Polly } = window['@pollyjs/core'];
+ let FetchAdapter = window['@pollyjs/adapter-fetch'];
+ let LocalStoragePersister = window['@pollyjs/persister-local-storage'];
+
+ Polly.register(FetchAdapter);
+ Polly.register(LocalStoragePersister);
+ let polly = new Polly('test', {
+ adapters: ['fetch'],
+ persister: 'local-storage',
+ });
+ let server = polly.server;
+
+ server.any().on('request', (req) => {
+ console.log('Server request:', req);
+ });
+
+ server.any().on('beforeResponse', (req, res) => {
+ console.log('Server response:', res);
+ });
+
+ /* this would be an admin for the file, they're allowed to do all operations */
+ function getOwner() {
+ return localStorage.getItem('owner') ?? users[0].id;
+ }
+
+ /* Server knows the author, probably by cookie or JWT token */
+ function getAuthor() {
+ return localStorage.getItem('author') ?? users[0].id;
+ }
+
+ /* this would be an admin for the file, they're allowed to do all operations */
+ function setOwner(user) {
+ localStorage.setItem('owner', user) ?? users[0].id;
+ }
+
+ /* Server knows the author, probably by cookie or JWT token */
+ function setAuthor(user) {
+ localStorage.setItem('author', user) ?? users[0].id;
+ }
+
+ function randomString() {
+ /* ~62 bits of randomness, so very unlikely to collide for <100K uses */
+ return Math.random().toString(36).substring(2, 14);
+ }
+
+ /* Our server "database" */
+ function getDB() {
+ return JSON.parse(localStorage.getItem('fakedb') ?? '{}');
+ }
+ function setDB(data) {
+ localStorage.setItem('fakedb', JSON.stringify(data));
+ }
+
+ function getConversation(uid) {
+ let store = getDB();
+ console.log('DB get:', uid, store[uid]);
+ return store[uid];
+ }
+
+ function setConversation(uid, conversation) {
+ let store = getDB();
+ console.log('DB set:', uid, store[uid], conversation);
+ store[uid] = conversation;
+ setDB(store);
+ }
+
+ function deleteConversation(uid) {
+ let store = getDB();
+ console.log('DB delete:', uid);
+ delete store[uid];
+ setDB(store);
+ }
+
+ function deleteAllConversations() {
+ console.log('DB delete all');
+ let store = {};
+ setDB(store);
+ }
+
+ server.host('https://api.example', () => {
+ /* create new conversation */
+ server.post('/conversations/').intercept((req, res) => {
+ let author = getAuthor();
+ let { content, createdAt } = JSON.parse(req.body);
+ console.log(req.body);
+ try {
+ let conversationUid = randomString();
+ setConversation(conversationUid, [
+ {
+ author,
+ createdAt,
+ modifiedAt: createdAt,
+ content,
+ uid: conversationUid /* first comment has same uid as conversation */,
+ },
+ ]);
+ res.status(201).json({ conversationUid });
+ } catch (e) {
+ console.log('Server error:', e);
+ res.status(500);
+ }
+ });
+
+ /* add new comment to conversation */
+ server.post('/conversations/:conversationUid').intercept((req, res) => {
+ let author = getAuthor();
+ let { content, createdAt } = JSON.parse(req.body);
+ let conversationUid = req.params.conversationUid;
+ try {
+ let conversation = getConversation(conversationUid);
+ let commentUid = randomString();
+ setConversation(
+ conversationUid,
+ conversation.concat([
+ {
+ author,
+ createdAt,
+ modifiedAt: createdAt,
+ content,
+ uid: commentUid,
+ },
+ ])
+ );
+ res.status(201).json({ commentUid });
+ } catch (e) {
+ console.log('Server error:', e);
+ res.status(500);
+ }
+ });
+
+ /* edit a comment */
+ server
+ .put('/conversations/:conversationUid/:commentUid')
+ .intercept((req, res) => {
+ let author = getAuthor();
+ let { content, modifiedAt } = JSON.parse(req.body);
+ let conversationUid = req.params.conversationUid;
+ let commentUid = req.params.commentUid;
+
+ try {
+ let conversation = getConversation(conversationUid);
+ let commentIndex = conversation.findIndex((comment) => {
+ return comment.uid === commentUid;
+ });
+ let comment = conversation[commentIndex];
+ let canEdit = comment.author === author;
+ if (canEdit) {
+ setConversation(conversationUid, [
+ ...conversation.slice(0, commentIndex),
+ {
+ ...comment,
+ content,
+ modifiedAt,
+ },
+ ...conversation.slice(commentIndex + 1),
+ ]);
+ }
+ res.status(201).json({ canEdit });
+ } catch (e) {
+ console.log('Server error:', e);
+ res.status(500);
+ }
+ });
+
+ /* delete a comment */
+ server
+ .delete('/conversations/:conversationUid/:commentUid')
+ .intercept((req, res) => {
+ let author = getAuthor();
+ let owner = getOwner();
+ let conversationUid = req.params.conversationUid;
+ let commentUid = req.params.commentUid;
+ let conversation = getConversation(conversationUid);
+ if (!conversation) {
+ res.status(404);
+ }
+ let commentIndex = conversation.findIndex((comment) => {
+ return comment.uid === commentUid;
+ });
+ if (commentIndex === -1) {
+ res.status(404);
+ }
+ if (
+ conversation[commentIndex].author === author ||
+ author === owner
+ ) {
+ setConversation(conversationUid, [
+ ...conversation.slice(0, commentIndex),
+ ...conversation.slice(commentIndex + 1),
+ ]);
+ res.status(204);
+ } else {
+ res.status(403);
+ }
+ });
+
+ /* delete a conversation */
+ server.delete('/conversations/:conversationUid').intercept((req, res) => {
+ let author = getAuthor();
+ let owner = getOwner();
+ let conversationUid = req.params.conversationUid;
+ let conversation = getConversation(conversationUid);
+ if (conversation) {
+ if (conversation[0].author === author || author === owner) {
+ deleteConversation(conversationUid);
+ res.status(204);
+ } else {
+ res.status(403);
+ }
+ } else {
+ res.status(404);
+ }
+ });
+
+ /* delete all conversations */
+ server.delete('/conversations').intercept((req, res) => {
+ let author = getAuthor();
+ let owner = getOwner();
+ if (author === owner) {
+ deleteAllConversations();
+ res.status(204);
+ } else {
+ res.status(403);
+ }
+ });
+
+ /* lookup a conversation */
+ server.get('/conversations/:conversationUid').intercept((req, res) => {
+ let conversation = getConversation(req.params.conversationUid);
+ if (conversation) {
+ res.status(200).json(conversation);
+ } else {
+ res.status(404);
+ }
+ });
+
+ /* lookup users */
+ server.get('/users/').intercept((req, res) => {
+ res.status(200).json({
+ users,
+ });
+ });
+ }); /* server.host */
+
+ /* Connect using the `connectTo` API */
+ polly.connectTo('fetch');
+
+ /************************************************
+ * Fake Users and associated pickers *
+ * Should be based on sessions and backend data *
+ ***********************************************/
+
+ const users = [
+ { id: 'alex', displayName: 'Alex' },
+ { id: 'jessie', displayName: 'Jessie' },
+ { id: 'sam', displayName: 'Sam' },
+ ];
+
+ /* Set initial Owner */
+ setOwner(users[2].id);
+
+ /* Set initial Author */
+ setAuthor(users[0].id);
+
+ /********************************
+ * Tiny Comments functions *
+ * (must call "done" or "fail") *
+ ********************************/
+
+ /**
+ * Callback for when the operation was successful.
+ * @template T
+ * @callback done
+ * @param {T} data - the data
+ * @returns {void}
+ */
+
+ /**
+ * Callback for when the operation failed.
+ * @callback fail
+ * @param {string|Error} error - the reason for the failure
+ * @returns {void}
+ */
+
+ /**
+ * The data supplied to create a comment.
+ * @typedef {Object} TinyCommentsCreateReq
+ * @property {string} content - comment content
+ * @property {string} createdAt - ISO creation date
+ */
+
+ /**
+ * The response returned when a comment was created on the server.
+ * @typedef {Object} TinyCommentsCreateResp
+ * @property {string} conversationUid - ID of created comment
+ * @property {?fail} onError - error callback to call when the comment can't be put into the document
+ * @property {?done