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.
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.
# 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 devThe sandbox is the exact runtime your applet gets in production, with device and theme toggles. Edit any file and the preview reloads instantly.
pnpm create freebeez-applet and yarn create freebeez-applet work too.Project structure#
The scaffold gives you a tiny, self-explanatory project:
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| File | Purpose |
|---|---|
applet.json | Manifest. Identifies your applet and points to the entry file. |
src/Applet.tsx | The entry. Its default export is your applet component. |
src/components/icon.tsx | Rendered to a static SVG for your catalog card. |
package.json | Provides the dev, build, and pack scripts via the FreeBeez CLI. |
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:
| Agent | Reads |
|---|---|
| Claude Code | CLAUDE.md |
| Cursor | .cursor/rules/freebeez.mdc |
| GitHub Copilot | .github/copilot-instructions.md |
| Gemini CLI | GEMINI.md |
| Codex, Zed & others | AGENTS.md (the cross-agent standard) |
Using a different tool? Point it at .agents/freebeez-applet.md and it has everything it needs.
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.
{
"id": "unit-converter",
"name": "Unit Converter",
"description": "Convert length, weight, and temperature",
"color": "#4A18D8",
"version": "1.0.0",
"entry": "src/Applet.tsx"
}| Field | Required | Description |
|---|---|---|
id | Yes | Unique, URL-safe id. Lowercase letters, numbers, and - . _ — starting with a letter or number. This is permanent. |
name | Yes | Display name in the catalog. |
entry | Yes | Entry file whose default export is your component (e.g. src/Applet.tsx). |
description | No | Short blurb for the catalog card. |
color | No | Brand color as a hex string, e.g. #4A18D8. |
version | No | Semver string. Bump it to publish an update (defaults to 1.0.0). |
react | No | Pin 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.
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.
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>
)
}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:
| Command | What it does |
|---|---|
npm run dev | Starts the local sandbox with live reload and device + theme toggles. |
npm run build | Builds the self-mounting bundle to dist/bundle.js — handy for debugging. |
npm run pack | Zips 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.
Publishing#
Shipping an applet is a short, predictable loop:
- Run
npm run packto produce your.zippackage. - 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.tsxis 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.
Dos & Don'ts#
- 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.
- 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
| Export | Signature | Description |
|---|---|---|
useTheme | () => ThemeMode | React hook for the current host theme; re-renders when it changes. |
getTheme | () => ThemeMode | Reads the current theme once (non-reactive). |
onTheme | (cb) => () => void | Subscribes to theme changes; returns an unsubscribe function. |
close | () => void | Asks the host to close the applet and return the user back. |
notifyReady | () => void | Signals the applet is mounted. The runtime already does this for you. |
Types
| Type | Description |
|---|---|
ThemeMode | "light" | "dark". |
AppletComponent | The type of a valid applet (a React component with no required props). |
AppletManifest | The typed shape of applet.json. |
HostMessage / AppletMessage | The 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.