Build apps
Local preview
Run your app on your machine as the platform does, and steer it from the terminal.
On this page
Three commands run your app on your machine the way the platform runs it: developer dev serves
a preview page that follows every change, developer set steers that page from another terminal,
and developer render writes one image to a file. None of them needs an account, a display or a
network connection, except for the hosts your app itself requests.
The preview page
ledable developer devdeveloper dev builds the project, serves the preview page and prints its address on stderr:
{"preview":"http://127.0.0.1:4400/"}The page is served on 127.0.0.1 only, on port 4400 unless you pass --port; --port 0 takes
any free port. Open the address exactly as printed: the page refuses requests for any other name,
localhost included, so another site cannot read it. The command runs until you stop it with
Ctrl-C.
Watching
Every change to a file in the project starts a new build, except in .ledable/, node_modules/
and dist/. A build type-checks the whole project first, as an upload does, then bundles the
app; changes made while a build runs start one more when it is done. After each build the command
prints a line on stderr, {"built": …} or {"build_error": …}, and every open page draws again.
What goes wrong shows on the page:
- A build fails, such as on a type error: the error shows above the panel, and the panel keeps the last image that built.
renderthrows: the error and its stack show under the panel, with lines in your own source files.settingsthrows: the error shows under the panel instead of a form.
What the page shows
- The panel: the render, as the LEDABLE app draws a display, and how long ago it was drawn. Under it, what the render tells the display: the animation behaviour, whether it is realtime, when the display would ask again (the soft TTL) and when it would give the image up (the hard TTL), the cache id, and how long the render took.
- Store preview: what
previewdraws. - Settings: a form for the app's settings, built from what
settingsreturns and following the same rules as the form in the LEDABLE app: which settings show, what a select offers, and what keeps the app from being added, which the page lists. Reset clears every setting. For an account or credential setting, type a handle thatledable.dev.jsonholds. - Device: the language, time zone and display time the simulated display asks with, which the
render reads as
ctx.language,ctx.timezoneandctx.durationMs.
The page draws again after every build and every change, not as time passes: reload it to render
again. The settings and device values are kept in .ledable/, so they are still there when you
start developer dev again, and every open page shows the same ones.
Steer it from the terminal
ledable developer set --set status=Away --set back_at=13:30:00
ledable developer set --timezone Europe/Berlin
ledable developer set --unset back_at
ledable developer set --resetdeveloper set changes what the running developer dev of the same project simulates, every
open page follows, and it prints the render that follows. --set key=value sets a setting and
--unset key clears one; both repeat. --reset clears every setting first. --language,
--timezone and --duration change the simulated display. A setting the app does not have is
refused. The output of the first command above says what the simulation holds, which settings
still keep the app from being added, and how the render went:
{
"values": {
"status": "Away",
"back_at": "13:30:00"
},
"panel": {
"language": "en",
"timezone": "UTC",
"durationMs": 5000
},
"blocking": [],
"render": {
"ok": true,
"behavior": "loop",
"realtime": false,
"softTtlMs": 300000,
"hardTtlMs": 600000,
"cacheId": null
}
}When the render fails, render has "ok": false, the error's detail and, for an error your
code threw, its stack.
Render to a file
ledable developer render --out frame.webp
ledable developer render --preview --out preview.webp
ledable developer render --values away.json --timezone Europe/Berlin --out away.webpdeveloper render builds the project once and writes the WebP a display would get, or with
--preview the store preview. It needs no running developer dev.
| Option | What it does |
|---|---|
--out <file> | The WebP to write; required |
--values <file> | A JSON file of settings; settings it leaves out take their defaults |
--preview | Write the store preview instead of a render |
--language <tag> | The display's language, such as en-US; en unless given |
--timezone <zone> | The display's time zone, an Area/City name; UTC unless given |
--duration <seconds> | How long the display shows the app, in whole seconds; 5 unless given |
The values file is one JSON object, and every value in it is a string, written as below:
{
"status": "Away",
"message": "At lunch",
"back_at": "13:30:00",
"color": "[122,146,194]"
}The values are settled as the LEDABLE app settles them before it adds an app, and the command refuses what the app would refuse: a setting the app does not have, a value its setting does not take, or a required setting left out. It prints where the file went and the headers that tell the display how to play it, as in the Quickstart.
How values are written
--set and --values take each value as text, the way a display's address carries it:
| Setting | Written as | Example |
|---|---|---|
string, select | The text | Away |
integer, float | A number | 12, 2.5 |
boolean | true or false | true |
multiselect | A JSON array | ["Sat","Sun"] |
date | Year, month and day | 2026-12-24 |
time | Hours, minutes and seconds | 13:30:00 |
datetime | Seconds since 1 January 1970, UTC | 1798761600 |
timezone | An IANA name | Europe/Berlin |
country | A two-letter code | DE |
location | A JSON object | {"lat":52.52,"lng":13.4,"desc":"Berlin"} |
colorRgb, colorRgba | A JSON array | [246,205,109], [0,0,0,128] |
account, credential | A handle ledable.dev.json holds | my-github |
ledable.dev.json
On the platform, some of what ctx offers comes from LEDABLE's services: your app's secrets, the
accounts and tokens people connected, time zone lookups and the news summary. The preview answers
them from ledable.dev.json beside ledable.json, which holds your own keys and tokens and stays
out of version control. All of it is optional; the preview reads it again on every build.
{
"secrets": { "TIDES_API_KEY": "your-own-key" },
"accounts": { "my-github": "a GitHub access token of yours" },
"credentials": { "my-token": "a token you created for testing" },
"timezones": [
{ "lat": 52.52, "lng": 13.405, "timezone": "Europe/Berlin" }
],
"news": {
"items": ["A headline", "Another headline"],
"counts": {}
}
}secrets: a value for each secretledable.jsondeclares, which the app reads fromctx.secrets. As on the platform, an app that declares a secret without a value fails every render. See Secrets.accounts: an access token for each account handle. Put the handle in an account setting, andctx.auth.getToken(handle)returns the token.credentials: a value for each credential handle, whichctx.credentials.get(handle)returns. A handle the file does not have reads asnull, as a credential the person deleted does, so you can see what your app does then. See Accounts and credentials.timezones: places and their time zones.ctx.locations.timezoneAt(lat, lng)answers with a place listed within half a degree of the point.news: the summaryctx.news.getSummary()returns:items, a list of headlines, andcounts.
Whatever the app asks for that the file does not have fails with an error that names what is
missing, except a credential, which reads as null. The file takes no other keys.
Network rules
The preview holds the app's requests to the same rules as the platform: fetch reaches only
HTTPS addresses on the default port, at hosts that
network.allowed_hosts declares. Any other
request fails in your code with the reason, so a host you forgot to declare shows while you
develop, not after you upload:
api.example.com is not among the app's network.allowed_hosts in ledable.jsonThe preview decodes images through ctx.images within the same size limits as the platform, too.