Signed over-the-air updates
Ed25519 pointers, SHA-256 bundles, bsdiff patches, cohort bands, min_shell_version gating and one-command rollback. The shell pins your public key; nothing delivered over the air can change it.
One codebase. People install it from the stores and get signed over-the-air updates in seconds. AI agents open the very same release in a simulator they can see and operate, so you test the whole app end to end with the assistant you already use. No emulator, no device lab.
Order TT-1001 · 4 200 FCFA
mobile money · receipt SIMULATED-…The simulator is headless, so every pull request can run your real app flows: permission prompts, payments, notifications, offline. No emulator, no device farm, no phone. A failing test stops the merge, and stops the over-the-air publish too. This repository does it on every push.
import { test, expect } from "@shipvo/test/vitest"; test("a declined payment places no order", async ({ phone }) => { await phone.device.set({ payment: { nextResult: "decline", pin: "1234" }, biometrics: { enrolled: true, nextResult: "success" }, location: { latitude: 4.0225, longitude: 9.6967 }, }); await phone.tap("Place order"); await phone.tap("Use fingerprint"); await phone.type("Mobile money PIN", "1234"); await phone.tap("Confirm payment"); await phone.waitFor("Payment declined"); expect(phone).not.toSee(/Order TT-\d+ placed/); });
ubuntu-latest. Nothing to rent.on: { pull_request, push: { branches: [main] } } jobs: simulator: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npx playwright install --with-deps chromium - run: | npx shipvo sim --port 4545 & # wait for http://localhost:4545/app/ - run: npx vitest run # publish.yml: the OTA publish only runs if this passes publish: { needs: test }
npm i -D @shipvo/test vitestThe simulator renders your app on the web and simulates the device, so it catches logic and flow bugs, not native crashes or slow-phone performance. Where it differs from real Android and iOS, we publish the gaps.
Four stages, one codebase. Click through; every panel is the real thing, not a mock.
shipvo init writes the OTA runtime, the device API, a simulator build, a publish workflow and your signing key. Your screens use one API for camera, location, biometrics, payments and notifications; it is real on phones and simulated for agents, and never pretends otherwise.
$ npx -y @shipvo/cli init myshop --name "My Shop" ✓ shipvo.json · app.json · App.tsx · metro.config.js ✓ .github/workflows/publish.yml ✓ .shipvo/seed.txt (Ed25519 signing key, git-ignored) // App.tsx import { device } from "@shipvo/device"; const code = await device.camera.scan(); const here = await device.location.current(); const ok = await device.auth.confirm("Pay 4 200 FCFA");
One line of MCP config. The agent gets each screen as an accessibility tree with refs, then taps, types and scrolls. Permission dialogs, the fingerprint prompt and payment sheets appear in the tree and it answers them like a person. end_session reports what your backend actually received.
// Claude Desktop · Cursor · Claude Code { "mcpServers": { "shipvo": { "command": "npx", "args": ["-y", "@shipvo/mcp"] } } } › open_app("myshop") - heading "My Shop" [ref=e6] - button "Scan a code" [ref=e11] › set_device(camera: { barcode: { data: "6181234567890" } }) › tap(e11) - dialog "Allow My Shop to take pictures and record video?" - button "While using the app" [ref=e14] › end_session() { steps: 18, orders: [{ total: 4200, payment: "mobile_money" }] }
shipvo publish builds, signs and ships the update; in a GitHub repo the workflow runs it for you on every push. It signs a release pointer (Ed25519), ships bsdiff patches from each installed version, and rolls out by cohort: 1 %, then 10 %, then everyone. Phones verify signature, hash and size, apply without a restart, and discard a release that fails to boot. shipvo rollback --to 7 when you need it.
$ npx -y @shipvo/cli login GitHub sign-in $ npx -y @shipvo/cli app create publish token, scoped to this app $ git push release 2 · bundle 1.7 MB · patch from r1 31.4 KB bands b0 (1%) b1 (10%) b2 (all) · signed · web artifact /w/myshop/r2/ › open_app("myshop@latest") the exact release phones run
The same scripted flows run in the simulator, on an Android emulator and on an iPhone simulator. We compare every actionable element and every text on every screen and publish the gaps as well as the score. This is how the simulator earned the right to replace your emulator for most of the loop.
Read the parity results →| Screen | Elements | Actionable | Text | | ------------------- | --------- | ---------- | ----- | | Camera permission | 3 / 4 | 100% | 100% | | Scanner | 11 / 10 | 100% | 100% | | Location permission | 4 / 5 | 100% | 100% | | Payment prompt | 7 / 6 | 100% | 100% | | Order placed | 13 / 10 | 100% | 77% | same order ✓ iOS · 21 screens · mean actionable 100% · text 95.3%
The same release, from the same signed bundle. Your customer sees pixels. Your assistant reads the screen like a person would, then taps by reference. This is a real Tide & Tackle screen, not a mock.

// what the agent reads, from MCP get_screen - heading "Delivery and payment" [level=1] [ref=e30] - heading "Delivery slot" [ref=e31] - button "Friday 14:00-18:00, 1 500 FCFA" [ref=e33] - button "Saturday 08:00-12:00, 2 000 FCFA" [ref=e34] - button "Use my location" [ref=e38] - textbox "Delivery address" [ref=e39] - radio "Mobile money" [ref=e41] - textbox "Mobile money number" [ref=e42] - button "Place order" [ref=e44] // then: tap(e44) · the backend receives the order
You publish once. Phones verify the signature, the hash and the size, apply the update without a restart, and roll back on their own if it fails to boot. Agents open that exact release in the simulator, so what you tested is what shipped.
a release going out an agent opening it · illustration
Each card is a shipped, documented piece. Nothing here is a roadmap item.
Ed25519 pointers, SHA-256 bundles, bsdiff patches, cohort bands, min_shell_version gating and one-command rollback. The shell pins your public key; nothing delivered over the air can change it.
Accessibility-tree screens with refs over MCP. Local with npm run sim, hosted with a key from shipvo login.
Camera, location, biometrics, payments, notifications. Real on phones, simulated for agents, labelled simulated every time.
Nightly runs on an Android emulator and an iPhone simulator; every gap published.
Flip listing.public and your app appears on shipvo.dev/apps and in every signed-in agent's list_apps.
The control plane is a Cloudflare Worker with KV and D1. Run it in your account with your keys, or register on the hosted one with shipvo app create; publish tokens are scoped to one app and every pointer is checked against your public key.
Taken on a 2 GB Redmi 9A, an Android emulator and an iPhone simulator in CI. Method and gaps in parity results.
Connect the assistant you already use in four steps, read an honest side-by-side with Expo, and check what it costs. We say where Expo is the better choice.
The remote MCP path: sign in once, paste one config, and ask Claude, ChatGPT or Cursor to buy something from your app. Prompts that find real bugs included.
Simulating and updating, side by side: what each runs, what each catches, and when to use which, or both.
Free in the beta, open source always. Worked monthly examples against Expo's plans, and the cost of testing every pull request.
Stories and guides, such as the three bugs our first typed test found in our own tooling.
Published with Shipvo and listed by their owners. Sign in once; any MCP client can open_app them. See all →
Build and publish an app from the guide alone, then tell us where it broke. Free hosting during the beta and a say in what gets built next.