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_dispatch flow operation signs with X-EmuView-Signature. It wrote X-Webhook-Signature as 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 now X-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 stored collections/* row as a prefix, treating it as cover for collections/trails. Nothing else has ever read it that way: the permission engine grants nothing for collections/trails from a collections/* 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/ or schema/, 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-written collections/* or schema/* 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_as is 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 whose run_as you 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 webhook role's collections/* 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 signs X-Forwarded-Host with 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 update already does — and confirm with GET /api/system/tenancy: forwarded_host_seen reads true once 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 carrying sha256=<hex> of the JSON body was X-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-prefs and emuview: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/resources now lists emuview://project/context, emuview://collections, emuview://system/settings and emuview://docs/agent-rules. A client that cached a sveltesync:// 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-*-v1 format ids. sveltesync-notebook-v1, sveltesync-flow-v1 and sveltesync-lens-v1 become emuview-notebook-v1, emuview-flow-v1 and emuview-lens-v1. Files you exported earlier keep working: every importer — the notebook parser, POST /automate/flows/import, the notebook flow cell and POST /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_type on a saved migration is stored server-side, so migration 0127 rewrites existing rows; there is nothing for an operator to do beyond applying migrations as usual. npx tsx scripts/rename-bundle-source-type.ts --check reports 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/batch rejected every sub-query carrying a filter. The handler accepted filter only as a JSON string and parsed it, while the SDK's types describe an object and sdk.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, geoNear and geoWithin alike, and sdk.batch() serialises exactly as collection.list() does. Deployed clients are unblocked without an SDK upgrade; upgrading additionally aligns batch()'s sort shorthand and ascending default with list().

  • geo_near / geo_within were silently ignored on GET /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 a 200. Both are parsed now, and a value that cannot be parsed is refused with 422 rather than dropped — as is a malformed geoWithin.bounds, which the builder used to swallow.

  • A - prefix on sortBy now works over HTTP and in /batch. -created_at previously missed the allowed-column check and fell back to created_at, quietly returning a different ordering than asked for. (The SDK's list() stripped the prefix client-side, so only raw HTTP and /batch callers were affected.) ?order= is also case-insensitive now.

  • /collections/batch did not gate soft-deleted records. includeDeleted was forwarded with no permission check, making batch the weakest door to tombstones that GET /records and /records/export both refuse. It now requires view_deleted, exactly as they do.

Changed

  • Breaking (already shipped, now reverted): /collections/batch stopped 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 answered 200 and only the affected entries carried an error, so a caller reading results[i].data saw 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} — no data key — while successful ones returned {data, total, page, limit, hasMore}. Callers naturally write results[i].data, so one bad sub-query put undefined where 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, message and status are unchanged, so code that checks them keeps working.

  • /collections/batch accepts the full list-option surface. fields, locale, cursor, deletedOnly, count and skipCount were 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 BatchQueryResult plus an isBatchFailure() 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 endpointsGET /api/i18n/languages and POST /api/i18n/translate.
  • SDKlocale on list/get/create/update, meta: 'i18n' on get, and the translatable / enumOptionLabels / labelI18n schema 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.