Developer documentation

Build a FreeBeez applet

An applet is a small, free web app that runs inside the FreeBeez web OS. If you can write a React component, you can ship one — there's no build config, no servers, and no infrastructure to manage.

What is an applet?

An applet is a single React component, exported as the default export of an entry file. FreeBeez bundles it, mounts it in an isolated frame, and serves it to users from the catalog.

  • Just React. Write components the way you already do. Hooks, state, the works.
  • Any npm package. Imports resolve from esm.sh at runtime — no install step, no allow-list.
  • Styling included. Tailwind utility classes and a dark: variant work out of the box.
  • Client-only. Applets run entirely in the browser. There's no backend to deploy.
Prerequisites
You'll need Node.js 18+ and basic React knowledge. That's it.

Quickstart#

Scaffold a project, install, and start the live sandbox. You'll have a running applet in under a minute.

terminal
# 1. Scaffold a new applet
npm create freebeez-applet@latest my-applet

# 2. Install dependencies
cd my-applet
npm install

# 3. Start the live sandbox (http://localhost:4321)
npm run dev

The sandbox is the exact runtime your applet gets in production, with device and theme toggles. Edit any file and the preview reloads instantly.

Tip
Prefer pnpm or yarn? pnpm create freebeez-applet and yarn create freebeez-applet work too.

Project structure#

The scaffold gives you a tiny, self-explanatory project:

my-applet/
my-applet/
├─ applet.json          # manifest: id, name, entry, …
├─ src/
│  ├─ Applet.tsx        # your applet (default-exported component)
│  └─ components/
│     └─ icon.tsx       # catalog icon (plain SVG)
├─ package.json         # scripts + dev dependencies
├─ tsconfig.json
└─ README.md
FilePurpose
applet.jsonManifest. Identifies your applet and points to the entry file.
src/Applet.tsxThe entry. Its default export is your applet component.
src/components/icon.tsxRendered to a static SVG for your catalog card.
package.jsonProvides the dev, build, and pack scripts via the FreeBeez CLI.
These are the minimum
applet.json stays at the root; all source lives under src/. Only applet.json, the entry, and src/components/icon.tsx are required. As your applet grows, add as many files and folders as you like — more src/components/, a src/lib/ for logic, src/hooks/, and so on. The bundler follows your imports, so any structure works.

Build with AI#

Every scaffolded project is agent-ready. Open it in your AI coding tool and just describe what you want to build — the agent already knows the platform rules, the SDK, and the best practices on this page, so you don't have to explain them.

The full agent guide lives in .agents/freebeez-applet.md. Each supported tool ships with an entry point that points to it:

AgentReads
Claude CodeCLAUDE.md
Cursor.cursor/rules/freebeez.mdc
GitHub Copilot.github/copilot-instructions.md
Gemini CLIGEMINI.md
Codex, Zed & othersAGENTS.md (the cross-agent standard)

Using a different tool? Point it at .agents/freebeez-applet.md and it has everything it needs.

Tip
Let the agent finish the loop: ask it to run npm run build and check the result in npm run dev before you package and submit.

The manifest#

applet.json describes your applet to the platform. Only id, name, and entry are required.

applet.json
{
  "id": "unit-converter",
  "name": "Unit Converter",
  "description": "Convert length, weight, and temperature",
  "color": "#4A18D8",
  "version": "1.0.0",
  "entry": "src/Applet.tsx"
}
FieldRequiredDescription
idYesUnique, URL-safe id. Lowercase letters, numbers, and - . _ — starting with a letter or number. This is permanent.
nameYesDisplay name in the catalog.
entryYesEntry file whose default export is your component (e.g. src/Applet.tsx).
descriptionNoShort blurb for the catalog card.
colorNoBrand color as a hex string, e.g. #4A18D8.
versionNoSemver string. Bump it to publish an update (defaults to 1.0.0).
reactNoPin the React version loaded from esm.sh (defaults to the platform version).

Writing your applet#

Export a React component as the default export. That's the entire contract.

src/Applet.tsx
import { useState } from "react"

export default function Applet() {
  const [count, setCount] = useState(0)
  return (
    <div className="flex min-h-[100dvh] items-center justify-center bg-white dark:bg-[#0f1115]">
      <button
        onClick={() => setCount((c) => c + 1)}
        className="rounded-2xl bg-[#4A18D8] px-6 py-3 font-bold text-white active:scale-95"
      >
        Clicked {count} times
      </button>
    </div>
  )
}

Importing packages

Import anything from npm — it's loaded from esm.sh at runtime. No install, no bundler config.

import { format } from "date-fns"
import confetti from "canvas-confetti"

Styling & dark mode

Tailwind utility classes are available, including the dark: variant. The host toggles the theme for you — apply dark: styles and they just work.

Choosing a React version

By default your applet shares the platform's React. To pin a specific version, set react in the manifest (e.g. "react": "19.2.4").

Using the SDK#

@free-beez/applet-sdk (already a dependency in the scaffold) gives you a typed contract with the host: theme awareness and a way to close your applet.

src/Applet.tsx
import { useTheme, close } from "@free-beez/applet-sdk"

export default function Applet() {
  const theme = useTheme() // "light" | "dark", updates live

  return (
    <div className="p-6">
      <p>Current theme: {theme}</p>
      <button onClick={close}>Close</button>
    </div>
  )
}
Note
The SDK is optional sugar over a small postMessage protocol. Dark mode already works through Tailwind's dark: variant — reach for useTheme() only when you need the value in JavaScript. See the SDK reference for the full surface.

Local development#

The CLI gives you three commands, available as npm scripts:

CommandWhat it does
npm run devStarts the local sandbox with live reload and device + theme toggles.
npm run buildBuilds the self-mounting bundle to dist/bundle.js — handy for debugging.
npm run packZips your applet into an upload-ready <id>-<version>.zip.

You can also call the CLI directly with npx freebeez dev, npx freebeez build, or npx freebeez pack.

Tip
The first load in the sandbox fetches your dependencies from esm.sh and can take a few seconds. Subsequent loads are cached and fast.

Publishing#

Shipping an applet is a short, predictable loop:

  • Run npm run pack to produce your .zip package.
  • Open the Submit page in the developer portal and drop in the .zip.
  • Pre-flight checks run automatically — valid archive, manifest, id availability, a clean build, and a fresh version.
  • An interactive preview shows your applet exactly as users will see it. Try it before you submit.
  • Pick a category and submit. It goes live instantly.

Publishing updates

To update a live applet, bump version in applet.json and submit again. The id stays the same; re-using an existing version is rejected.

Rules#

A few hard requirements keep the catalog safe and consistent. Submissions that break these are rejected at pre-flight.

  • The entry must default-export a component. FreeBeez mounts your default export — nothing renders without it.
  • ids are unique and permanent. An id taken by another developer is blocked, and you can't change yours later.
  • Bump the version to update. Each published version of your applet must be new.
  • The icon is plain SVG. src/components/icon.tsx is rendered to static markup on the server, so it may only import React — no hooks, no other packages.
  • Applets are client-only. There is no server, no Node APIs, and no filesystem. Everything you ship runs in the user's browser.
Your bundle is public
Anything in your applet — code, strings, keys — is downloadable by anyone. Never embed secrets, private API keys, or credentials.

Dos & Don'ts#

Do
  • Design mobile-first, then check the desktop view.
  • Support dark mode with dark: variants.
  • Keep dependencies few and small for fast first loads.
  • Handle empty, loading, and error states gracefully.
  • Use the SDK close() for a back/close action.
Don't
  • Ship secrets or private keys — the bundle is public.
  • Assume a backend, Node APIs, or the filesystem exist.
  • Try to break out of the frame or read the host page.
  • Pull in huge libraries for a tiny feature.
  • Hard-code light-only colors that break in dark mode.

Best practices#

Responsive by default

Applets run on phones and desktops. Build mobile-first and use the sandbox's device toggle to check both before you ship.

Respect the theme

Pair every light style with a dark: counterpart, or read useTheme() when you need the value in logic. Avoid hard-coded backgrounds that only look right in one theme.

Stay lean

Dependencies download from esm.sh on first load, so fewer and smaller packages mean a snappier applet. Prefer the platform and small, focused libraries.

Be accessible

Use semantic elements, label your controls, and make sure everything works with the keyboard. It's good for everyone.

SDK reference#

Everything exported from @free-beez/applet-sdk.

Helpers

ExportSignatureDescription
useTheme() => ThemeModeReact hook for the current host theme; re-renders when it changes.
getTheme() => ThemeModeReads the current theme once (non-reactive).
onTheme(cb) => () => voidSubscribes to theme changes; returns an unsubscribe function.
close() => voidAsks the host to close the applet and return the user back.
notifyReady() => voidSignals the applet is mounted. The runtime already does this for you.

Types

TypeDescription
ThemeMode"light" | "dark".
AppletComponentThe type of a valid applet (a React component with no required props).
AppletManifestThe typed shape of applet.json.
HostMessage / AppletMessageThe message types behind the host protocol, if you'd rather use it directly.

Troubleshooting#

My applet renders blank

Confirm your entry file has a default export, and that every package you import actually exists on esm.sh at the version you requested. Remember the first load downloads dependencies and can take a few seconds.

Pre-flight says the build failed

Run npm run build locally to see the exact compiler error, fix it, then re-pack.

"Version already exists"

You're re-submitting a version that's already live. Bump version in applet.json.

A package won't load

Double-check the exact npm name and that the version is published. Visit https://esm.sh/<package> in your browser to confirm it resolves.

Ready to ship?

Scaffold an applet and submit it to the catalog.