Skip to content

No-Build Todo App

A collaborative todo app using plain HTML and JavaScript — no bundler required.

Project Structure

Your app directory can contain any files or assets — everything is hosted by Poe. The only requirements are:

  • index.html — required for all apps
  • synced-store-backend-config.js — required if your app uses Synced-Store (and you must call Poe.setupStore() in your client JS)
my-todo-app/
├── index.html                      # Entry point (required)
├── frontend.js                     # App logic
└── synced-store-backend-config.js  # Backend mutators and actions (required for Synced-Store)

Poe Employee Note

Currently the platform injects an import map into the app's index.html at serve time, which is what makes import { Poe } from "@poe/embed-api/v1.js" work without a bundler. In the future we'll probably want creators to include a script tag instead (e.g. <script src="https://poe.com/v1/embed-api.js"></script>) so the mechanism is more explicit and doesn't require server-side HTML rewriting.

index.html

html
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Todo App</title>
</head>
<body>
  <input id="input" placeholder="New todo" />
  <button id="add">Add</button>
  <ul id="list"></ul>
  <script type="module" src="frontend.js"></script>
</body>
</html>

frontend.js

javascript
import { Poe } from "@poe/embed-api/v1.js";

async function addTodo(ctx, input) {
  await ctx
    .table("todos")
    .set({ itemKey: input.id, value: { text: input.text } });
}

Poe.setupStore({ mutators: { addTodo }, schemaVersion: 1 });

// Subscribe to data changes — re-renders whenever data changes locally or from other users
Poe.store.subscribe(
  (tx) => tx.table("todos").entries().toArray(),
  (entries) => {
    const list = document.getElementById("list");
    list.innerHTML = entries.map(([, v]) => `<li>${v.text}</li>`).join("");
  },
);

// Handle adding todos
let nextId = 0;
document.getElementById("add").addEventListener("click", () => {
  const input = document.getElementById("input");
  const text = input.value.trim();
  if (text) {
    Poe.store.mutate["addTodo"]({ id: String(nextId++), text });
    input.value = "";
  }
});

synced-store-backend-config.js

The backend config exports the same mutators so the server can run them canonically.

javascript
async function addTodo(ctx, input) {
  await ctx
    .table("todos")
    .set({ itemKey: input.id, value: { text: input.text } });
}

export default {
  mutators: { addTodo },
  actions: {},
};

TIP

Mutators must behave identically on client and server. Keep the implementations in sync — or extract a shared file and import from both.

Upload to Poe

Publish the directory using the Slop-Poe CLI:

bash
slop-poe apps publish --handle my-todo-app --dir my-todo-app