Skip to content

Step 0: develop, in one process

A layout says which services run in which process. layouts/develop.toml, before moderation exists:

database = "sqlite:///guestbook.db"

[processes.all]
services = ["web", "book"]

[services.web]
port = "${PORT:-8000}"

[services.book]
database = "sqlite:///guestbook.db"

database at the top is where the framework's services keep their tables, here the events. Book's own database setting names the same file, since an entry and its event must share a transaction; with two different databases, publish would write to one and the consumers would read the other. ${PORT:-8000} reads the environment, with a default.

$ myxo migrate layouts/develop.toml
2026-10-19 07:59:48,979 8773 myxo INFO migrated book
2026-10-19 07:59:48,979 8773 myxo INFO migrated web
$ myxo serve layouts/develop.toml
$ curl -s -X POST localhost:8000/entries -H 'content-type: application/json' \
    -d '{"name": "Ada", "message": "Lovely day"}'
{"id":1}
$ curl -s localhost:8000/entries
{"entries":[{"id":1,"name":"Ada","message":"Lovely day","flagged":false}]}

In one process, web calls book directly, as a method call, with the same timeout and errors as across a network.

Adding a service

Moderation flags the entries that use a banned word. It follows book's Signed events, after their commit (src/guestbook/moderation.py):

class Moderation(Service):
    name = "moderation"
    stage = 2

    def __init__(self, config: object, runtime: Runtime) -> None:
        super().__init__(config, runtime)
        self.book = runtime.client(BookAPI)

    @subscribe(Signed)
    async def check(self, event: Signed) -> None:
        entry = await self.book.get(EntryRef(event.entry_id))
        if BANNED & set(entry.message.lower().split()):
            await self.book.flag(EntryRef(entry.id))

@subscribe(Signed) delivers each event at least once to the consumer group moderation, the service's name: once in the whole installation, however many replicas run it, in the order of the commits. A handler may see an event twice after a crash, so it must be harmless to repeat, as flagging is. If book is unavailable, the handler raises Unavailable and the event comes back after a backoff. Any other error stops the consumer, and the runtime's supervisor restarts it.

Declare the entry point (above), install the package again (uv sync), since the runtime reads entry points from the installed package, add "moderation" to the services of processes.all, and restart. A consumer group that is new starts from the oldest event kept, so moderation also checks the entries signed before it existed, within the hour that events are kept:

$ myxo serve layouts/develop.toml
$ curl -s -X POST localhost:8000/entries -H 'content-type: application/json' \
    -d '{"name": "Bot", "message": "cheap casino here"}'
$ curl -s localhost:8000/entries
{"entries":[...,{"id":2,"name":"Bot","message":"cheap casino here","flagged":true}]}

Next, read step 1, one box.