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 fakectx, 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/testingsubpath returns a fully typedOxyFunctionContextbacked by in-memory stores. Import it from a test file and the fakectxgoes away.- It refuses where the host refuses, in the host’s own words - The manifest’s seven capability gates, the
destinationsallowlist and thedestination_write_policy(customer warehouses read-only without a reason, the org’s OLTP refused),ctx.txoutside Postgres,warehouse.upsertoutsideON 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)removesBuffer,TextEncoder,processand the rest for the duration of one call, so aReferenceErrorthat used to arrive on the first production request arrives in CI instead. - Every host call is recorded and overridable -
t.ops()andt.callsTo(op)return what the function actually asked the host to do, andt.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 runpicks 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": truein 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-ciwrites 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 functionhandlerget 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 serveandoxy startall 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.
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-branchwith--promoteis 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.semanticreads the compiled model - Inside an Oxy Function,ctx.semanticused 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.ymlis 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 greensuccess. It now shows asfailed · 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
SimpleAggregateFunctioncolumns decode as numbers - Values in aSimpleAggregateFunction(sum, UInt64)column could come back as quoted strings on some ClickHouse versions. They now decode as the underlying type, just like a plainUInt64column. - Clearer database errors - A server error caused by a database connection problem now carries a
database_unreachablecode, and a failed query carriesdatabase_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
oxyclinked tohttp://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.