Commands, settings, SDK functions, anything in the docs.

Build apps

How apps work

Where an app runs, when it draws, and what it may and may not do.

On this page

Where an app runs

Your code never runs on the display. A display keeps a playlist of apps, and for each one it asks LEDABLE's servers for an image. The platform loads the version of your app that the playlist names, calls its render, turns the frames it returns into a WebP and sends that back: a still image for one frame, an animation for several. The display then plays the image for as long as that playlist entry is on screen.

Each version runs in an isolate of its own: a closed-off JavaScript environment that holds your bundle and nothing else. The bundle is your code together with the parts of the SDK it imports, the drawing code included, built when you upload. A published version never changes, so it draws the same pixels for as long as displays use it, whatever happens to the platform around it.

Three functions

The entry module's default export is the object defineApp makes from three functions:

TSX
import { defineApp } from "@ledable/sdk/app";

export default defineApp({
  settings: (ctx) => ({ /* every setting the app has */ }),
  render: (ctx, params) => ({ /* the image for one display */ }),
  preview: (ctx) => ({ /* the picture the store shows */ }),
});
  • settings returns every setting the app has: what people fill in when they add it. It is code, not configuration, so it can build a list of options from data. The platform keeps what it returns for a day (24 hours) unless the app sets settingsTtlMs. See Settings.
  • render draws the image for one display, from that display's settings in params and the request in ctx. It returns the frames and, optionally, how to play them and how long they last.
  • preview draws the store's picture of the app. It must give the same image every time: the platform renders it once per version and keeps it.

An app with a credential setting also has verifyCredential; see Accounts and credentials.

Each function gets its own context, with only what it may depend on:

settingsrenderpreview
Assets (ctx.assets)yesyesyes
Time (ctx.now)yesyesno
Cache (ctx.cache)shared by everyoneyesno
Secrets (ctx.secrets)yesyesno
The display and its settingsnoyesno

settings sees nothing of any one person or display, because everybody who adds the app gets the same form. preview sees nothing that can change, because its image is kept for good.

When render runs

render runs every time a display asks for the image, and a display asks:

  • when its playlist changes, such as when the app is added or its settings change;
  • again once the image's soft TTL has passed. The display keeps playing the old image until the new one arrives;
  • once more after a failed attempt, a little later. A display keeps the last image it got until its hard TTL has passed; after that it drops the image and shows that it is loading.

A display does this for every app in its playlist, not only the one on screen, so the next app is ready when its turn comes. Without TTLs of your own, an image is renewed after 5 minutes and dropped after 10 minutes without a renewal. Return softTtlMs and hardTtlMs to follow what you show: a clock asks again at the next minute, a day count at midnight. Caching and refresh covers TTLs, keeping the display's image when nothing changed, and sharing work between displays.

Each display asks for itself: a hundred displays with your app make a hundred renders. Keep nothing about one display or person in module-level variables. An isolate serves every display that runs the version and can be replaced at any time; share work through ctx.cache instead.

When render throws, or returns { ok: false, msg } to say in its own words that it cannot draw now, the display gets no new image and keeps the one it has.

What ctx gives a render

Everything that is not a pure computation comes through ctx, so a render is a function of its inputs and a test can supply every one of them.

MemberWhat it isMore
nowThe time of this request, on the platform's clockTesting
timezone, languageThe display's time zone (an IANA name such as Europe/Berlin) and language tag
resolutionThe panel's width and height in pixelsDrawing
durationMsHow long the display shows this playlist entry each timeAnimation
model, firmware, instanceIdThe display's model and firmware version, and an id for this playlist entry
cacheIdThe cache id of the image the display already hasCaching and refresh
cacheA cache for work you do not want to repeatCaching and refresh
assetsThe images, animations and fonts your version shipsImages and assets
imagesDecoding a PNG or JPEG you fetchedData and the network
secretsYour app's own keysSecrets
auth, credentialsThe accounts and tokens people connectedAccounts and credentials
locationstimezoneAt(lat, lng): the time zone at a place, or null
newsgetSummary(): a short list of current headlines LEDABLE keeps

The whole type is RenderContext; what render returns is RenderResult.

What an app may not do

An app runs in that isolate, and some things that work in Node.js or a browser do not exist there or are refused:

  • No host APIs. The bundle runs without Node.js modules, a file system or environment variables, and the type check that every build runs knows none of them. Files your app needs are assets, read through ctx.assets.
  • Time only from ctx.now. Do not read Date.now() or new Date(). ctx.now is the time the platform gives the request, and a test can set it.
  • Network only to declared hosts. fetch reaches only the hosts ledable.json lists in network.allowed_hosts, over HTTPS on the default port. Any other request fails in your code with the reason. People see the hosts before they add the app. See Data and the network.
  • No state between renders that you rely on. Each render may run in a different isolate.

Rules

What the sections above come down to:

  1. render reads the time only from ctx.now, and preview reads no time, network or settings. Tests pin the clock, and the store keeps a version's preview for good.
  2. Images, animations and fonts are files in the project, imported as assets, never pixel data written into code, so the store can check and serve them. Tables of numbers that a computation needs may stay in code.
  3. Check every response from another service at the boundary, with a schema, and keep its field names as the service sends them. See Data and the network.
  4. Anything that changes what the app draws needs a new version number, because a published version and its assets never change.
  5. Everything outside your own code comes through ctx.
  6. Declare every host the app requests in network.allowed_hosts; nothing else is reachable.