Changelog
Release notes, new features, breaking changes, and bug fixes for each EmuView version.
#Changelog
This page tracks every EmuView release with details on what was added, changed, fixed, and removed.
#Versioning
EmuView follows semantic versioning. API responses include an X-API-Version: v1 header. Breaking changes to the REST API increment the version prefix (e.g., /api/v2/).
#How to read entries
Each release is grouped into four categories:
- Added — New features and capabilities
- Changed — Modifications to existing behaviour
- Fixed — Bug fixes
- Removed — Deprecated features that have been removed
#Staying up to date
If you're self-hosting, check the upgrade guide for instructions on pulling new releases and applying migrations.
#Releases
#2026-09-14 — One webhook secret, one signature header name
Changed
- The
webhook_dispatchflow operation signs withX-EmuView-Signature. It wroteX-Webhook-Signatureas a literal of its own while collection webhooks signed under the shared constant, so one secret went out under two names on the wire — and a receiver written against one of them would find no header on the other's traffic and treat an unverified body as unsigned. The timestamp that accompanies it is nowX-EmuView-Timestamp; the old names are not sent. This is breaking for a receiver that verifies a flow's dispatch, and it ships as a clean rename for the same reason the 2026-09-07 one did: no receiver outside the operator's own instance exists yet.
#2026-09-07 — Flow delegation reads permission rows the way the database does
Changed
A
<prefix>/*permission row no longer lets a role delegate that prefix to a flow. Choosing which roles a caller may hand a flow through Run as was the one place in the product that read a storedcollections/*row as a prefix, treating it as cover forcollections/trails. Nothing else has ever read it that way: the permission engine grants nothing forcollections/trailsfrom acollections/*row — a permission row matches the exact resource or the global*, and only API-key scope strings glob. The delegation check now uses that same rule, so a caller can only delegate authority they could exercise themselves. Handing a flow a role more powerful than the caller was a privilege escalation, and this closes it.Who is affected. No role EmuView seeds — the shipped roles hold no concrete resource under
collections/orschema/, so no seeded pair changes. The rows at risk are the ones you created: the role editor writes one row per collection, so a role holding a hand-writtencollections/*orschema/*row and used to delegate roles that hold per-collection rows will now be refused.Existing flows keep running. The rule applies when a
run_asis set, not when a flow executes, so nothing stops mid-run and no scheduled flow breaks on upgrade. You will see the change as a role missing from the Run as picker in the flow editor, or as a refusal when saving a flow whoserun_asyou are changing.How to check. Only roles holding a prefix row changed reach:
npx wrangler d1 execute <instance>_sveltesync --remote --command \ "SELECT r.name AS role, p.resource, p.action FROM permissions p JOIN roles r ON r.id = p.role_id WHERE p.resource LIKE '%/*';"No rows means you are unaffected. The seeded
webhookrole'scollections/*rows do appear; they were already inert for per-collection checks and delegate nothing either before or after this change.What to do. Grant the resources explicitly: replace the prefix row with one row per collection the role should actually reach — which is what the role editor produces anyway — and delegation works again, now matching what the role can really do.
#2026-09-05 — The wire and storage identifiers say EmuView
Changed
- The internal proxy-host signature derives from an
emuview:prefix. The app worker signsX-Forwarded-Hostwith the new derivation only; the gateway verifies the new derivation first and, until 2026-10-05, falls back to the old one. Deploy the gateway before the app —npm run updatealready does — and confirm withGET /api/system/tenancy:forwarded_host_seenreadstrueonce the app worker has redeployed. The other order leaves the gateway host-blind until the app follows. - Collection webhooks sign with
X-EmuView-Signature. The header carryingsha256=<hex>of the JSON body wasX-SvelteSync-Signature; the old header is not sent. Receivers that verify the signature must read the new name. This is breaking for any receiver, and it ships as a clean rename because no receiver outside the operator's own instance exists yet. - Browser preferences keep their keys' new names without resetting. The
theme, help-panel and panel-width preferences now live under
emuview-theme,emuview-help-prefsandemuview:panel-width:*. A small script in the page head moves each existing value across on first load and removes the old key, so nothing changes for the person using the browser; where storage is unavailable it does nothing and the page renders as before. - MCP resource URIs use the
emuview://scheme.GET /api/v1/mcp/resourcesnow listsemuview://project/context,emuview://collections,emuview://system/settingsandemuview://docs/agent-rules. A client that cached asveltesync://URI gets a 404 and should re-list; nothing is persisted server-side, so there is no compatibility window. - Exported notebooks, flows and lens templates carry
emuview-*-v1format ids.sveltesync-notebook-v1,sveltesync-flow-v1andsveltesync-lens-v1becomeemuview-notebook-v1,emuview-flow-v1andemuview-lens-v1. Files you exported earlier keep working: every importer — the notebook parser,POST /automate/flows/import, the notebook flow cell andPOST /lens/import-json— accepts both spellings, and only the exporters were changed. Nothing needs re-exporting, and an old file and a new one import to the same thing. - The bundle migration source is
emuview-bundle.source_typeon a saved migration is stored server-side, so migration0127rewrites existing rows; there is nothing for an operator to do beyond applying migrations as usual.npx tsx scripts/rename-bundle-source-type.ts --checkreports any row left holding a source type the connector registry does not recognise — useful on a database restored from a backup taken before the upgrade.
#2026-08-10 — Batch queries: filter contract restored
Fixed
POST /collections/batchrejected every sub-query carrying afilter. The handler acceptedfilteronly as a JSON string and parsed it, while the SDK's types describe an object andsdk.batch()posted it unchanged. The object stringified to"[object Object]", so each filtered sub-query returned{ "error": "invalid_query", "status": 422, "message": "The `filter` parameter could not be parsed: \"[object Object]\" is not valid JSON" }while unfiltered sub-queries in the same request succeeded. The endpoint now accepts either form, for
filter,geoNearandgeoWithinalike, andsdk.batch()serialises exactly ascollection.list()does. Deployed clients are unblocked without an SDK upgrade; upgrading additionally alignsbatch()'s sort shorthand and ascending default withlist().geo_near/geo_withinwere silently ignored onGET /records. The same mismatch pointing the other way: the list route forwards those query params as strings and the query builder only ever tested for objects, so the constraint was dropped and the caller received the whole collection with a200. Both are parsed now, and a value that cannot be parsed is refused with422rather than dropped — as is a malformedgeoWithin.bounds, which the builder used to swallow.A
-prefix onsortBynow works over HTTP and in/batch.-created_atpreviously missed the allowed-column check and fell back tocreated_at, quietly returning a different ordering than asked for. (The SDK'slist()stripped the prefix client-side, so only raw HTTP and/batchcallers were affected.)?order=is also case-insensitive now./collections/batchdid not gate soft-deleted records.includeDeletedwas forwarded with no permission check, making batch the weakest door to tombstones thatGET /recordsand/records/exportboth refuse. It now requiresview_deleted, exactly as they do.
Changed
Breaking (already shipped, now reverted):
/collections/batchstopped accepting object filters. The regression reached production in the previous gateway deploy and broke deployed clients silently from the client's point of view — the batch still answered200and only the affected entries carried an error, so a caller readingresults[i].datasaw missing data rather than a failed request. It should have been called out as a breaking API change; this entry is that record. Object filters work again, and a regression test now covers both wire forms.A failed sub-query now returns
data: []. Failed entries used to carry only{error, message, status}— nodatakey — while successful ones returned{data, total, page, limit, hasMore}. Callers naturally writeresults[i].data, so one bad sub-query putundefinedwhere an array belonged and surfaced far away as "Cannot read properties of undefined". Entries are now uniform: a failure is an empty list plus its error, and a partial failure degrades instead of exploding.error,messageandstatusare unchanged, so code that checks them keeps working./collections/batchaccepts the full list-option surface.fields,locale,cursor,deletedOnly,countandskipCountwere typed as accepted by the SDK but discarded by the handler; they are honoured now.
Notes
- The per-entry error contract is documented in
Batch and aggregate and the API reference, and
the SDK exports
BatchQueryResultplus anisBatchFailure()helper so callers have a type to check against.
#2026-07-23 — Content translations
Added
- Translatable fields — mark any text-like field translatable to store one
value per language. The records API returns the requested language via
?locale=(with fallback), or the whole{ locale: value }map with?locale=*. See the Content translations guide. - Languages setting (Settings → Languages) — one canonical, BCP-47 language list with a default, per-language fallback chains, direction, and enable toggles.
- Per-language record editor — a language bar with completeness dots, Single and Compare views, AI "translate empty fields", and per-language status chips (AI draft / needs review / human / outdated, with automatic outdating when the source changes).
- Localized labels — localize enum option labels and field labels while the stored value stays stable.
- Per-language sort —
?sortBy=on a translatable field orders by the active locale, with an expression index provisioned automatically when the field is indexed. - New endpoints —
GET /api/i18n/languagesandPOST /api/i18n/translate. - SDK —
localeonlist/get/create/update,meta: 'i18n'onget, and thetranslatable/enumOptionLabels/labelI18nschema properties.
Notes
- No migration or downtime: translatable columns and the status column are added on demand. Collections with a single language are unaffected.
- Not yet supported: filtering by a translatable field's language, and independent per-language publishing.