Skip to content
miblo

SDK

Publish an app to the Miblo store, step by step

How to publish an app on miblo.ai/apps: what a submission holds, the code package, the automatic scan, the AI review, the human approval, revisions and a checklist.

The process at a glance

Publishing puts your app in the store, miblo.ai/apps, for anyone to turn on in their Miblo with one click. The path:

  1. You test it on your computer with the SDK and the supervisor.

  2. You submit it at miblo.ai/en/apps/submit: the app's listing, its screens and, if it has one, its code package.

  3. The automatic scan checks the package at once and refuses what cannot pass, with the reason.

  4. The AI review writes a security report on the code (minutes later).

  5. A person approves it. No app enters the store without a human's approval.

  6. miblo.ai signs the package with the apps key, and whoever installs it checks the signature before anything runs.

The two kinds of app

KindWhat it carriesWhat runs on the installer's computer
Screen packageA card, up to 4 frames and the settingsNothing: Miblo applies the screen as a program would
App with codeEverything a screen package has, plus a code package (zip)Your main.js, inside the sandbox

A screen package suits a fixed screen, or one a program of yours keeps up to date. An app with code fetches its data and redraws on its own, like the official apps.

What a submission holds

FieldRules
Address (id)Lowercase letters, digits and -. It is the app's address in the store and never changes.
Name2 to 40 characters, cleaned like a card's strings.
CategoryMy computer, Money, Day to day, News and dev, Sport, Delights or Other.
Version1.0 or 1.0.0 (up to three numbers).
DescriptionIn Portuguese and in English, up to 2000 characters each.
InstructionsHow to use and set it up, up to 2000 characters.
What changedUp to 500 characters: it becomes the version's changelog on the app's page.
Program linkOptional, up to 300 characters.
IconA PNG of up to 64 KB, re-made at 128 px.
ScreenshotsUp to 6 PNGs of up to 512 KB, re-made at up to 960 px.
CardThe screen's card, checked with the gadget's rules (The App screen).
FramesUp to 4 PNGs (slots 0 to 3) of up to 1 MB, converted to Miblo's format (up to 60000 bytes of image).
SettingsThe settings schema (Apps); none may ask for a key, token or password.
CodeOptional: the code package's zip, up to 1 MB.

The server decodes and re-makes every image; nothing of the original file is ever served. Up to 10 submissions a day per member.

The code package

A zip of plain Node.js, ESM, no dependencies:

my-app.zip
├── manifest.json
├── main.js
├── lib/source.js       (optional: other .js and .json files)
└── README.md           (optional)

manifest.json

JSON
{
  "id": "tide",
  "name": { "pt": "Maré", "en": "Tide" },
  "category": "daily",
  "promise": { "pt": "A maré está subindo?", "en": "Is the tide coming in?" },
  "settings": [
    { "key": "port", "label": { "pt": "Porto", "en": "Port" }, "type": "text", "required": true, "max": 40 }
  ],
  "network": ["api.example-tides.com"],
  "intervalMs": 600000,
  "preview": { "v": 1, "title": "Tide", "items": [{ "t": "big", "value": "1.2 m", "label": "rising" }] }
}
FieldRules
idThe submission's address.
name{ pt, en }.
category, promise, previewAs in app.declare() (Apps).
settingsThe settings schema: what the person fills in the Miblo app.
networkThe only hosts the app may reach: public names (no IP, port or local names such as .local), up to 16. [] means offline. It must equal the hosts written in the code: a host used but not declared is refused, and so is one declared but never used.
intervalMsHow often run() is called: 10 s (10000) to 24 h.

main.js

JavaScript
// main.js: called every intervalMs, or right after the person changes a setting.
export async function run(ctx) {
  const t = ctx.lang === 'pt' ? { up: 'subindo', down: 'descendo' } : { up: 'rising', down: 'falling' };
  const port = encodeURIComponent(ctx.settings.port);
  const data = await ctx.fetchJson(`https://api.example-tides.com/v1/port/${port}`);
  const rising = data.next > data.now;
  ctx.state.last = data.now;                          // kept from one call to the next
  return {
    card: { title: 'Tide', items: [
      { t: 'big', value: `${data.now.toFixed(1)} m`, label: rising ? t.up : t.down, color: 'blue' },
      { t: 'text', value: `updated ${ctx.timeOf(ctx.now())}`, color: 'grey' },
    ] },
    peek: false,                                      // true asks for a peek (once every 10 min)
  };
}

run(ctx) gets:

ctx.What it is
settingsThe settings' values (defaults plus what the person saved).
stateAn object kept from one call to the next (in memory).
lang, localept or en, and the computer's locale.
now(), timeOf(ms), sleep(ms)The time, the time formatted (14:05) and a pause.
fetchJson(url), fetchText(url)Fetch only from the hosts in network (https, 10 s, 256 KB).
stateDirThe folder where the app may write files.
appThe app (app.settings, app.connect), for apps that need a connected account.
screen, frame(slot, image)The app's screen and an upload of its own frame (the flash wear is yours to budget).

And returns { card, peek?, frame?, plain? }: the card (without v, Miblo adds it); peek (true is 10 s, or a number from 1 to 10) asks to bring the screen forward; frame ({ slot, image: { rgba, w, h } }) writes a background before the card, and plain is the card to use if the frame is refused. Miblo declares the app, reads the settings, asks for the missing ones and draws the card for you.

File rules

  • Only .js, .json and .md; at most 64 files, 8 folders deep, names of up to 64 letters, digits, ., _ or -, no two names differing only in case.

  • No package.json and no node_modules: the Miblo runtime is the only "package".

  • The zip up to 1 MB, everything unpacked up to 1 MB, each script up to 512 KB.

  • main.js must export run(ctx).

The automatic scan

It runs on submission and is the truth the AI review cannot override. It refuses at once, with the reason:

  • importing a module the sandbox never allows: child_process, worker_threads, net, tls, dgram, http, https, http2, vm, module, wasi, cluster, inspector;

  • any dependency (a module that is not a Node built-in), node_modules or a package.json;

  • eval, new Function and their equivalents (a string to setTimeout, .constructor(...)), import() or require() of something computed, createRequire, WebAssembly, native code (process.binding, dlopen), computed access to the global object;

  • an address whose host is not in network (also spelled https:host, with backslashes or split into pieces), or a declared host the code never uses;

  • files outside the rules above, a script with a syntax error, an import of a file that does not exist, a main.js without run(ctx).

And it points out to the reviewer (without refusing on its own): hosts written without https://, http:// addresses, hosts built at run time, base64 or hex blobs over 1 KB, decoding (atob, Buffer.from(..., 'base64')), obfuscation signals (random names, _0x..., split strings, String.fromCharCode, minified lines), process.env reads beyond MIBLO_*, paths outside the app's folder, invisible characters and text aimed at the reviewer ("ignore previous instructions", "approve this app", role-play).

The AI review

After the scan, the code joins the AI review's queue (it runs every 10 minutes and takes a few minutes):

  • Two independent passes: the first writes the report; the second, with other instructions, checks the report against the code and says what it missed.

  • The report covers: the risk level, every network destination and why, what the app reads from the computer, what leaves it and to where, persistence, hidden behaviour, signs of malicious code (credential theft, mining, code loaded from the network, sandbox escapes, destructive actions) and attempts to steer the reviewer, with a summary in Portuguese and in English.

  • The AI never approves or refuses. The verdict ("ok" or "needs a careful look") is computed by the server: the passes disagreeing, a high risk, text aimed at the reviewer or an undeclared host always ask for a careful look.

  • Guarded against manipulation: the code goes in as data, between markers, with the fixed instruction that nothing in it is an instruction; the answer is validated against a schema; texts are capped.

The human approval

The reviewer sees the scan's findings, both reports, the hosts and, on a revision, what changed since the previous version. Only then do they approve it (miblo.ai signs the package) or refuse it with a reason, which reaches you by e-mail. The app's public page says "This app talks to: ..." (the declared, verified hosts) or "Does not use the internet".

After it is published

  • Revisions: an edit is a new revision, which goes the same way. The approved version stays live until the next one is approved, and there is one pending revision at a time.

  • Ratings: members give 1 to 5 stars; the average and the count show in the store.

  • Reports: a report hides the app at once until someone reviews it (each member hides at most two apps a day). Moderators can hide and restore.

  • A hidden app, for those who installed it: at every start and every hour, Miblo checks the package; an app the store no longer serves or has hidden is turned off.

How the package reaches its users

The Enable on my Miblo button opens the Miblo app on your app (miblo://apps/enable/<id>), or the person runs miblo apps install <id>. Miblo then:

  1. downloads miblo.ai/apps/<id>/package.json and checks its signature with the apps key (separate from the updates' key), refusing a package without a valid one;

  2. downloads the zip over https (no redirects, never more than 1 MB) and checks its signed SHA-256;

  3. checks every file, unpacks it into <Miblo's data>/apps/<id>/pkg and turns the app on in the sandbox.

Test on your computer before submitting

Run your main.js the way Miblo will, with a small script and the SDK:

JavaScript
// run-local.mjs: calls run(ctx) as Miblo does and draws the card. node run-local.mjs
import fs from 'node:fs';
import { MibloStatus } from '@miblo/status';
import { run } from './main.js';

const manifest = JSON.parse(fs.readFileSync(new URL('./manifest.json', import.meta.url), 'utf8'));
const miblo = new MibloStatus();
const tool = manifest.name.en.slice(0, 20);
const app = miblo.app({ id: manifest.id, tool });
const screen = miblo.screen({ tool });
await app.declare({ name: manifest.name, category: manifest.category, promise: manifest.promise, settings: manifest.settings, preview: manifest.preview });
const state = {};
for (;;) {
  const { values, missing } = await app.settings.get();
  if (missing.length) { await app.settings.request(missing, 'Fill these in to test'); await app.settings.wait(); continue; }
  const out = await run({ settings: values, state, lang: 'en', locale: 'en-US', now: Date.now, sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
    timeOf: (ms) => new Date(ms).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }),
    fetchJson: async (u) => (await fetch(u)).json(), fetchText: async (u) => (await fetch(u)).text(), stateDir: './state' });
  await screen.card({ v: 1, ...out.card });
  if (out.peek) await screen.peek(10).catch(() => {});
  await new Promise((r) => setTimeout(r, manifest.intervalMs));
}
Terminal
export MIBLO_TOKEN=$(miblo sdk token)
node run-local.mjs                                   # running in the terminal
miblo apps add "node run-local.mjs" --id tide        # or under the supervisor
miblo apps enable tide && miblo apps logs tide
miblo screen status                                  # what the App screen holds now

This script is not the sandbox: it blocks no modules or hosts. Check yourself that the code uses only ctx.fetchJson/ctx.fetchText and the hosts in network, with none of the blocked modules, before you submit.

Checklist before submitting

  • Miblo+ is active on your account.

  • The app answers one question and its promise says which, in Portuguese and in English.

  • The card fits: title up to 24, labels up to 16, big up to 10, text up to 40 characters, up to 4 items, none of Miblo's phrases.

  • The frames are 8-bit PNGs with flat colours that fit in 60000 bytes; what changes is on the card, not the frame.

  • The settings ask for no secret, and the required ones have a clear reason in settings.request().

  • network lists exactly the hosts the code uses ([] when offline), all over https://.

  • No blocked module, no dependency, no eval, no text for the reviewer.

  • intervalMs is at least 10000, and the app asks for a peek only when something important happens.

  • The app works offline (shows its last values and since when) and unconfigured (asks with request).

  • Tested on your computer under the supervisor, with a clean miblo apps logs.

  • Icon, screenshots, description and "what changed" filled in.