Home → Mini Apps

Mini Apps

Ask for an invoice ledger and get one — a real single-page application with a SQLite database behind it, at its own local URL, still there tomorrow. A chat answer disappears into scrollback; this does not.

Shipped in v0.5.0. Off by default — turn it on under Settings → Mini Apps, which takes effect immediately.

An invoices Mini App: three summary tiles showing outstanding, overdue and paid totals, above a ledger table with reference, customer, dates, totals and status pills.
An invoices app, built to the contract every Mini App is handed.
The same invoices app rendered in dark mode.
Dark mode follows the reader's system setting, with a toggle available.

Two kinds

A page is the default and the right answer for most things: one HTML file with Alpine over a shared server that runs no code the model wrote. Nothing to install, serving the moment it is saved.

A Next.js app is a real Node application — its own server, routing, dependencies and process — for when the job genuinely needs server-side logic, several routes, or a database that is not SQLite. It is started and stopped from the panel and takes a few minutes the first time while its dependencies install.

That is a different bargain, not a bigger version of the same one. A page can be trusted with a database it cannot send SQL to because it cannot send anything; a Next.js app is server code, so the guarantee becomes what it can reach: its own process, its own port — its own origin, so it can reach neither aico nor another app — and an environment with every key, token and password stripped by pattern rather than by a list somebody has to remember to update.

What is not contained: a Next.js Mini App runs Node as you. cwd is pinned to its directory, but nothing stops server code reading elsewhere on the disk, and npm install runs third-party postinstall scripts. It is the trust you extend to any repository you clone and run — not a sandbox, and calling it one would be the dishonest part.

The second port is the design

A Mini App page is JavaScript a model wrote. The aico API runs shell commands and keeps its token in the workspace's localStorage. Serve both from one origin and generated code inherits both — not through malice, but because "fetch the thing at /api/" is a reasonable line for a generated page to contain.

So Mini Apps get their own port, which the browser enforces as a different origin for free. The separation is structural rather than careful: the module that serves Mini Apps cannot reach the agent, because it does not import it. Each server refuses the other's origin, and a connect-src 'self' CSP sits behind that.

Apps do not send SQL

The obvious design is an endpoint that runs whatever SQL the page sends. It is one function, it supports everything, and it is wrong here: every form field becomes a place where a bug turns into a statement, and a generated app that concatenates a search box into a WHERE clause is not hypothetical — it is the most likely thing for one to do.

Instead the page names a table and passes values. The server builds the statement, checks every identifier against the schema that actually applied, and binds every value. There is no path from a string in the browser to a token in a query.

await aico.db.list('invoices', { where: { status: 'sent' },
                              orderBy: 'issued_at', direction: 'desc' })
await aico.db.create('invoices', { customer: 'Acme', total: 1200 })
await aico.db.update('invoices', id, { status: 'paid' })
await aico.db.remove('invoices', id)

What every app is handed

A model asked to build an invoices app will otherwise write its own fetch wrapper, its own table markup, its own colours and its own idea of a form — every time, differently. So the parts that should not vary are shipped, and the guide describing them is returned at the moment an app is created rather than carried in the system prompt on every unrelated turn.

The shape on disk

A Mini App is a directory, so you can open it in an editor, diff it, copy it somewhere else, or read it without the tool that made it.

<workspace>/miniapps/<slug>/
  app.json          identity and timestamps
  schema.sql        the tables, applied at open
  data.sqlite       the database
  public/
    index.html      the app — one page, no build step

The schema is a file rather than an API because tables are the app's shape, and a shape the running page can change is not a shape — it is a suggestion. It is also reviewable: something you can read and keep, instead of a sequence of calls you would have to replay to understand.

A plugin, off by default

It is the one feature that opens a listening socket of its own, and that should be opted into rather than inherited. Turn it on under Settings → Mini Apps, or:

{
  "miniApps": {
    "enabled": true,
    "port": 7318          // default: one above the aico server's
  }
}

Off means the model cannot see the tool at all — not that it is asked not to use it. A tool that is present gets called eventually, and calling it while nothing is listening builds an app nobody can open.

Bound to loopback. There is no login, because an app that could authenticate would need somewhere to keep credentials and a way to be wrong about them — reachability is the boundary instead.

Tested where it matters

31 checks over real HTTP against a real database file, run by npm test: CRUD and column defaults, an injected ORDER BY, a quote in a value, a table outside the schema, encoded path traversal, a foreign origin, a constraint violation reported as the caller's fault — and a restart in the middle, because "the data is still there" is the one claim a single process cannot make about itself.