Upgrading to 0.38.0

0.35 through 0.38 in one pass — the boot-breaking access declarations, the stricter input handling, and the runtime behaviour that moved underneath you.

0.35, 0.36, 0.37 and 0.38 landed within two days of each other, so this page covers them as one upgrade. It is much smaller than 0.34 — but two of the changes will stop your app from booting until you make a decision, and one changes how every request is validated.

Run the upgrade first, fix the boot, then read what moved at runtime.

1. Run voltro update

voltro update

Every breaking change below either ships a codemod that rewrites your source or prints written steps during the update. What a codemod cannot do is make an access decision on your behalf — that part is yours.

2. What breaks your boot

A declared event must decide who may listen (0.35)

defineEvent's guards: was optional, and an empty list was skipped — so under security.defaultDeny an event with no access declaration was subscribable by anyone who could open the socket, while the identical shape was already refused for every procedure. The boot gate now covers defineEvent too.

Declare an access decision on each event: a guards: list, or an explicit openAccess: '<reason>' when it genuinely is public.

Twelve first-party plugin routes now require a scope (0.35)

Every first-party plugin rpc route now declares an access decision, and defaultDeny is enforced in the dispatch spine as defense in depth. Previously-open routes that now need a scope:

Route Scope
billing.startCheckout / portalUrl / previewChange / changePlan / changeSeats / invoices billing:manage
billing.reportUsage billing:report
governance.export / erase admin:full (was already enforced in-handler, now declared)
storage.mintUploadUrl / ingestUrl storage:manage
storage.listRefs storage:browse

Migration: grant each scope to the role or subjects that legitimately hold that capability — via an rbac role, resolveScopes, or api-key scopes. The codemod lists every affected route plus the open-by-design surfaces that did not change.

The framework table set no longer reads a runtime flag (0.35)

CDC, VOLTRO_UNDO and VOLTRO_TRACING_PERSIST used to move the declared table set, which meant the migrate job's env and the pod's env could disagree — a green apply followed by a crash loop. app.config.ts gained schema: { traces?, undo? } to declare the two that still need a decision.

A failing *.startup.ts now refuses the boot (0.38)

It used to warn and let the server come up. If your startup module is allowed to fail, handle the failure inside it — the framework will no longer serve traffic behind a startup that did not complete.

3. What changed at runtime

An undeclared input field now rejects the call (0.37)

A field a procedure's input schema does not declare rejects the request. It used to be silently discarded and the call ran with what was left. This is the change most likely to surface in a client you did not update in lockstep: an extra property that used to be ignored is now an error. Check any hand-built request payloads.

IMPERSONATION_METADATA_KEY moved to @voltro/protocol (0.38)

It names the one reserved key in Subject.metadata, and Subject is protocol's type — two packages need it (plugin-auth writes the mark, plugin-audit reads it) and a plugin must not depend on another plugin. The codemod repoints the import, preserving an alias and the type-only form. Everything else stays where it was: impersonationOf, isImpersonated, ImpersonationMark and impersonationAuditRedactor are still exported from @voltro/plugin-auth.

An audit row now also records when an action was taken through an impersonated session, on the default settings, and no redactor can remove that mark.

Upstream 401s report a different code (0.36)

A 401 from an upstream now produces code: 'unauthorized', not code: 'session_expired'. The connection vault's own failure — the case where we do know the credential is unusable — becomes code: 'credential_unusable'. If you branch on these codes, update the branch.

RunStepStatus gained 'skipped' (0.35, plugin-ai-flows)

A sixth member means an exhaustive switch stops compiling and a status-keyed lookup has a hole. A manual codemod fires on any app that names the type or its literals. Everything else in that release is additive.

The analytics CDC mirror versions from the change, not the clock (0.35)

Under changeScope: 'fleet' (postgres CDC, mysql binlog) the mirror's version is derived from the change's own position in the totally-ordered fleet stream instead of each replica's clock. N replicas still issue N writes per change — that is the transport — but they are now byte-identical, so the sinks' existing guards dedupe them for free.

This closed a real defect: under clock skew larger than the gap between two changes to one row, a peer's duplicate of the older image could take the higher version and win in the warehouse permanently and silently.

If you wrote a custom sink, AnalyticsMirrorImpl.maxVersion is now required — a replica joining mid-stream seeds each key from the warehouse's high-water mark. All shipped warehouse sinks implement it; the codemod note covers a custom one. New tunable: VOLTRO_ANALYTICS_MIRROR_VERSION_STATE_LIMIT (default 100000).

The short checklist

  1. voltro update — let the codemods run.
  2. Declare access on every defineEvent.
  3. Grant the twelve plugin-route scopes to the roles that should hold them.
  4. Move CDC / undo / traces decisions into app.config.ts's schema: { … }.
  5. Make sure your *.startup.ts cannot fail unintentionally.
  6. Audit client payloads for fields your input schemas do not declare.
  7. If you have a custom analytics sink, implement maxVersion.