Docs

Putting an app on Boxfort

Boxfort hosts small web apps made of static files and gives each one a link, a database and sign-in for the people who use it. This page covers everything a creator needs. Boxfort is in alpha, so a few pieces are marked as coming soon.

Getting started

An app on Boxfort is a folder of static files with an index.html at the top: plain HTML, CSS and JavaScript, or the output of a static build (Vite, Astro, SvelteKit, Create React App). Boxfort serves those files as they are. It never runs a build and never runs server code.

Each app lives at its own address, <app>--<handle>.boxfort.app, where the handle is the one you pick when you sign up. Sign-up is open to anyone and only needs an email address you can confirm.

There are three ways to put an app online:

Use Boxfort from Claude

The Boxfort connector lets Claude build an app and put it online without a computer. Add it once and it works in Claude on iPhone, Android, the web and the desktop app.

Add the connector

  1. In Claude, open Settings, then Connectors, and choose Add custom connector.
  2. Name it Boxfort and paste this address:

    https://mcp.boxfort.app/mcp

  3. Choose Connect. Claude opens a Boxfort page: type the code we email you (this also creates your account if you don't have one, and asks you to pick a handle), then tap Allow.

On iPhone and Android: connectors you add in one place show up everywhere you're signed in to Claude. If your Claude app doesn't offer Add custom connector, add it at claude.ai in your phone's browser (or on a computer), then open the app. In a chat, make sure Boxfort is switched on in the tools menu.

Claude's Free plan allows one custom connector. On a Team or Enterprise plan, an owner may need to add the connector for the organization first.

Just ask

  • "Make me a grocery list app and put it on Boxfort."
  • "Add a quantity field to my grocery app."
  • "Which of my Boxfort apps have the most visitors?"
  • "Roll my chore chart back to the last version."

What the connector can do

ToolWhat it does
get_boxfort_guideTells Claude how a Boxfort app is shaped: static files, box.collection, box.json and the limits.
deploy_appTakes the app's files (up to 200 files and 5 MB per call), runs the same checks as every other upload, copies allowed CDN files into the app and deploys it. The first deploy of a name creates the app. Source that needs a build step is refused.
list_appsYour apps, their links, live versions and active visitors.
get_app_filesThe live version's files, so Claude can change an app it made in an earlier chat.
rollback_appMakes an earlier version live again (by default, the one before the current one).
app_statusChecks an app when something seems wrong: whether it's paused, full, out of this month's requests or storage, has a deploy waiting for review, or has open reports, and what to do about each.
open_ticketOpens a support ticket, or an appeal of a pause, with Boxfort's team. Replies come by email and on the dashboard's Support page.

Signing in and out

The connector signs in with OAuth: Claude never sees your email code. Allowing it creates an API token called "MCP: Claude" on your dashboard's API tokens list. Revoke that token to disconnect Claude; it asks you to sign in again next time. If you stop using the connector, its sign-in expires after 90 days.

Upload a built app

First time: sign up and upload

Upload your app on the home page takes you through it in one go:

  1. Drop your app: a built folder, a zip of it, or a single HTML file (served as index.html). Give it a name.
  2. Confirm your email with the code we send.
  3. Pick your handle. Your app deploys and you get its link.

Your files stay in the browser tab until your email is confirmed, so nothing is uploaded for an address that isn't yours. If you already have an account, signing in works the same way, and Boxfort asks before replacing an app that already has that name.

After that: drag and drop

The dashboard's home page and each app's page take a new version. Drop the folder your build makes (usually dist), a zip of it, or use Choose folder / Choose zip. The folder needs an index.html at its top.

  • node_modules, .git and system files are left out automatically.
  • Your browser checks the files first and lists what it finds. If you drop a project's source (a package.json with a build step), it tells you the build command to run, such as npm run build, and which folder to drop afterwards. Boxfort never builds apps for you.
  • Items marked fix, such as a script from a CDN, can still be deployed with Deploy anyway, but they won't load until they're bundled (see CDNs).
  • The upload then gets the upload checks. You see the live link and any warnings, or the reason it was stopped. A refused upload never goes live and never creates an app.

Versions and rollback

Each app keeps its 5 newest versions plus the live one. An app's page lists them, and any of them can be made live again in one click.

App settings

Each app's page on the dashboard lets you:

  • Pause the app. Visitors see a "paused" page and new deploys are refused. Nothing is deleted.
  • Set the visitor cap: how many active visitors the app takes, up to your plan's limit.
  • Set the guest window: how long a guest can be away before they stop counting toward the cap (see visitors).
  • See visitors, newest first, and ban anyone. A ban blocks that visitor on every device and frees their seat. Or delete a visitor, which removes them and everything they created in the app, including shared documents. Deleting can't be undone.

Libraries and fonts from CDNs

For safety, an app on Boxfort may only load files from its own address. Scripts, stylesheets, fonts, images and API calls pointing at other sites are blocked by the browser.

When you deploy with the Claude connector, Boxfort handles the common case for you. Anything the app loads from these CDNs is copied into the app at deploy time and its links are rewritten:

  • fonts.googleapis.com
  • fonts.gstatic.com
  • cdnjs.cloudflare.com
  • cdn.jsdelivr.net
  • unpkg.com

Fonts a stylesheet refers to and modules a script imports are copied too, and integrity hashes are checked before they're dropped. Up to 80 copied files, 15 MB in all and 6 MB per file. Use exact versions in CDN links (react@18.3.1, not react).

  • A script or stylesheet from any other site is refused with a fix. For example, use https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4 instead of cdn.tailwindcss.com.
  • Images, API calls and embeds from other sites only get a warning, but won't load.

When you upload a folder from the dashboard, CDN files aren't copied yet: install the library with your package manager so your build bundles it, or save the file into your app's folder and link to it there.

React without a build

Claude on your phone can't run npm run build, and neither does Boxfort. A single-page React app still works by loading React and Babel from unpkg and compiling JSX in the browser:

<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/@babel/standalone@7.26.4/babel.min.js"></script>

<div id="root"></div>
<script type="text/babel">
  function App() {
    const [items, setItems] = React.useState([]);
    React.useEffect(() => { box.collection("items").list().then(setItems); }, []);
    return <ul>{items.map((i) => <li key={i.id}>{i.name}</li>)}</ul>;
  }
  ReactDOM.createRoot(document.getElementById("root")).render(<App />);
</script>

Use the React and ReactDOM globals, not import, inside text/babel scripts. Deploy this through the connector so the three files get copied in.

Pictures in your app

Put your app's photos and pictures in a folder called images and Boxfort sizes them for each visitor. A phone photo you drop in at full size is shrunk before it's sent, and browsers that can show AVIF or WebP get those smaller formats.

  • Without anything extra, a picture is sent at most 1920 pixels wide.
  • Add ?w= for a smaller size, for example <img src="/images/cat.jpg?w=400"> for a thumbnail. Widths round up to a set of common sizes, and a picture is never made bigger than it is.
  • PNG, JPEG, WebP and AVIF files up to 10 MB are resized. GIFs (so animations keep moving), SVGs and larger files are sent as they are.
  • Each size is made once and kept. Making one counts toward your plan's monthly image resizes (see limits). Past that, pictures are sent as uploaded until the next month, so nothing breaks.

Pictures anywhere else in your app are sent exactly as uploaded. Every file, including the originals in images, counts toward your upload size.

Saving data: the box SDK

Every page on Boxfort already has box loaded; you don't add a script tag. The first time the app saves or reads data, the visitor gets a guest account automatically.

The same code runs anywhere. Away from Boxfort (a Claude preview, a file opened on your laptop) box saves to the browser's localStorage instead, so you can try an app before it's online. box.mode is "platform" on Boxfort and "local" everywhere else.

// a collection is a named list of documents
const items = box.collection("groceries");

const milk = await items.add({ name: "Milk", done: false });
// -> { id, createdAt, updatedAt, name: "Milk", done: false }

const todo = await items.list({
  where: { done: false },
  orderBy: "-createdAt",   // "-" for newest first
  limit: 50,
});

await items.update(milk.id, { done: true });   // shallow merge
await items.get(milk.id);                      // the document, or null
await items.remove(milk.id);

// live: called now, then whenever the list changes (anyone's change, in a shared collection)
const stop = items.subscribe((docs) => render(docs), { where: { done: false } });
CallDoes
box.collection(name)Opens a collection. Names use letters, numbers, - and _, up to 64 characters. Collections don't need creating first.
add(data)Saves a new document and returns it with id, createdAt and updatedAt added.
list(options)Returns documents, oldest first unless you set orderBy.
get(id)One document, or null.
update(id, patch)Merges the given fields into the document (top level only) and returns it.
remove(id)Deletes the document.
subscribe(callback, options)Calls callback with the documents (same options as list) right away and again whenever they change, including other visitors' changes to a shared collection. Returns a function that stops it. Add onError to the options to handle a failed refresh.

list options

  • where: up to 10 fields. A value matches exactly ({ done: false }); { in: [...] } matches any of up to 50 values; { gt, gte, lt, lte } compares numbers or strings.
  • orderBy: a field name, with - in front for descending. Defaults to createdAt.
  • limit: defaults to 100, at most 200.

Accounts

Every app shows a small account button in a corner. It opens a Boxfort sheet where the visitor can add their email (to keep their account and open it on other devices), sign in, set a display name, sign out, delete their data, or report the app. The sheet is Boxfort's own page, so the app can't change it.

const me = await box.account.current();
// -> { id, isGuest, email, displayName, isOwner }

box.account.open();                         // open the sheet from your own Settings button
box.account.onChange((me) => render(me));   // after a claim, sign-in, sign-out or new name; null once signed out
box.overlay.configure({ position: "bottom-left" });  // or top-left, top-right; bottom-right by default
box.overlay.hide();                         // once you have your own button that calls open()
  • isOwner is true when you, the app's creator, are signed in with the email of your Boxfort account. Use it to show editing screens, and mark the collections only you may change as owner-write in box.json. Your dashboard's app page has a link that signs you in. You can always get in, even when the app is full.
  • If the app doesn't call onChange, the page reloads when the visitor switches to another account, so nobody sees the previous person's data.
  • Away from Boxfort there is one local visitor, who counts as the owner, and no button.

Images visitors upload: box.files

For photos, avatars and other pictures, use box.files instead of putting images in documents:

const photos = box.files("photos");
const f = await photos.upload(input.files[0]);
// -> { id, url, name, type, size, createdAt, mine }
img.src = f.url;                              // served by Boxfort from your app's address
await posts.add({ caption, photo: f.id });    // keep the id in a document
const all = await photos.list();              // newest first; { mine: true } for just the visitor's
await photos.remove(f.id);
  • Images only: PNG, JPEG, GIF, WebP or AVIF, up to 10 MB each. Boxfort checks the file itself, so a renamed file is refused, and SVG isn't allowed because it can carry code.
  • Uploads count toward your plan's storage. A folder is private to each visitor unless box.json shares it under "files".
  • You can delete any image in your app when you're signed in to it. If visitors see each other's images, put a Report link next to them: /_box/report?file= plus the image's id.
  • Away from Boxfort, uploads stay in the page's memory until it reloads.

Good to know

  • Store one document per item, not one big array, so two people editing at once don't overwrite each other.
  • A document can be up to 64 KB, and an app can hold up to 10,000 documents. id, createdAt and updatedAt are set by Boxfort and can't be written.
  • Every call returns a promise. A failed call throws an error whose message includes the reason, such as the app being full or the visitor saving too quickly.

Who sees what: box.json

By default each visitor only sees their own documents. To share a collection, add a box.json file next to index.html:

{
  "name": "Grocery list",
  "description": "One list for the whole house.",
  "collections": {
    "groceries": { "access": "shared" }
  }
}
AccessWho readsWho changesGood for
private (default)Only the visitor who made itOnly themJournals, personal trackers
shared-readEveryone in the appEach visitor edits and deletes their ownRecipe boards, sign-up sheets
sharedEveryone in the appEveryoneShared lists, chore charts
owner-writeEveryone in the appOnly you, signed in to the app with your Boxfort emailMenus, announcements, content you publish

The same modes work for box.files folders: add "files": { "photos": { "access": "shared-read" } }. For files, "changes" means uploading and deleting, and you can delete any image.

description (up to 200 characters) shows on the label visitors see before the app opens and in link previews. Through the connector, Claude can pass the same settings as deploy_app's manifest.

Calling APIs: Connections

An app can call OpenAI, Anthropic, Google Gemini, Resend (email) or another API with your key, without the key ever being in the app. Paste the key on the app's Connections page in the dashboard (Claude's add_connection gives you a link to it), then call the API through Boxfort:

const res = await box.fetch("openai", "/v1/chat/completions", {
  method: "POST",
  body: { model: "gpt-4.1-mini", messages, stream: true },
});
for await (const e of res.events()) { /* streamed as it arrives */ }

Boxfort adds the key on the way out and only ever sends it to that API's own address. Only visitors with an account (guests included) can make calls.

Limits you set

Each Connection has a Limits page, so a busy app or one eager visitor can't run up your bill:

SettingWhat it doesDefault
Calls per visitor per hourEach visitor's calls in one clock hour30 for AI, 5 for email, 60 for custom
Calls per monthAll visitors together, reset on the 1st (UTC)3,000 for AI, 300 for email, 10,000 for custom
Max tokens per callA call asking for more is held to this2,048 for AI
Model, or From for emailEvery call uses it, whatever the app asks forThe app chooses
Other fixed fieldsAny top-level body fields to set on every call, as JSONNone

When a limit is reached, box.fetch throws an error with a code (visitor_limit or monthly_limit), a message you can show, and retryAfter in seconds. The API's own errors come back as a normal response. Outside Boxfort, box.fetch throws not_on_boxfort, since your keys only live here.

Converting an existing app

Most small apps made with Claude need only a few changes. Claude does these for you when you use the connector; if you're converting by hand:

  • localStorage or IndexedDB. These still work on Boxfort, but the data stays in one browser. To share it or keep it across devices, save each item with box.collection(...).add() instead, and read with list().
  • Supabase. Keep your supabase.from(...) code and load Boxfort's stand-in client instead: import { createClient } from "/_box/supabase.js" (links to supabase-js on a CDN are swapped for it automatically when you deploy). Each table becomes a collection; list shared tables in box.json. Filters, ordering, insert, update, upsert, delete, single() and realtime postgres_changes work. Joins inside select(), rpc(), Storage and edge functions don't, and sign-in is Boxfort's own: auth.getUser() returns the visitor, and sign-in calls open Boxfort's account sheet.
  • Firebase. Replace the calls with box.collection and remove the client library. Who-sees-what moves into box.json.
  • API keys in the code. Everything in an app is public, so uploads with a real key (OpenAI, Anthropic, Stripe, AWS, GitHub and others) are stopped. Remove the key, add it as a Connection, and call the API with box.fetch.
  • Server code (API routes, PHP, Python, cron jobs). Boxfort doesn't run it. Move what you can into the browser and box.collection.
  • CDN scripts and fonts. See CDNs.
  • A build step. Run the build on your computer and upload the output folder, or ask Claude for a version with no build step.

Visitors and guest accounts

The first time someone opens an app, Boxfort shows a label with the app's name, its description and your handle, plus a note that apps are made by individuals. The app can't change or skip that label. Link previews and search engines see the app's own page.

"Continue as guest" gives the visitor a guest account for that app, kept in a cookie in their browser. No password, no email. Each app has its own guests and its own data, so one app can't see another's.

From the account button, a guest can add their email with a 6-digit code. Their account and data stay the same, and they can open it on another device by signing in with the same email. "Continue with email" on the label does the same for a new visitor.

Who counts toward the cap

  • A guest counts toward the app's visitor cap while they're active. When the cap is reached, new visitors see that the app is full.
  • A guest who hasn't been back within the app's guest window (30 days unless you change it) stops counting, which frees their seat. Their data stays. If they come back they get everything back, and they're counted again as long as the app has room.
  • Banned visitors never count.

How long data is kept

  • Guest data is kept for at most 90 days of inactivity. A guest who hasn't opened the app for 90 days is deleted along with everything they created in it, including documents they added to shared collections. This limit is the same for every app and can't be changed.
  • Claimed accounts are permanent. A visitor who claims their account with an email address keeps it, and their data, until the app's creator deletes them from the dashboard, which also deletes everything they created in the app. Claimed accounts always count toward the cap.
  • Visitors can delete their own data. "Delete my data" in the account sheet removes the visitor and everything they created in the app straight away.

Upload checks and review

Every upload, from Claude, the dashboard or the API, is checked before anything goes live. Keys are always masked in the results.

ResultWhenWhat happens
StoppedAPI keys or private keys in the files; key or password files; server scripts or programsNothing goes live. You get the list of what to fix.
Held for reviewA password or card form together with signs of phishing (another company's name or look-alike, "your account has been suspended" wording, sending data to a chat bot, redirecting to a real sign-in page), or a page asking for a wallet recovery phrase. Also any upload whose address, or a site it links to, is on Google Safe Browsing's list of phishing or malware sitesThe current version stays live while a person reviews the upload.
WarningOne of those signs on its ownGoes live. The warnings are shown and kept with the version.

When an upload is held

A held upload is kept but not served. The Boxfort team looks at it and either approves it, which makes it live like any other version, or rejects it with a note, which deletes its files. Either way you get an email. Your app's page on the dashboard shows held uploads and the reviewer's note, and the API lists them at GET /api/v1/apps/:name/holds. Uploading a newer version replaces a held one that's still waiting.

Honest apps with a sign-in form rarely get held. If yours is, write to us.

Plans and limits

Everyone is on Free during the alpha. Paid plans open with the public beta, and every plan has hard caps, so there's never a surprise bill.

LimitFreeMidPro
Apps31050
Active visitors per app25100500
Data stored, all apps together50 MB500 MB5 GB
Upload size25 MB50 MB50 MB
Requests a month1M3M15M
Image resizes a month (images)5008002,000

Every upload is also limited to 2,000 files. When an app's data is full, adding or changing documents fails until something is deleted; deleting always works.

Deploy API and Claude Code

To deploy from a script, create a token under API tokens on the dashboard, zip your built folder and post it. The first deploy of a name creates the app.

# PowerShell
Compress-Archive -Path dist\* -DestinationPath app.zip -Force
Invoke-RestMethod -Method Post -Uri https://dashboard.boxfort.app/api/v1/apps/todo/deploys `
  -Headers @{ Authorization = "Bearer $env:BOXFORT_TOKEN" } -ContentType application/zip -InFile app.zip

# macOS / Linux
(cd dist && zip -r ../app.zip .)
curl -X POST https://dashboard.boxfort.app/api/v1/apps/todo/deploys \
  -H "Authorization: Bearer $BOXFORT_TOKEN" -H "Content-Type: application/zip" --data-binary @app.zip
RequestDoes
GET /api/v1/appsYour apps and their links
POST /api/v1/apps/:name/deploysUpload a zip and make it live after the checks. A stopped upload returns 422 with the list; a held one returns 422 with hold: { id, status }
GET /api/v1/apps/:name/deploysVersion history
POST /api/v1/apps/:name/rollbackMake an earlier version live: {"version": "v..."}
GET /api/v1/apps/:name/holdsHeld uploads with their review status and note
GET /api/v1/meWho the token belongs to
DELETE /api/v1/meRevoke the token used for the request

Claude Code and other tools that run on your computer can use the local Boxfort MCP instead of the connector. It checks a project folder, runs its build, and uploads the output, signing in through a link you approve on the dashboard. Install steps will be here once it's published.

Help

Questions, a problem with your app, or something you'd like to see? Email hello@boxfort.app. To report an app that looks unsafe, use the report link at the bottom of its label, or email us.