Skip to content

Build a mini app

A mini app is Localix's own application format: screens for the person, a native backend, agent tools over Localix's APIs, a guide for the agent and commands with pickers. It is not a plugin. A Claude Code plugin gives the agent skills, commands, agents, hooks and servers in Claude Code's format; the two share nothing. Use this page to write a mini app's manifest and backend and to see what the host gives it. To install, switch on or remove mini apps, see Mini apps. Localix's own mini apps are in the Localix marketplace.

Trust boundary: A mini app you add awaits your yes before its code runs; one the agent builds for you starts enabled. A mini app's native backend runs as trusted code in the API process.

Terminology

TermMeaningBoundary
Mini appA folder with one .localix-mini-app/mini-app.json: backend, screens, views, settings, commands.Localix only. It holds no .claude-plugin/, skills or hooks.
GuideA mini app's guide.md, read by the agent with MiniAppGuide before it uses the app.Read on demand; the prompt lists only that the app has one.
Native backendA JavaScript or TypeScript module whose register(ctx) the API process loads.Declared by a mini app's backend; not sandboxed.
WorkbenchA mini app's screen in the workspace, built from ui/register.tsx.Opens as a tab; a rail screen can also become a Mac app.
Marketplace mini appA mini app installed from a marketplace, the Localix marketplace's included.Its installed copy lives in ~/.localix/mini-apps/ with yours.
PluginA Claude Code plugin, .claude-plugin/plugin.json and its components.Not a mini app; see Claude Code plugins.

What you can do

CapabilityMechanismBoundary
Have the agent build oneDescribe what it should do, with the authoring plugin; see Build with the agent.Needs the authoring plugin from the Localix marketplace; the app it installs is enabled at once and runs unsandboxed.
Add tools over Localix APIsA mini app's backend exports register(ctx) returning tools.Native code runs in process and is not sandboxed.
Teach the agent your appWrite the mini app's guide.md; long material goes in pages under guide/.The agent reads it with MiniAppGuide; nothing of it sits in every prompt.
Offer a mini app commandDeclare it under commands in mini-app.json, with its prompt and argument pickers.It runs as /<app>:<command>.
Shape runtime behaviorReturn lifecycle hooks, a channel, or one memory provider from register.Hook timeouts and optional host services limit these contributions.
Add a native workbenchShip ui/register.tsx; the app builds the artifact and swaps it in place.Installed from a marketplace, or your own under ~/.localix/mini-apps once on.
Add portable UIDeclare sandboxed HTML views; each opens as a work-item tab.A sandboxed view cannot use the host kit or native React registrations.
Run as its own Mac appThe person installs the mini app as an app; see Mini apps as Mac apps.macOS only; the mini app needs a rail viewlet, its main screen.

What you can build

A mini app can be anything from a calorie counter to a database client, and it is part of Localix from its first build. Every chat already has an agent that reads files and images, so a mini app keeps state, shows it and gives that agent tools; it needs no model, upload pipeline or OCR of its own. A calorie counter is a log_meal tool and a screen: you send a meal photo, the agent reads it with a model that accepts images and calls the tool. The workbenches and games of the Localix marketplace are mini apps on the same contract, and the larger ones run to tens of thousands of lines.

What a mini app usesThroughGuide
The chat agenttools from register(ctx) register with the agent; no glue codeNative module contract
A turn started by the appctx.host.wake with a message and the app's own toolsWakes
Decision modelsctx.decisions.decide or decide(ctx, …): typed questions, spend recorded per chatSemantics
Embeddings and semantic searchctx.semantic: index its own items, query by meaning, rank or embed textsSemantics
Suggestions in its own fieldstextInputCompletionSurfaces and useMiniAppTextInputCompletionWorkbench UI
Vault secretsctx.host.resolveSecret, storeSecret (a free key only) and bindSecretVault
Storage per chat and per projectctx.dataDir, ctx.sessionStorage, ctx.host.workspaceScope and workspacesOne chat's data
Change feeds and live pushctx.host.changes, the store feed, wsFrames streams on Localix's one socketSync
Context for the agentresourceProvider, searchProvider and contextProviders from backendNative module contract
Chat mediactx.host.media.getBytes(id) for an upload it was handed; putBytes for what it produced—
Terminals and filesctx.terminalRead (read-only), ctx.watchPath, host.fileEditors for a file familyWatching files
Routes and screensrouteMount with routeValidator; ui/register.tsx built against the host's kitRoutes, UI
Its own Mac appInstall Name as App from its tab, rail button or the command paletteMac apps

Availability: ctx.host and most of its services are optional; a per-run load (tools, hooks) has no host, so a backend checks before it calls. What both loads need is on the context itself: ctx.sessionStorage, ctx.agents and ctx.decisions. Decision models need a connection that serves one; semantic search answers once its model is on this computer.

How it works

Mini app or plugin

A mini app is what the person uses, in Localix's format; a plugin is what the agent uses, in Claude Code's. A folder is one or the other: a folder with both .localix-mini-app/ and .claude-plugin/ loads nothing and says why. A mini app tells the agent how to use it in its guide.md, not in skills; the prompt lists each enabled mini app in one line, marked when it has a guide, and the agent reads the guide with MiniAppGuide before it first uses the app. The contract records the rules.

Tools over Localix's APIs, settings or a screen make a mini app; skills, commands, hooks and MCP servers for any agent make a plugin. For a workbench, the authoring plugin's create command forks its Daily Log template into a new folder and installs it when it passes its checks; see workbench UI.

A minimal mini app

A mini app is its own folder, with no plugin in it: .localix-mini-app/mini-app.json, guide.md, backend/index.ts.

JSON
{
  "name": "calorie-counter",
  "version": "0.1.0",
  "description": "Logs meals and their calories.",
  "apiVersion": 1,
  "category": "utilities",
  "backend": "./backend/index.ts"
}

guide.md is plain Markdown for the agent: when to call log_meal, what to extract from a meal photo, and what to tell the person afterwards. The backend gives the agent that tool:

TypeScript
import { appendFile } from 'node:fs/promises'
import { join } from 'node:path'
import type { MiniAppContext } from '@localixai/mini-app-sdk/backend'

export function register({ z, defineTool, dataDir }: MiniAppContext) {
  return {
    tools: [
      defineTool({
        name: 'log_meal',
        activeVerb: 'Logging a meal',
        description: 'Record a meal the person ate, with its items and estimated calories.',
        inputSchema: z.object({ items: z.array(z.string()), calories: z.number() }),
        mutates: true,
        classify: () => ({ effect: 'host' }),
        async execute(_toolCallId, input) {
          const entry = { at: new Date().toISOString(), ...input }
          await appendFile(join(dataDir, 'meals.jsonl'), `${JSON.stringify(entry)}\n`)
          return { content: `Logged ${input.calories} kcal.` }
        },
      }),
    ],
  }
}

classify tells the permission gate where the change lands (host: the app's own data), so a session that refuses network effects still allows it. An installed backend has no repository dependency tree: use relative modules and built-in modules only, or pre-bundle dependencies.

The app layer

A mini app that keeps a list does not write its routes, storage or live updates. With "apiVersion": 2, the SDK's app layer turns a declared collection into all three. This is how the Daily Log template of the authoring plugin is built:

TypeScript
import {
  createMiniApp,
  defineCollection,
  type MiniAppContext,
} from '@localixai/mini-app-sdk/backend'

const mealsSpec = defineCollection({
  name: 'meals',
  item: mealSchema,
  input: mealInputSchema,
  create: (input, { id, now }) => ({ ...input, id, at: input.at ?? now.toISOString() }),
  scope: 'app',
  search: (meal) => `${meal.name} ${meal.note}`,
})

export function register(ctx: MiniAppContext) {
  const app = createMiniApp(ctx)
  const meals = app.collection(mealsSpec)
  return app.registration({ tools: createTools(ctx, meals) })
}

meals has list, get, add, update, remove and search, for the tools to call. app.registration mounts GET and POST on /rpc/<app>/meals and PATCH and DELETE on /rpc/<app>/meals/:id, each answering with the whole list. It also adds the feed that updates an open screen when a tool writes and the index that finds items by meaning. scope: 'chat' keeps one list per chat instead, and routes or backend fields add anything else.

decide, rankByMeaning and askAgent from the same module reach the host's decision model, embedding model and chat agent, and answer with a fallback when none is available. @localixai/mini-app-sdk/backend/testing tests the backend through what the agent and the screen do: testMiniAppContext, callTool, requestRoute and fakeSemantic. The screen's side (useCollection, screenUi) is in Build workbench UI.

Scope and enablement

Session scopeMini app folderAlso visible
Project<project>/.localix/mini-apps/<name>/Your own and the ones installed from a marketplace
No project~/.localix/mini-apps/<name>/The ones installed from a marketplace are kept here

A project session sees your personal mini apps as well as its own. On a name collision yours wins and the project's copy is hidden, so a repository cannot run its code under a permission you gave your own app. A mini app keeps its data in ~/.localix/mini-apps-data/<name>/, which outlives updates and reinstalls. A personal mini app has one configuration in every project; a secret setting is kept in the vault. Use the Store (Mini apps) or /mini-app to switch one on or off and configure it; after copying a folder into a root, run /mini-app reload.

Native module contract

register(ctx) may return these contributions:

Return fieldEffectBoundary
toolsAdds validated model-facing tools.Tool names cannot shadow built-ins.
hooksObserves or transforms supported message and tool lifecycle points.Each invocation is isolated and capped at five seconds.
backendAdds routes, resource and search providers, context providers, completions, live streams.Optional boot-time host services may be absent during per-run loads.
channelsStarts long-running messaging bridges.Channels come from native code, not a manifest field.
memoryAdds one external memory provider.The first enabled provider wins; output and waits are capped.

Import SDK contracts with import type from @localixai/mini-app-sdk/backend; runtime values (z, defineTool, config, helpers, paths, logging, UI intents, OAuth) come from ctx; hono, zod and the SDK are the host's instances. The host calls register(ctx) more than once and reuses the module: keep what a person creates under ctx.dataDir (one chat's data under ctx.sessionStorage), a warm service at module scope. execute takes the call id, then input.

Hooks and commands owns hook timing and command behavior; the SDK owns the types.

Results a tool reports

The transcript shows a call with isError: true as failed, so keep it for a call that did nothing because the model got it wrong. The Localix marketplace's game and board tools follow these rules:

CaseReport
A batch whose later step is refused after earlier steps changed stateSuccess: what the earlier steps did, then which step was not run and why.
A step the model could not predict (a random tile, a sunk ship)Skip it or stop the batch there; do not fail the call.
Control passes to someone else mid-batch (a new game the user opens)Stop the batch and report it as success.
A lookup that finds nothing (no connection configured)A plain answer, not an error.
The same input that changes state on every call (a list of moves)Declare behaviorHints: { idempotentHint: false }; the repeat guard then also compares results.

Without that hint, the host's repeat guard counts identical calls regardless of their results and stops the run on the eighth in a row.

A tool that produced something to look at offers it with ctx.ui.offer({ uri, label }). Returned as details, the transcript draws it as a button; under details.reveal, beside other data, the app's own tool card decides how to show it. Either way the host appends App view: <uri> to the model's result unless the content already names the URI. An app never opens a view itself; the agent gives the person that localix:// link, and the person opens it.

Identity the host binds

An app that records who acted, or streams someone's own data, must not read that identity from model or client input. The host binds it; a task-board mini app in the Localix marketplace uses all three:

SeamAnswerBoundary
ctx.agents.list(userId, sessionId)The enabled agents of the session's scope, its project's included: { id, name, description? }Read-only; rejects a session the user does not own. Store id.
context.session.agent in execute{ agentId, name } of the agent whose turn it isAbsent when the run has no addressable speaker.
start(params, send, { userId }) on a wsFrames entryThe host-supplied Localix user ID of the socketTags s, r and n are core's; registration rejects them.

Watching files

A backend that reacts to files changing — logs it tails, a folder another program writes — uses ctx.watchPath(path, recursive, onEvent, onError), never node:fs.watch: Bun's recursive fs.watch on macOS delivers only part of a burst. The host's watcher (FSEvents, inotify, ReadDirectoryChangesW) reports every change and calls onError when events may have been lost; re-read once there. It exists on the boot-time load only, so a backend singleton keeps the one it was given, with a slow backstop poll for loads without it. To push a change to an open view, use a live stream.

UI paths

UI pathAuthoring formAvailability
Sandboxed viewA mini app views entry pointing to an HTML fileInstalled mini apps
Native workbench UIui/register.tsx, built into an artifact (guide)Mini apps the user enabled
Keyboard ownership@localixai/mini-app-sdk/ui/focus claims the keyboard for a surfaceHost and extensions share one stack

A sandboxed view uses acquireLocalixApi() for theme events, its owning chat, scratch state, context publication, and same-app backend actions. See Build a sandboxed HTML view for the bridge, scope rules, CSP and limits.

Configuration

All paths are relative to the mini app's folder and confined to it.

Mini app manifest

.localix-mini-app/mini-app.json in the folder (miniAppManifestSchema, @localixai/mini-app-sdk/app-manifest) refuses unknown keys, a plugin's skills or hooks among them:

FieldShapeEffect and boundary
namestring, requiredLetters, numbers, dashes or underscores; the namespace of its tools and commands.
version, descriptionstring, requiredThe app's version, and what it does, shown in Settings and in the agent's listing.
displayName, …author, homepage, repository, license, keywordsDisplay fields; a published homepage is an https:// address.
iconpath in the folderThe app's icon in the store, Settings and its Mac app. Required to publish; see icon.
screenshots{ path, caption? } array; max 10Shown on the app's store page at their own resolution; .png, .jpg or .webp, ≤5 MB each.
tagsstring array; max 8Lower-case words joined by dashes; the store filters and searches by them.
apiVersionpositive integerTargets the contract, currently 2. A newer version blocks loading; omission warns.
categorydevelopment, data, content, utilities or gamesGroups workbenches in the rail's tools menu; one per app.
surfacesstring array; ['agent']workbench adds a screen. Unknown values are retained.
privatebooleanKeeps an authoring scaffold out of packaging.
backendstring or string arrayThe native entrypoint(s) exporting register(ctx); a package names the built .mjs.
guidepath; ./guide.mdThe agent's guide; pages it links live in guide/<page>.md.
configconfig-option mapSettings delivered through ctx.config.
commandsmap keyed by command nameEach { prompt, description?, args?, argumentHint?, featured?, activateTools? } (max 8 tools).
viewsview array; max 6Sandboxed iframe UI, each a work-item tab; HTML capped at 1.5 MiB.
promptSections{ title, content } array; max 3Static text under the app's prompt section; title max 80, content max 2,000 characters.
fileAssociationsassociation arrayFile types the app opens inside Localix. The Store offers the app for such a file; they never reach the operating system.

The native UI is found by convention at ui/register.tsx. A views[] entry is { id, title, file, icon?, agentDescription? }; a fileAssociations[] entry is { ext, name, description?, mimeType?, role?, rank? } with role Editor, Viewer, Shell or None and rank Owner, Default, Alternate or None. An ext is an extension (docx), one of several parts (request.json, which package.json is not) or a whole file name (dockerfile). Declare every kind your UI opens with host.fileEditors or host.fileFormats: packing copies the list into the marketplace, and when someone opens a file of that kind that nothing they have opens, the file tab offers your app with Install. Rank a kind None to open it without being offered for it (the Store).

Config options

FieldType and defaultEffectBoundary
typestring, number, boolean, directory, or file; stringSelects the editor and scalar type.directory and file still store strings.
descriptionstring; emptyExplains the field.Empty text produces a warning.
requiredboolean; falseWarns when no effective value exists.A missing value does not itself block loading.
secretboolean; falseMasks the value in API responses.Native code receives the value; avoid defaults for secrets.
defaultstring, number, or booleanSupplies an initial effective value.Match the declared type yourself.
titlestring, optionalSupplies a human-readable label.Rendering hint.
multipleboolean, optionalMarks a field as accepting multiple values.Hint only; storage remains scalar.
min, maxnumber, optionalSupplies numeric bounds.Hints; stored values are not checked against them.

Commands and their pickers

A mini app's command lives in mini-app.json: its prompt takes ${name} for each declared argument, and $ARGUMENTS and $1 as in Claude Code. args adds chips and pickers: text or choice, static options or an optionsScript, and an optional { arg, equals } condition. The command runs as /<app>:<command>, and activateTools switches on up to eight deferred tools while it runs.

Earlier layouts

A .localix-app/app.json, a root mini-app.json or a root plugin.json is not read. A folder without .localix-mini-app/mini-app.json is not a mini app, and one with both .localix-mini-app/ and .claude-plugin/ loads nothing.

Test and package

A mini app brings its own tests; run them with bun test in its folder, or let the authoring plugin's check command run every gate. Bundle any package dependencies into the backend and point backend at that file. To offer it to others, pack it into a marketplace: see Publish a mini app.

Localix's own mini apps

Localix ships no mini app. Localix's own mini apps and plugins live in the Localix marketplace, a repository whose releases publish a mini app marketplace. The Store reads that marketplace by default, and its apps install, update and are verified like any other marketplace's. They are written against the same SDK and kit as yours, so their source is a reference for the contract on this page; their own guides live in that repository.

Limits

  • Native tools, hooks, channels and backends run as trusted local code; allowing a mini app is consent, not isolation. One you installed awaits your yes with its code not imported, and the agent can offer the approval through Configure. Its tools stay unknown until then.
  • A secret: true config value is masked in API responses but stored with the mini app's settings and supplied to trusted native code. Use the vault host service when a backend needs vault semantics.
  • File associations from installed mini apps are inert: desktop handlers are fixed at bundle build.
  • An installed backend cannot assume repository packages are available at runtime.

Source of truth