Building Apps with Claude Code¶
The sobamail/skill repository is a Claude Code skill named sobamail-app. Once it is installed, Claude knows the platform the way these docs describe it: how a click in the UI becomes a mutation on every replica, which soba.* calls a replicated handler may use, how to declare a table or an event, how to keep import pins current, how to read an instance's mutation log, and how to inspect and drive your app in the running client. You keep working the way the tutorials show; the skill lets Claude do the same work with you.
Installing the Skill¶
Clone the repository into a skills directory. The folder name must be sobamail-app, because Claude Code takes the skill's name from it.
To make the skill available in every project:
To make it available in one project only, clone it under that project's .claude/skills directory instead. The develroot of your app is a natural place:
Update the skill the same way you update any git checkout: run git pull in the clone.
The mutlog.py script described below needs the msgpack Python module to decode mutation payloads. Without it the script still runs, but it prints each payload as hex.
Using the Skill¶
Open Claude Code in your develroot, the folder that holds Mutator.mjs. See Instantiating Your First App if you have not located it yet.
The skill loads on its own whenever a task touches a Sobamail app: a Mutator.mjs, the soba global, an object key of the {namespace}Name form, a ?sha224= import pin, or an instance database. You can also load it by hand by typing /sobamail-app in the Claude Code prompt.
Describe the change you want in the terms the Concepts chapter uses. Some requests that the skill is built for:
- "Add a
notestable with aNoteAddinsert event and aNoteGetrequest that returns the rows." - "The UI should show a settings card for the reminder interval."
- "Reply to incoming
{https://example.com/ns}Ordermessages with a confirmation mail." - "Why does deploying fail with
import URL must be?" - "Explain what this
process()method does."
The skill knows the rules that the runtime enforces, so Claude applies them without being told: a replicated event handler stays deterministic, a boolean never reaches the database, every table has exactly one insert event, and a changed module gets its import pins refreshed from the leaf modules up to Mutator.mjs.
Reload after a change to the subscription list
When static objects changes, the running instance does not notice by itself. Open the Application Manager, select the instance under Instances, and click Reload Objects. Claude will remind you, but it cannot click the button for you.
What the Skill Contains¶
The skill is a set of Markdown references plus two scripts. You can read the references yourself; they condense these docs and the conventions of the reference apps into a form made for quick lookup.
| Path | Content |
|---|---|
SKILL.md | The event flow from UI to replica, and the recipe for any backend change |
references/app-anatomy.md | Copyable shapes: Mutator statics, modules and namespaces, object classes |
references/runtime-api.md | Every soba.* call with its signature and whether a replicated handler may use it |
references/platform-concepts.md | The platform vocabulary |
references/wire-and-ui.md | The /api/v2/object wire format and the Svelte UI patterns |
references/dev-and-deploy.md | Developer mode, the tutorial sequence, import pins, deployment |
references/mutlog.md | The _soba_mut1 mutation log |
references/devtools.md | Driving a running app over Chromium DevTools: the scripts, what to grant, recipes |
scripts/gen-hash.py | Computes the ?sha224= pin of a module file |
scripts/mutlog.py | Dumps an instance's mutation log, decoded |
scripts/devtools/*.mjs | Inspect and drive an app's page in the running Sobamail client |
Refreshing import pins¶
scripts/gen-hash.py <file> prints the SHA-224 digest of a module in the encodings an import accepts. Put the value into every import of that module, then recompute the importer, since its pin is part of its content. The script is the same one the deployment chapter refers to, shipped with the skill so Claude can run it for you.
Reading the mutation log¶
scripts/mutlog.py opens an instance database read-only and prints the mutation log in a readable form, so it is safe to run on a live profile.
# list the instances in a profile
python3 scripts/mutlog.py <profile-dir>
# the log of one app's instance, newest rows first
python3 scripts/mutlog.py <profile-dir> --app <appid> --tail
# only inserts into one table, with parent hashes
python3 scripts/mutlog.py <profile-dir> --app <appid> --table notes --op i --parents
# per table, operation and status counts
python3 scripts/mutlog.py <profile-dir> --app <appid> --summary
The profile directory is the folder two levels above your develroot: the develroot lives at apps/devel/<appid> inside it. --instance <instid> selects an instance by id instead of by app id, and pointing the script at a main.db file skips the lookup altogether. --limit N caps the output at N rows, with 0 meaning all, and --since N shows only rows with an id above N.
Ask Claude to read the log when a replicated event does not take effect, when two replicas disagree, or when you want to see what a UI action actually wrote.
Driving the app in the running client¶
Sobamail serves Chromium DevTools on 127.0.0.1:50134, the same endpoint Inspecting the Frontend connects a browser to. The skill's scripts/devtools directory talks to it from the command line, so Claude can look at your app's page and act on it while you watch. Turn on Enable CEF Remote Debugging under Tools ▸ Options ▸ General ▸ Connection first, and let Sobamail restart; the scripts need Node 22 or later and no packages. Every script takes --port when the endpoint is elsewhere, and reaches that one host only.
Every script but the first takes the instance id of the app's page, the 26 characters at the end of its soba://app/<appid>/<account>/<instid> URL. Two windows of one app for two accounts share a title and differ in instance id, so start by listing the pages:
| Script | Does |
|---|---|
pages.mjs [--port 50134] | Lists Sobamail's pages, app pages first, with their instance ids |
query.mjs <instid> <css> [--limit 20] | Prints the elements a CSS selector matches: tag, text, rect, classes, role, disabled state |
shot.mjs <instid> [out.png] [--selector <css>] [--scale 2] | Screenshots the page, or one element of it, as PNG |
console.mjs <instid> [--seconds 5] | Prints the page's console messages, uncaught exceptions and browser log entries for a while |
network.mjs <instid> [--seconds 5] [--api] [--full] | Lists the page's requests for a while; /api/v2/object traffic is decoded from msgpack to the SMC object it carries |
eval.mjs <instid> '<expression>' | Evaluates a JavaScript expression in the page and prints its value as JSON |
input.mjs <instid> '<steps as JSON array>' | Drives the page with real mouse and keyboard input, at the rects query.mjs reports |
reload.mjs <instid> | Reloads the page past the cache, after pnpm run dev-watch wrote a new UI build |
The first five read; the last three act. The skill suggests granting the reading scripts in the project's Claude Code settings and letting the acting ones ask each time, so Claude inspects freely and clicks with your consent.
Some things to ask for with these:
- "Show me what the UI sends when I click Save." Claude runs
network.mjswith--apiwhile you click, and reads the decoded object off the wire. - "Why is the list empty after the reload?" Claude reads the console and the requests, then evaluates an expression against the page's state.
- "Try the drag-and-drop yourself." Claude measures the elements with
query.mjs, drives the gesture withinput.mjs, and takes a screenshot to show you the result.
Turn remote debugging off when you are done
While the option is on, any program on your computer can inspect and control every page Sobamail shows, your mail in the reader pane included.