Working with data
Fetch over HTTP against a declared host allowlist, poll politely, persist small state, and prefer host events over polling.
What the sandbox means for data
A widget document has no network and no browser storage. fetch, XHR and
WebSockets are blocked by the sandbox's content security policy, remote
image URLs will not load, and the sandboxed frame has no localStorage,
cookies, or IndexedDB. This is not a missing feature, it is the security
model: everything a widget does to the outside world goes through the SDK,
where the manifest's grants apply.
So for data there are exactly two doors:
dd.http.fetchfor the network, against hosts declared in the manifest.dd.storagefor persistent state, a small per-instance key-value store.
Fetching over HTTP
Declare the exact hostnames the widget talks to, and nothing else works:
"permissions": {
"http": { "hosts": ["api.open-meteo.com", "geocoding-api.open-meteo.com"] }
}Users see this list word for word at install time ("Talks to the internet, only to: ..."), which is why exact hosts beat wildcards: a widget that names two API hosts is easy to trust. Then:
const res = await dd.http.fetch({
url: "https://api.open-meteo.com/v1/forecast",
query: { latitude: "44.43", longitude: "26.10", current: "temperature_2m" },
});
if (res.status === 200) render(res.json);JSON responses arrive pre-parsed on res.json; text rides res.bodyText;
images can be fetched with as: "dataUrl" and fed straight to an img or
dd-icon (the sandbox allows data: images). Redirects, methods, header
rules, size caps and pacing are all specified in
the http reference.
Polling with dd.poll
Any fetch-on-a-timer goes through dd.poll. Never hand-roll the loop: the
helper owns the interval, exponential backoff on errors, and staleness, and
it keeps polling paced even when your render throws. The Weather widget is
the complete pattern in one place, settings included:
let settings = {};
let poller = null;
await dd.settings.bind((next) => {
settings = next;
poller?.refresh(); // settings changed: re-poll right now
});
poller = dd.poll({
// read each cycle, so the interval follows the live setting
every: () => Math.min(180, Math.max(10, Number(settings.refreshMin) || 30)) * 60_000,
fetch: async () => {
const query = (settings.city || "").trim();
if (!query) throw new Error("Set a city in settings");
const geo = await dd.weather.geocode(query);
const current = await dd.weather.current({
lat: geo.lat, lon: geo.lon,
units: settings.units || "celsius",
});
return { current, place: geo.label };
},
onData: ({ current, place }) => {
const d = dd.weather.describe(current.code, current.isDay);
temp.value = `${Math.round(current.temperature)}°`;
condition.value = d.label;
},
onError: (err) => {
temp.value = "--°";
condition.value = `${err?.message || err} · retrying`;
},
});The pieces worth copying even when your data source differs: every as a
getter so the interval follows a live setting, refresh() from the
settings callback, a fetch that throws to signal failure, and an
onError that degrades the UI honestly instead of freezing the last good
value. Backoff after errors is automatic (from 1 second up to your
interval).
Pick intervals that match how fast the data actually changes. Weather defaults to 30 minutes with a floor of 10; a stock ticker might justify a minute. Every instance of your widget polls independently, and rate limits apply per instance.
Secrets in requests
APIs that need a key are the reason the secretRef setting type exists.
Declare one, and the user picks a stored credential in the settings dialog;
your code then references it by setting key, and the host injects the real
value after validating the request:
"settings": [
{ "key": "apiKey", "type": "secretRef", "label": "API key" }
]const res = await dd.http.fetch({
url: "https://api.example.com/v1/me",
headers: { authorization: { $secret: "apiKey" } },
});The widget never sees the secret's value: not in JS, not in logs, not in
error messages. Never ask users to paste keys into a plain text setting;
that stores the key as an ordinary readable setting and defeats the whole
mechanism.
The built-in weather stack
Weather is common enough that the SDK ships it: dd.weather.geocode,
current, describe, and icon wrap Open-Meteo with no API key needed.
It is convenience, not privilege: the calls route through your own grants,
so the manifest still declares the two Open-Meteo hosts (and storage if
you want the geocode cache). Shapes in
the reference.
Small persistent state
dd.storage is a per-instance key-value store for JSON-serializable
values: caches, counters, the state a widget should wake up with. The
Pomodoro widget persists its timer and, crucially, validates on
restore:
const stored = await dd.storage.get("state");
if (stored && typeof stored === "object" && typeof stored.phase === "string") {
state = {
phase: ["work", "short", "long"].includes(stored.phase) ? stored.phase : "work",
running: stored.running === true,
endsAt: Number(stored.endsAt) || 0,
};
}Stored shapes outlive widget versions: what you wrote in 0.1 will be read
by 0.3, so never trust a stored value's shape blindly. Field-by-field
validation with defaults, as above, makes updates painless. Use
dd.storage.delete(key) to evict stale cache entries. The store is small
by design (caps in
the reference); it is for
state, not datasets.
Prefer events over polling
Before you poll, check whether the host already pushes what you need.
System vitals, now-playing metadata, audio spectrum frames, time ticks,
desktop and folder changes, and widget-state changes all arrive as
events: fresher than any polite polling interval
and cheaper for everyone. The change events that carry no payload
(desktop.changed, folders.changed, widgets.changed) follow one
pattern: the ping tells you your listing is stale, the matching list call
is the read path.
Polling is for data the host does not have, which in practice means external APIs.
When the network is down
Widgets live on the desktop through sleep, wifi changes, and offline stretches, so failure is a normal state, not an exception:
- Let
dd.poll's backoff do the retrying; do not add your own retry loop on top. - Degrade visibly but calmly: keep the layout, blank the values, note the retry (the Weather widget shows the error and "retrying" in its condition line).
- Show staleness honestly. A
.dd-statusline with "updated 3h ago" beats silently displaying old numbers as current. - Cache the last good result in
dd.storageif the widget is useful with slightly old data; validate it on restore like any stored shape.
Popouts and flyouts
Open a second sandboxed pane from a widget, exchange messages with it, and use the flyout helper for panels that retract on their own.
Using the components
Build widget UI from the dd-* elements, keep structure in HTML, and use signals and dd.ui.create for the parts that actually change.