docs(adr): server-driven Dynamic Widgets - #266
Merged
Merged
Conversation
Adds ADR 0002 planning a device-fetched JSON props source for Dynamic Widgets so any backend can drive a widget without running Voltra's renderer. Covers config, request/response contract, engine selection, scheduling, and failure handling per platform. Tracks #176. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ec7iFMH6yjH1W4zTEHyzGN
Server-driven Dynamic Widgets reuse the existing serverUpdate key and are selected by the presence of entry. Adds a layered widget server request store shared by both engines, an explicit request API (method, query, headers, body), and the GET-with-body rule verified against platform HTTP stacks. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ec7iFMH6yjH1W4zTEHyzGN
app.json serverUpdate becomes the default layer of a runtime settings store. The app can override url, interval, method, query, headers, body, and enabled per widget or globally. Drops family for Dynamic Widgets, deprecates the credential APIs, records the agreed decisions on interval floors, cache headers, trial render scope, and deferred events. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ec7iFMH6yjH1W4zTEHyzGN
…tate Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ec7iFMH6yjH1W4zTEHyzGN
Key every per-widget store by a WidgetScope type, resolve settings per scope, reserve instance names in the request and env, and record the ordering against the open per-instance configuration work. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ec7iFMH6yjH1W4zTEHyzGN
Props writes never pause server fetching; only enabled: false does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ec7iFMH6yjH1W4zTEHyzGN
V3RON
force-pushed
the
claude/dynamic-server-widgets-plan-p8rlok
branch
from
September 5, 2026 19:04
fae7528 to
6970018
Compare
V3RON
marked this pull request as ready for review
September 8, 2026 17:25
V3RON
added a commit
that referenced
this pull request
Sep 9, 2026
Implements ADR 0002. Stacked on #266, which adds the ADR; review and merge that first, and this retargets to `main`. Closes #176. ## What is this? A widget that wants fresh data without the app running has had one option: `serverUpdate` on a payload-driven widget, where the server runs Voltra's JSX renderer and returns a full UI payload. That rules out any backend not written in Node, the fetch is GET-only, and the URL is frozen at build time with no way to tell the server which account or range the widget wants. Giving a widget both `entry` and `serverUpdate` now makes it a server-driven Dynamic Widget: the device fetches a plain JSON object from the configured URL and passes it to the bundled JS as props. Rendering stays on the device and the server never sees a component tree, so a hand-written PHP script is enough. `serverUpdate` in app.json also becomes a set of defaults rather than the final word. `setWidgetServerUpdate` lets an app change the URL, interval, method, query, headers and body at runtime — per widget or for all of them, for both render engines — and turn fetching off entirely to drive a widget itself. ## How does it work? `entry` decides how a widget renders and `serverUpdate` decides where its data comes from, so the two keys together select one of four providers on iOS and receivers on Android. That choice is made once, when the native project is generated; no runtime code asks whether a widget is server-driven. Both engines read their settings through one resolver per platform, composed of four layers stacked in a fixed order — app.json config, the deprecated credentials, global runtime settings, per-widget runtime settings. `headers` and `query` merge per key; everything else takes the value from the most specific layer that sets it. That rule lives in the resolver and nowhere else, and the same code builds every request, which is what gives payload widgets runtime URLs and non-GET methods too. Everything per widget is keyed by a `WidgetScope` rather than a bare id, so per-instance fetches can be added later without changing callers. Every fetch is fetch, parse, trial-render, commit. Props are rendered once off screen before they are committed, so a response the widget cannot draw leaves the last good one on screen rather than blanking the widget. A body that is not a JSON object is rejected, and one shaped like a Voltra payload is rejected by name, since pointing a widget with an `entry` at a payload endpoint is the mistake sharing the config key invites. `ETag`, `Cache-Control: max-age` and `Retry-After` are honoured, clamped to what each platform can schedule. Widgets see the outcome on `env.serverUpdate` — `status`, `fetchedAt`, `error`, `httpStatus` — so they can show "updated 3 min ago", dim when the data is stale, or hide the freshness line while the app has taken them over. That is the only change to the existing Dynamic render path: an environment seam the plain engine leaves empty. On Android the work runs in the app process through WorkManager and never pushes `RemoteViews`; drawing stays in `VoltraClientGlanceWidget`. On iOS the new timeline provider wraps the existing one, keeping `placeholder` and `getSnapshot` off the network. ## Why is this useful? Any backend can drive a widget now, in any language, by returning the data it already has instead of learning Voltra's renderer. Per-tenant deployments can ship a widget with no URL at all and supply one after login. An app can point a widget at a different endpoint, add an auth header, or take the widget over entirely without a rebuild. Payload widgets get the same runtime settings, ETag revalidation and `locale` parameter without any config change, and keep their existing request and schedule when no runtime settings are set.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is this?
ADR 0002, the plan for server-driven Dynamic Widgets (#176). Today a widget that wants fresh data without the app running has to use
serverUpdateon a payload-driven widget, which means the server must run Voltra's JSX renderer. This ADR lets a Dynamic Widget fetch a plain JSON object from any HTTP endpoint and render it on device, so a PHP, C#, or Go backend can power a widget without Voltra on the server.Documentation only. No code changes and no changeset.
How does it work?
entrydecides how a widget renders,serverUpdatedecides where data comes from. A widget with both is server-driven and Dynamic. No new config key.serverUpdatein app.json becomes the default layer of a runtime settings store. The app can override URL, interval, method, query, headers, body, andenabledper widget or globally throughsetWidgetServerUpdate. The credential APIs become deprecated wrappers over the same store.WidgetScopetype and the request and env reserve instance names, so the later per-instance work (feat(android): configure Dynamic Widgets per placed instance #218) extends this design instead of rewriting its storage.Platform behaviour was checked against Apple's WidgetKit documentation, Android's WorkManager and Glance documentation, the Glance source, and the platform HTTP stacks (GET with a body is dropped by
URLSessionand silently turned into POST by Android'sHttpURLConnection).Why is this useful?
Any backend can drive a widget, the request can carry per-user input, and the app can change the endpoint or take a widget over at runtime. Everything is additive. The one behaviour change is that
entryplusserverUpdate, which is silently ignored today, starts fetching props.The status is "Accepted — not yet implemented", which per the ADR README makes the document the specification for whoever implements it.
🤖 Generated with Claude Code
https://claude.ai/code/session_01Ec7iFMH6yjH1W4zTEHyzGN
Generated by Claude Code