Skip to main content
September 25, 2026 · v0.5.153, v0.5.154

New Features

Unit-Test an Oxy Function Against the Host’s Real Gates

Every custom app that unit-tests an Oxy Function has had to hand-roll its own fake ctx, and each copy ends up more permissive than the platform in ways its author never finds out about. A function that forgets "storage": { "write": true } in its manifest passes its own tests and then fails closed in production with StorageCapabilityMissing. The SDK now ships the context itself.
  • createTestContext(manifest, { function, databases }) - A new @oxy-hq/sdk/testing subpath returns a fully typed OxyFunctionContext backed by in-memory stores. Import it from a test file and the fake ctx goes away.
  • It refuses where the host refuses, in the host’s own words - The manifest’s seven capability gates, the destinations allowlist and the destination_write_policy (customer warehouses read-only without a reason, the org’s OLTP refused), ctx.tx outside Postgres, warehouse.upsert outside ON CONFLICT, ctx.fetch’s outbound safety rules and byte cap, and the transaction bracket’s rollback-on-throw all behave in a test exactly as they behave in production — including the error text.
  • The isolate’s missing globals are missing in the test too - t.run(fn) removes Buffer, TextEncoder, process and the rest for the duration of one call, so a ReferenceError that used to arrive on the first production request arrives in CI instead.
  • Every host call is recorded and overridable - t.ops() and t.callsTo(op) return what the function actually asked the host to do, and t.override(op, impl) substitutes an answer. Both are typed by a closed list of operations, so a typo in an operation name is a compile error.
  • Reads answer from the real shape zoo - Seed with .zoo(), .table(name, columns).insert(rows) or .raw(name, rows), and typed columns render the way the warehouse renders them — wide integers as exact digit strings included.
The test context deliberately does not model everything: Airhouse’s DDL refusals and schema-qualified writes, storage’s real S3 paging, the DNS layer of the outbound-safety check, the ctx.query row cap, and engine-specific statement errors are all named in the module header as out of scope. oxyc checks run against a real deployment remains the layer that catches those.

CI Runs an App’s Checks Without Storing a Key

oxyc publish already mints its own short-lived credential from the CI job’s identity and stores nothing — and then the very next step in a release workflow reached for a long-lived API key just to ask whether what it shipped works.
  • A publish credential can run the app’s own checks - Running an app’s declared checks with a token that may already publish arbitrary code to that app adds no capability, so the check-run route now accepts one. oxyc checks run picks its surface from the credential it holds, and with no credential at all in a job that can prove its identity, it mints one.
  • The grant stays narrow - A run is refused unless the function is marked "check": true in the manifest and the token names that app. Reading another app’s functions or runs answers not found, exactly as an out-of-scope App Operator does, so a token can’t use the difference to enumerate what exists on a deployment.
  • oxyc init-ci writes the step for you - The generated workflow includes a checks step only when the manifest actually declares a check, because a generated workflow that fails on its first run teaches people to delete steps they need.
A deployment older than this release doesn’t return the app id alongside the minted token, and the CLI says so rather than guessing.

Every Custom App and Function Is Measured, With Nothing to Configure

Per-app and per-function metrics sat behind a watchlist environment variable holding a comma-separated list of app UUIDs, maintained per environment. It was unset everywhere — so every app folded into a single __other__ bucket, the per-function series never existed at all, and every per-app panel was permanently empty.
  • Labelled by default - Every app is now labelled by its id and every function by its name, with no configuration anywhere. The watchlist variable is removed.
  • Bounded on purpose - A first-come budget per process caps the labels at 64 apps and 128 app/function pairs, sized against real traffic with room to spare. A value that arrives after the budget is spent folds to __other__ — which should never appear, and signals that the estate outgrew the cap if it does.
  • Series stay stable - An app that has been admitted is always re-admitted, so a series never flips between its id and __other__ mid-process, and two apps that both name a function handler get two separate series rather than one collision.
  • Isolate startup timing is readable - The histogram buckets for isolate initialization were ranged against a figure that turned out to be wrong by a factor of several. Re-ranged to 5 ms–1 s with four edges across the 25–100 ms band, the p50 and p95 now resolve to about 10 ms instead of a factor of two.
Function metrics currently reflect only queued runs handled by the worker fleet. Route-mode function traffic is served by the Factory instance, which isn’t scraped yet — so an absent series there is not a healthy zero. The metric’s own help text and the platform metrics documentation now say so.

Rolling Back a Release No Longer Wedges on Migrations

Reverting a deployment to an earlier image used to be blocked outright when the newer image had applied a migration: the older binary’s migrator aborted with “Migration file of version ’…’ is missing, this migration has been applied but its file is missing”, and the failing job held up the entire rollout until someone deleted the ledger row by hand.
  • A newer ledger row is tolerated, not deleted - The migrator now skips ledger versions it wasn’t built with when it reads the table, leaving the row itself untouched — so rolling forward again still sees the migration as applied and doesn’t re-run it. For migrations the binary does know, behavior is bit-identical. Each tolerated version is named in a warning.
  • Wired into every migrator - All eight migration chains inherit it, so oxy migrate, oxy serve and oxy start all behave the same way.
  • 0.5.153 is the carrier release - Tolerance runs in the binary you revert to, which means the release that first ships it isn’t protected by it. This release therefore deliberately adds no ledger row of its own, so reverting from it is a plain re-pin. From the next release onward, no release is forward-only on migration-ledger grounds.
This fixes the rollout mechanism, not schema compatibility. A dropped column, a new NOT NULL without a default, a tightened constraint, or a data backfill will still break a revert — those now fail at runtime rather than blocking the rollout. And reverting to any image older than 0.5.153 still needs the manual step, because tolerance has to be present in the image you are rolling back to.

Leave Kiosk Mode From the Tablet

Enrolling a browser as a store kiosk used to be a one-way door. A personal phone enrolled during a demo kept sending every Oxygen app to the crew sign-in board and Store Ops, and the only way out was an org admin revoking the device from Settings → Crew.
  • “Leave kiosk mode” on the device itself - A kiosk admin can now sign in from the tablet, land on the kiosk management page, and leave kiosk mode after an in-page confirmation. Sign-in goes back to normal and the device shows as Revoked.
  • Enrolling a signed-in browser warns first - If the browser is already signed in, enrollment names the account and asks you to confirm with Enroll anyway, so a personal device isn’t turned into a kiosk by accident.
  • “Sign in as an admin” goes to the right place - From the kiosk sign-in board, an admin now lands on the kiosk management page instead of an unrelated screen.
  • Signing out of a kiosk really signs the admin out - A kiosk that has been signed out no longer quietly keeps the admin’s web session alive, including on org subdomains.

Preview Semantic Model Changes in a Custom-App Draft

A change to a .view.yml couldn’t be tried in a custom app without first merging it to main. It can now be staged behind a draft build, and production keeps reading the live model.
  • oxyc publish --semantic-branch <branch> - Compiles a workspace branch into a staging revision and pins the draft build to it. Semantic queries, SQL queries and the semantic boundary from a previewed draft page read the branch’s model. The live app and the workspace’s current revision don’t move.
  • Draft only - Combining --semantic-branch with --promote is refused. Promoting stays a pointer move, and the promote response lists every view and topic that differs from what the live app will read.
  • Correct results under a pin - Result caches are kept separate per pinned revision, and pre-aggregated rollups (which are built from the promoted model) are bypassed, so a draft never shows numbers from the wrong model.
  • ctx.semantic reads the compiled model - Inside an Oxy Function, ctx.semantic used to read view files from the raw working copy, which on a diskless replica didn’t exist. It now reads the compiled revision, the same way the HTTP semantic query route does.

Previews: Changes Are Tested on Your Real Data Before They Ship

Oxy staff can now try a workspace branch, a pipeline or automation change, or a custom-app build against a customer’s real data before it goes live. Nothing in a preview can reach production. Every write is held, refused, or sent to an isolated copy.
  • Workspace previews - Staff compile a branch and open any page against it, using a preview bar, Settings → Workspace → Previews, or Open preview in the IDE. Anything that would run or change something is refused while previewing, and background work never reads a preview.
  • Pipeline checks and dry runs - A branch’s changed .airway.yml is checked against the live tables when it compiles, and each change is classed as additive, a warning, or one that needs Reset schema. An automation can dry-run against the branch. Reads and agent steps run normally, and each write is held and reported as “would have written”.
  • Isolated preview schemas - In a dry run, Airhouse writes go to schemas the preview owns, and those schemas are dropped after a set time. Changed transforms build automatically and their row counts are compared with live.
  • Staging custom-app functions - On a staging host, functions run with every side effect sent somewhere safe: a staging branch of the org’s OLTP database, a separate storage area, email delivered only to the person testing, a secrets overlay, and mapped non-production warehouse destinations. A staging failure never pages the on-call team.
Previews are a staff tool. Customers don’t need to set anything up; this is how changes to your workspace and apps get verified before they reach you.

Platform Improvements

Compile Reliability

  • A compile that dies before it starts now triggers the backoff - The self-healing backoff that stops a failing workspace from re-enqueueing a compile counted failed compile revisions — but a compile rejected before it begins (because the workspace’s working copy isn’t on the node it dispatched to) never writes a revision row. The guard read healthy because it could not report, and one workspace re-enqueued a doomed compile every 30 seconds for hours. A second, independent guard now reads the task queue’s own terminal states, so such a workspace backs off 5 minutes, then 10, 20, 40, up to a six-hour ceiling. A cancellation is fetched but never counted as a failure, and an operator’s Run compile now still bypasses the wait entirely.
  • Blank workspaces can be recompiled - A newly provisioned org’s default workspace has no git repository, and every attempt to compile it failed with “could not resolve HEAD commit on main”, so changes such as adding a database never reached the app. These workspaces now compile from a snapshot.

Custom App Reliability

  • A release can’t silently break a working function - Before a new release rolls out, every published function’s manifest is checked against the new release’s rules. If the release would start refusing something a function did successfully in the last week, the rollout stops and the previous version keeps serving.
  • Failures show as failures in the invocation history - A call that returned a server error, or caught a failed ctx.* call, used to appear as a green success. It now shows as failed · 500, with a tooltip.
  • Low-traffic breakages are reported - A function that worked, broke and stayed broken now raises an alert even when it is called too rarely to hit the usual failure threshold.
  • “Not from CI” badge - The apps admin list flags a live build that a person published by hand instead of trusted-publishing CI, so a build that will be missed by the next automated republish is visible.

Pre-aggregation Accuracy

  • Edited definitions no longer serve old rollup numbers - Changing a measure’s expression, aggregation type, filters or source table without renaming anything could leave the analytics agent answering from a rollup built before the edit, under the Pre-aggregated badge. Rollups are now matched against the current definitions, and a stale one falls back to the warehouse.
  • Promoting a rollup change builds it right away - Adding a measure to a rollup and promoting it built nothing until the next scheduled cycle, up to 10 minutes later and sometimes much longer. A promote that changes a rollup now starts a build immediately. A rollup whose first build returned no rows is no longer treated as fresh for hours.
  • Rollups build from the promoted model - Background rollup builds now use the promoted workspace revision. Before, workers on a multi-instance deployment had no working copy to read, which caused repeated unnecessary recompiles.

Data and Connections

  • ClickHouse SimpleAggregateFunction columns decode as numbers - Values in a SimpleAggregateFunction(sum, UInt64) column could come back as quoted strings on some ClickHouse versions. They now decode as the underlying type, just like a plain UInt64 column.
  • Clearer database errors - A server error caused by a database connection problem now carries a database_unreachable code, and a failed query carries database_query_failed, so a connectivity issue no longer looks like a bad query.

Interface

  • App Operators no longer see a permission error on every admin page - The admin sidebar asked every staff member’s browser for workspace health data, and App Operators, who can’t view it, saw a “You don’t have permission to do this” message on every page. The sidebar now asks only when the user can open that page.
  • Invitations created from the CLI link to the right host - Invitations and owner invites created through oxyc linked to http://localhost:3000. They now use the address of the deployment the request was sent to.
  • The acting-as banner no longer pushes the user menu off screen - While a staff or partner assume-role session was live, the sidebar wrapper still claimed the full viewport height even though the page had been shortened by the banner, so the bottom of the left rail — including the user menu — was clipped away. Every page with a sidebar is corrected, and only while an acting session is running.

Supply Chain

  • The published Oxy image is signed and pinned by digest - Each multi-architecture image is now built, signed with keyless signatures and verified before any public tag is created, and every tag is read back to confirm it points at the bytes that were signed. Deployments pin the image by digest rather than by tag, and a promotion verifies the signature before it pins.

Documentation

  • Docs brought in line with the code - The published documentation was reconciled against the current behavior of the platform across a broad sweep of pages.