Changelog

Publish, fork, and share your apps

Jun 15, 2026·21 min read

This week Soot grew a front door. There’s a community marketplace to discover, fork, and publish apps; deployed projects now land on clean, shareable addresses and come with a real iOS simulator anyone can open in their browser; and the whole product is now simply called Soot. Underneath, the agent factory got a serious reliability pass and we closed an entire class of Postgres-versus-edge-database bugs at the source.

Platform

Explore: a community marketplace to discover, fork, and publish apps

There’s a whole new place to see what people are building. Explore is a searchable, tag-filterable grid of app cards — each showing the author, what runtimes it targets, real app icons, screenshot thumbnails, and how many times it’s been installed and liked. You can like apps, install them, and publish your own straight from a project’s settings. It opens seeded with a curated starter catalog — Photo Feed, Arcade Game, Flight Tracker, Todo List, and a Landing plus Docs site — so there’s something to explore from day one, and browsing stays live as you go: likes and new listings appear without a refresh.

Explore separates two things cleanly: live apps and forkable templates, each with its own tab. You can now fork any forkable project into a fresh copy you own — a “Fork to new project” action clones every file into a new project, resets agent state, threads, and build history so you start clean, and drops you into the fork immediately while the copy finishes in the background. Publishers opt their source into being forkable behind a confirmation, so nobody can pull source out of a private app, and re-publishing the same project updates its existing listing instead of spawning duplicates.

yesinstallbrowse Explorepick an app or templateforkable?Fork to new projectclone files, reset agent stateplus historyswitch into the fork whilecopy finishesinstall into your workspace

Paste a link to a shared app or a community listing into Slack, Twitter, or iMessage and it now shows a real preview card instead of a bare URL. Each shared app gets a generated 1200x630 preview image — the app name in a bold display face, the author, a twemoji icon, an optional screenshot of the running app, all over a gradient tinted uniquely from the app’s name. And that screenshot is real: when you publish, Soot captures the live preview of your running app and puts it on both the marketplace card and the link image. Preview and explore pages now render their title, description, and image on the server, so the card always resolves where before these pages produced none at all. Cards resolve whether the link points at an internal id or a deployed app’s name.

Deploy gives you a clean address and a shareable in-browser iOS sim

Free deploys used to live on ugly raw hostnames. Now every deployed project gets a clean address you pick yourself: the Deploy dialog checks the name’s availability as you type, previews the final URL, and stops you from deploying onto a name someone else already took. Login redirects and data sync target that same clean origin your visitors see, so everything lands on one address.

Even better, a deploy is now more than a website. You also get a shareable link to your app running as a real iOS simulator, booted against your deployed backend, that anyone can open in their browser with no install — the chrome-free simulator, your actual screens, your real data. The web build and the iOS sim each get their own prefixed address so the two never collide, publishing the sim happens automatically every time a web deploy finishes, and a hiccup building the sim never blocks your website from going live.

Icon Designer: an AI-driven app-icon studio

A new Icon Designer pane brings an Apple Icon Composer-style studio right into Soot (a Pro feature). You get a squircle canvas with a keyline grid, gradient and glyph controls, and a Gemini-powered design chat you can ask to shape the icon, with real iOS-style depth — a glassy radial highlight, a linear shade, a drop shadow on the glyph for legibility. One design drives every output at once: your project menu glyph, the app’s icon, a complete favicon set with a web manifest, and an opaque 1024px App Store image — so a single icon is production-ready with no extra steps.

Game Pet example: a creature that grows and blooms as it levels up

The Game Pet example got a real glow-up. The creature now visibly evolves with its level — it starts as a small bare sprout and plumps up, unfurling a flower crown petal by petal toward full bloom around level 5. Each care action grants XP so you hit a level roughly every five cares, fast enough to feel within a single session, with a golden burst on every level-up and an XP bar showing your percentage to the next one. Just as importantly, the creature actually renders on real hardware now: a GPU shader bug that left a blank canvas on real iOS and native Chrome was fixed at the shader level (the deep dive is in Tech), so the pet shows up everywhere it should.

The app follows your system theme, with no flash of the wrong one

Soot used to always boot dark. It now defaults to your system appearance, and a tiny script runs before any other code — reading your saved preference and your OS dark-mode setting and applying the right theme synchronously — so you never see a flash of the wrong theme on first paint. The web and native preview panes forward the resolved light or dark value too, so a guest app set to “system” mode tracks the host correctly, and the iOS simulator canvas finally renders against the right background instead of always painting light.

Soot, everywhere

The public product name moves from Contrast to Soot across every surface a user sees — the site, pricing, auth flows, Explore, example docs, the desktop and native previews, and all agent prompts. The marketing home got a substantial visual overhaul anchored by a WebGL sumi-ink watercolor background — a grain-free, curl-flow dispersed edge with watercolor “fingers,” warm embers and a purple fringe, a soft blue ribbon under the light seam, and a final pigment-grain pass — with new dither textures, per-section theming, a deeper pure-black dark mode, and a live theme-aware todo app rendered in a real phone frame replacing the old static screenshots. On narrow screens the navigation collapses into a hamburger that opens a bottom sheet, and the changelog now has its own per-entry pages and a blog-style index of cards.

Tech

One global slug registry makes clean deploy URLs safe across every project

Giving every free deploy a clean, prefixed host is harder than it looks because each project’s deployment state lives inside its own Cloudflare Durable Object — and a DO is scoped to its project, so it has no way to know whether another user already claimed a name. The fix is a global deploySlug control-plane table that acts as the authoritative reservation registry, with a deploySlug.reserve mutator as the single atomic gate that decides who owns a name. Routing stays database-free at request time: a standalone wildcard worker maps a slug to its worker name purely by string transform and proxies to the per-app worker, so there’s no per-request database lookup in the hot path.

Deployed apps moved from a bare <slug> host to prefixed app-<slug> and ios-<slug> subdomains, and the prefix is the isolation boundary by construction: a user slug can no longer shadow an apex infra subdomain, so the reserved-slug list collapses to a short vanity-blocking set. Both forms are covered by the existing DNS wildcard, so no new records are needed, and two new host-prefix constants thread the change through the app URL, hostname, auth URL, sync targets, routing worker, deploy dialog, and DO naming in one atomic pass.

slug to workerName, no DBowns the namebrowserwildcard worker on the apexper-app workerdeploySlug.reserve (atomicregistry)web deploy completesbuild native bundle to R2visitor opens ios-slugsubdomainSootSim shell boots againstdeployed backend

The Postgres-versus-edge-database divergence: a class of prod bugs killed at the source

Following last week’s move onto Cloudflare, several prod regressions this week shared one root cause: the production Durable Object’s orez/sqlite layer does not behave exactly like Postgres. JSONB columns come back as raw TEXT strings instead of parsed objects; computed boolean expressions came back as integers; and a handful of Postgres-only SQL constructs simply do not exist there. Code written and tested against Postgres read fields off what was actually a string in prod and silently got nothing — blanking saved screenshot decks, misbehaving build-config platform and branch gates, and 500-ing every preview upload on a date_trunc and a ::timestamp cast the DO can’t execute.

We fixed each instance and then closed the class. A typed coercion helper parses JSONB and timestamps at the DO boundary instead of assuming Postgres semantics. An orez/sqlite compiler upgrade now returns real booleans, so the old sqlBool coercion shim was deleted in full. And a new static sql-compat check — added to bun check — walks every source file, parses the AST to extract tagged SQL and query call sites, and flags hazard rules (date_trunc, RIGHT, gen_random_uuid, unnest, FOR UPDATE, ::timestamp) with a file, line, and migration hint before they can reach the edge. It ships as an agent skill too, so factory-generated code learns the same rules.

The blank-canvas-on-real-GPU bug: non-uniform control flow in WGSL

The Game Pet creature rendered fine in tests but came up blank on real iOS and native Chrome — a textbook leniency gap between headless and real GPUs. The entity fragment shader called fwidth(d), a derivative builtin, after a per-kind branch chain whose kind==3 arm contained a discard. That is non-uniform control flow, which the WebGPU spec forbids around derivatives. Dawn — the backend behind Metal on real iOS and real-GPU Chrome — rejects such a shader module outright, invalidating the entire render pipeline and silently dropping every GPU submit, hence the blank canvas; headless SwiftShader was lenient and hid the bug right up until native.

The fix hoists the antialiasing width to let aa = fwidth(p.x) + fwidth(p.y) + 0.001 at fragment-function entry, before any branching, so the derivative runs in uniform control flow. A second blank-on-capture issue was a premultiplied-versus-opaque alpha mismatch that made a fully-opaque scene read back empty in capture and native snapshots, fixed by switching the alpha mode to opaque. The old WebGPU feature-detection fork was removed so there’s a single render path, and both gotchas were written into the WebGPU agent skill so generated game code avoids them.

The agent factory survives transient errors and cold-boot races

A run of reliability work made the factory survive the transient LLM and network errors that previously wedged whole waves. Support agents that hit a transient error — Anthropic 500/502/503/429, failed fetches, ECONNREFUSED — used to flip to an unrecoverable error state and prompt the orchestrator to spawn a duplicate; a deterministic recovery scan now detects stranded agents (with a 30s cooldown to bound flapping) and resets them to idle. Transient-error classification was broadened to the full Anthropic and network set with exponential-backoff retries (1.5s, 3s, 6s) instead of retrying into an ongoing storm, and — most critically — the orchestrator’s first decomposition turn, the one that creates every task, was previously unretried, so a 500 storm there meant zero tasks ever got created and the wave wedged permanently; it now wraps the same retry path.

A separate cold-boot wedge is also gone. Factory stacks run with HMR disabled, and on a cold boot Vite’s dep optimizer could discover a not-yet-warm import, re-run, and bump its browserHash mid-session — so modules loaded before and after each got a different copy of @rocicorp/zero. Because Zero’s internals tag is a module-local Symbol, the two copies fail identity checks: every useQuery in orchestrator and agent code throws and optimistic mutations fall back to slow server round-trips. Gating optimizeDeps.noDiscovery on the no-HMR flag forces exactly one optimizer pass with the four Zero singletons pinned, and on a true cold boot optimistic mutations now reflect in 5-22ms instead of 200ms-plus.

Factory optimistic mutation latency on cold bootOptimistic mutation (before)200ms+ — server round-tripOptimistic mutation (after)5–22mssingle stable Zero copy after gating dep discovery; lower is better

The deploy change-gate diffs against the live commit, not a superseded push

A subtle interaction between CI’s deploy serialization and its change-detection stranded a schema change off-prod, causing the data sync to 500 on a missing table — the third bug of this class in a week. CI serializes main-push deploys and skips intermediate runs as superseded, marking them success without flipping prod. The surviving run then diffed from the superseded run’s tip — a commit that never went live — so any schema change that lived only in the skipped run was invisible to the gate, the data worker never deployed, and the table was missing in prod. The app worker now ships a /__contrast_deploy.json marker stamped with the actually-deployed commit; the release flow fetches it (cache-busted, 8s timeout) and diffs from that commit, so a stranded schema change stays inside the diff window until it genuinely ships, falling back to the previous behavior only when prod is unreachable.

The marketing home and stale-poke reconnects stop bothering Zero — and you

Two Zero-sync robustness fixes converge on one outcome: Zero never bothers you with infra noise it can recover from silently. Its IndexedDB error-recovery path was rotating storage the instant the bundle loaded, even on the unauthenticated marketing home where Zero isn’t meaningfully active; a new activity gate now blocks any storage rotation until the IDE surface is actually in use. And stale client-view poke cookies after a sync gap used to be terminal — rather than patch upstream Zero (an attempt was reverted), a new check recognizes the recoverable stale-cookie reasons and calls Zero’s documented resume path exactly once per distinct reason, skipping both the storage rotation and the user-visible error toast. Upstream Zero ships unpatched.

AI Factory

  • The wave grader was sometimes scoring a marketing landing page as the finished product; broadening the auth-marker patterns, clicking the right call-to-action while skipping hidden duplicates, and following the shared auth route as a last resort moved one validation wave from grading the landing (specCoverage 0.05) to the real authed app (0.45) — and a loud warning now fires when a capture never reached the app, so a login-stuck capture can’t post a real design score.
  • The lane file-ownership merge gate stopped false-blocking on non-conflicts: it now warns-and-allows unless a truly active cross-branch owner exists, lanes that replace or remove a file own both paths, and even a legitimate conflict is manager-overridable.
  • Factory agents stopped duplicating work — create_task rejects a normalized title that matches an open task, and named-agent creation is idempotent — and a reference-counted lease holds the classify-preview iframe visible long enough to register as rendered.
  • The three-tier orchestrator/pro/flash model split collapsed to a single project modelMode that assigns the heavy model to planners, designers, and QA and the flash model to workers, pair, and reviewers, with an explicit request always winning.
  • Prompt rules now make agents re-domain placeholder, empty-state, and result copy (not just nav labels) and derive any cross-screen metric from one shared selector, so a recipe app no longer ships social-feed copy and two screens no longer disagree on the same stat.
  • The factory floor view works in light mode with a new office-floor background, the agent inspector was restructured into a compact overlay, and overview shadows now track the theme instead of hard-coded black.
  • GLM-5.2 is now a selectable model, and provider pricing is derived from the model registry so a new model can’t fall through to the punitive unknown-cost fallback in wave scorecards.

SootSim

  • Virtualized scroll views now compute the correct scrollable range by also walking content children for the real maximum edge, so lists that previously clamped to zero scroll the full distance, and phantom blank space at the bottom of legend-list views is gone.
  • A Photos-style gallery now renders without pixel smearing when a raster layer rotates or scales — a tree-walk forces a full-surface redraw for that frame, exactly as iOS does behind moving raster layers.
  • Inspect mode works: a Yoga wasm pointer-identity bug that wedged layout on hover is fixed, and taps now pass through to the canvas in the empty state instead of being swallowed.
  • The iOS 26 floating tab bar got correct hit-testing near its edges, larger icons, a glass shadow, and proper search-tab alignment; the native context menu anchors from the trigger’s top edge like UIKit’s UIMenu, with an accurate shadow.
  • The text cursor advances to the start of the next line after a hard newline, matching UITextView insertion-point placement across left, center, and right alignment.
  • Unknown SF Symbols now render a visible placeholder instead of an invisible gap, and heart plus photo.on.rectangle.angled were added; the app-switcher blur now clears as a selected app opens.
  • Keyboard-controller-backed lists render correctly (passthrough view stubs aligned to react-native-keyboard-controller 1.21.x), and missing viewPositionInWindow, getConstants, and AccessibilityInfo methods no longer crash on boot.
  • The CLI redacts focused secure password fields as <redacted secure text>, and its text finder no longer matches single keyboard keys against long search targets.

Workspace and Site

  • The workspace now leads with a wider iOS/native preview pane on both Dev and Prod stages, with existing layouts migrating automatically, and a new focus-lock button in the top bar pins keyboard focus to the active pane.
  • The marketing home gained a live preview wrapped in an error boundary (a retry card replaces a blank section on error), an updated hero tagline, and a redesigned navigation and Community panel.
  • AI chat folds bursts of file reads and searches into one grouped tool timeline with per-tool counts instead of opaque “ran N actions” cards, and card titles and paths are now selectable text.
  • Mermaid diagrams and stat charts in docs and blog posts render correctly (an SVG-as-flex-item zero-height bug is fixed), and the blog reading view got a typography refresh.
  • The “Open” button reliably routes home and zooms into the IDE from any page, fixing a mount-order race, and PR preview recordings now capture the full before-to-after transition rather than opening on the settled result.

Tech (smaller wins)

  • The native JS bundling pipeline was unified into one shared module so the headless CLI and the deploy-time sim publisher produce byte-identical bundles.
  • Native-build access became deny-by-default with an admin email check, a runtime allowlist table extendable without a deploy, and an env bootstrap — and App Store submission now requires explicit per-deployment owner approval (TestFlight stays automatic).
  • The Apple-status poller re-arms once per process on the first request after a restart, so a build already mid-flight through Apple’s up-to-14-day review isn’t silently abandoned.
  • The transitional apex mirror was fully decommissioned now that traffic is consolidated on the apex, collapsing the nightly soak workflow from two jobs to one.
  • tsserver no longer loads the multi-MB seed template payload (split into a non-typechecked JS blob plus a thin type declaration), removing a real editor project-load penalty.
  • The SootSim menu protocol was extracted into a single pure-data package export so the host chrome can import it without dragging the canvas graph into the bundle.

Fixes & polish

Beyond the headline work, 94 fixes and 1 performance improvement landed this week. A few worth calling out: preview uploads stopped 500-ing in production once the monthly upload metering moved off SQL the edge database can’t run; landing and org pages stopped fetching all their data twice right after sign-in, where an auth effect was re-firing on every load during hydration; and the GitHub sign-in popup now clears its “opening…” state cleanly whether sign-in succeeds, fails, or you close the window, instead of hanging on a spinner.

Ready?

Create a web, iOS, and Android app in minutes with agents working alongside you.