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:
- From any Claude chat, including Claude on your phone: add the Boxfort connector once, then ask Claude to put your app on Boxfort.
- By dropping its folder on the Boxfort dashboard. New here? Upload your app signs you up and deploys in one go.
- From a script or Claude Code, with the deploy API or the local Boxfort MCP.
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
- In Claude, open Settings, then Connectors, and choose Add custom connector.
- Name it Boxfort and paste this address:
https://mcp.boxfort.app/mcp
- 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
| Tool | What it does |
|---|---|
get_boxfort_guide | Tells Claude how a Boxfort app is shaped: static files, box.collection, box.json and the limits. |
deploy_app | Takes 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_apps | Your apps, their links, live versions and active visitors. |
get_app_files | The live version's files, so Claude can change an app it made in an earlier chat. |
rollback_app | Makes an earlier version live again (by default, the one before the current one). |
app_status | Checks 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_ticket | Opens 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:
- Drop your app: a built folder, a zip of it, or a single HTML file (served as
index.html). Give it a name. - Confirm your email with the code we send.
- 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,.gitand 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.jsonwith a build step), it tells you the build command to run, such asnpm 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@4instead ofcdn.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 } });
| Call | Does |
|---|---|
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 tocreatedAt.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()
isOwneristruewhen 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 asowner-writein 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,createdAtandupdatedAtare 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" }
}
}
| Access | Who reads | Who changes | Good for |
|---|---|---|---|
private (default) | Only the visitor who made it | Only them | Journals, personal trackers |
shared-read | Everyone in the app | Each visitor edits and deletes their own | Recipe boards, sign-up sheets |
shared | Everyone in the app | Everyone | Shared lists, chore charts |
owner-write | Everyone in the app | Only you, signed in to the app with your Boxfort email | Menus, 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:
| Setting | What it does | Default |
|---|---|---|
| Calls per visitor per hour | Each visitor's calls in one clock hour | 30 for AI, 5 for email, 60 for custom |
| Calls per month | All visitors together, reset on the 1st (UTC) | 3,000 for AI, 300 for email, 10,000 for custom |
| Max tokens per call | A call asking for more is held to this | 2,048 for AI |
| Model, or From for email | Every call uses it, whatever the app asks for | The app chooses |
| Other fixed fields | Any top-level body fields to set on every call, as JSON | None |
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 withlist(). - 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 inbox.json. Filters, ordering,insert,update,upsert,delete,single()and realtimepostgres_changeswork. Joins insideselect(),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.collectionand remove the client library. Who-sees-what moves intobox.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.
| Result | When | What happens |
|---|---|---|
| Stopped | API keys or private keys in the files; key or password files; server scripts or programs | Nothing goes live. You get the list of what to fix. |
| Held for review | A 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 sites | The current version stays live while a person reviews the upload. |
| Warning | One of those signs on its own | Goes 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.
| Limit | Free | Mid | Pro |
|---|---|---|---|
| Apps | 3 | 10 | 50 |
| Active visitors per app | 25 | 100 | 500 |
| Data stored, all apps together | 50 MB | 500 MB | 5 GB |
| Upload size | 25 MB | 50 MB | 50 MB |
| Requests a month | 1M | 3M | 15M |
| Image resizes a month (images) | 500 | 800 | 2,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
| Request | Does |
|---|---|
GET /api/v1/apps | Your apps and their links |
POST /api/v1/apps/:name/deploys | Upload 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/deploys | Version history |
POST /api/v1/apps/:name/rollback | Make an earlier version live: {"version": "v..."} |
GET /api/v1/apps/:name/holds | Held uploads with their review status and note |
GET /api/v1/me | Who the token belongs to |
DELETE /api/v1/me | Revoke 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.