Skip to content

Ship an App

API

Build your App’s front-end in the AI tool you already use: Claude Code, Cursor, Lovable, Replit, Bolt or your own editor. work.studio is everything behind it: sign-in, data, agents, automations, connectors, plans and billing. It also hosts the built front-end at https://<address>.workstudio.run, inside your own organization and environment.

The work looks like this:

  1. Start from a template, or from a project you already have.
  2. Connect your AI tool to work.studio, so it builds against your App’s real setup.
  3. Deploy. Every upload is checked and gets its own preview address.
  4. Make it live, and roll back if you need to.

An App on workstudio.run has two parts:

  • A static front-end: the folder your build makes, with HTML, JavaScript, CSS, images, fonts and other web files.
  • App functions (optional): your own server code, served at /api/* on the same address. Use them for what must not run in the browser: a call with a secret API key, results only paying people get, a check people must not be able to skip. Every organization that can host an App can use them. See App functions.

Everything else goes through the App SDK to the platform: people sign in on work.studio’s hosted page, their records live in the App’s data collections, and its agents, plans and connectors run on work.studio.

  • A work.studio organization that has been approved, whose owner has verified their email. Hosting is only for these organizations.
  • Node.js on your machine, if you deploy with the command line.
  • For the command line, an organization API key. See API keys.

1. Start from a template or your own project

Section titled “1. Start from a template or your own project”

The App Kit makes a new project with the App’s front-end, its workstudio.app.yaml and instructions for AI tools.

Terminal window
npm create workstudio-app@latest corner-bakery
# or a members-only tool:
npm create workstudio-app@latest ops-desk -- --template internal-tool
# or the smallest App, sign-in and one collection:
npm create workstudio-app@latest pocket-notes -- --template blank
# or sign-in, notes, photos and an AI assistant, with no payments to set up:
npm create workstudio-app@latest my-showcase -- --template showcase
cd corner-bakery
npm install
export WORKSTUDIO_API_KEY=svx_tk_… # your organization API key (or: npx workstudio login, once)
npx workstudio apps apply # makes the App on work.studio, claims corner-bakery.workstudio.run, writes .env.local
npm run dev
Template What you get
starter (the default) People sign in, keep their own records, pay for a plan, share by link, and talk to the App’s assistant
internal-tool Members only: only your organization’s members sign in. A team table over a data collection, and an assistant that knows it
blank People sign in and keep their own notes, in one notes collection. No AI, no payments, no functions, so there’s nothing to delete before you build
showcase People sign in, keep notes, upload photos, and chat with a small assistant (Claude Haiku). No payments, so there’s no Stripe key to set up: each person gets a small monthly AI allowance

The folder name becomes the App’s key, its address and its name. Everything the App is on the platform lives in workstudio.app.yaml: npx workstudio apps plan shows what would change, and npx workstudio apps apply makes it so.

Each template includes AGENTS.md and CLAUDE.md. They tell your AI tool the rules: a static front-end, server code only in functions/, everything else through the SDK, and npx workstudio deploy to ship. They also tell it to deploy a preview early, and where the App’s requests and errors are written down.

A new App also gets a .mcp.json, which connects Claude Code to work.studio’s MCP server for this project (see Connect your AI tool). It holds no key, so you can commit it.

The starter template includes an example function in functions/index.ts, served at /api/pro-tip, and the page code that calls it with the person’s sign-in (src/api.ts). If your organization can’t use App functions yet, deploy stops with FUNCTIONS_NOT_ENABLED: set functions: false under hosting in workstudio.app.yaml, or delete the functions/ folder.

Each App has one address, https://<address>.workstudio.run. It’s first come, first served, and stays yours for as long as the App exists.

  • The command line claims it on npx workstudio apps apply, from hosting.slug.
  • In work.studio, open the App’s Ship › Hosting, type the Address and select Claim address.

An address is 3 to 40 lowercase letters, digits and hyphens, starting and ending with a letter or digit, with no --. Platform words (such as www, api, admin, login) and well-known brand names are reserved. Once a deployment has gone live, the address is fixed; a new address means new hostnames.

Every hostname of the site is added to your App’s web sign-in addresses automatically, so sign-in works on it without more setup.

work.studio’s MCP server lets your AI tool read how your App is really set up: its data collections and fields, its plans’ feature keys, its agents’ ids, who can sign in, and its hosting. With it, the tool writes code that matches the platform instead of guessing.

An App made with npm create workstudio-app already has a .mcp.json that names the server. Open the project in Claude Code, approve the workstudio server when it asks, then run /mcp and sign in to work.studio.

For every project instead, add it once:

Terminal window
claude mcp add --scope user --transport http --client-id mcp-public workstudio https://api.work.studio/mcp/v1

The first time Claude Code uses it, it asks you to sign in to work.studio. You can also run /mcp in Claude Code to sign in.

If you belong to more than one environment, my.studio’s Settings › Connect an agent gives the exact command for the one you’re in. See MCP.

The tool works as you, with exactly the access you have. These are the App tools:

Tool What it does
workstudio_list_apps Lists your organization’s Apps, to find the App and its id
workstudio_get_app The App’s setup: sign-in, data collections and fields, plans and feature keys, agents, hosting
workstudio_app_build_brief The same setup written as instructions for building the front-end
workstudio_hosting_status The address, what is live in each environment, recent deployments and their findings
workstudio_hosting_promote Makes a checked deployment live
workstudio_hosting_rollback Makes an earlier deployment live again

Uploads always go through the command line or the upload on Ship › Hosting, not through the MCP server. A tool connected with an API key needs the key’s own permission to change automations to make a deployment live or roll back.

Terminal window
npx workstudio deploy

deploy runs your build, packs the built folder, checks it, uploads it, and prints what the checks found and a preview address. It refuses before uploading when the platform would refuse the bundle, and when a built file holds what looks like a secret key, such as a Stripe secret key, a work.studio API key, an AWS key or a private key. It names the file and the kind of key, never the key itself.

Without --promote, a deployment is only a preview. A preview address is a public link: anyone who has it can open the App and call its functions. Preview functions don’t receive the App’s secrets; a secret reads as undefined there, and only the live deployment’s functions get them. Deploy early all the same, as soon as the App builds, and again after each change. Checking the App on its preview address finds what local testing can’t, such as the browser rules below.

Option What it does
--promote (or --prod) Makes the deployment live once it passes the checks
--message "fix header" A note you see in the list of deployments
--no-build Skips the build and uploads the folder as it is
--dir build Uploads another folder than hosting.dir
--env <name> Deploys to another environment of the App

The command exits with 0 when the deployment is ready or live, 2 when it’s rejected, and 3 when it’s held for review, so you can use it in a script.

deploy warns when .env.local is missing or was written for another environment: the build bakes in the App ids that npx workstudio apps apply writes there. Run apply before your first deploy and again after every change to the manifest, also once the App is deployed: it changes only what differs.

Every upload is checked before anything is served. The result is one of:

Result What it means
Ready Passed the checks. Open its preview, then make it live
Rejected Something under BLOCK must be fixed. Nothing is served from it
Held for review Something under HOLD looks like a phishing or credential page. A person at work.studio reviews it. It isn’t served, not even as a preview, until it’s released

See Checks on every upload for what each finding means.

Each deployment gets its own preview address, https://<address>--d<last 8 characters of its id>.workstudio.run. Open it and check the App before you make it live.

Make it live:

  • In work.studio: on Ship › Hosting, select Make live on the deployment.
  • From the command line: npx workstudio hosting promote <id>. The whole id always works; deploy prints it. npx workstudio hosting deployments also shows a short id for each. Or deploy with --promote in one step.
  • From your AI tool: ask it to make the deployment live (workstudio_hosting_promote).

The deployment that was live before stays in the list. It no longer has a preview address, and its functions no longer receive the App’s secrets. Rolling back to it gives both back.

Roll back:

  • In work.studio: select Roll back to this on an earlier deployment.
  • From the command line: npx workstudio hosting rollback <id>.
  • From your AI tool: workstudio_hosting_rollback.

npx workstudio hosting status shows the address, what’s live in each environment and today’s use. npx workstudio hosting deployments lists deployments, newest first. work.studio keeps the last 10 deployments of each environment, plus whatever is live.

The environment where you claimed the address is served at https://<address>.workstudio.run. Each other environment of the App has its own address, https://<address>--<environment>.workstudio.run, and its own live deployment. Deploy to one with --env <name>. See Environments.

On Ship › Hosting, select Take offline, or run npx workstudio hosting delete --yes. Every address of the App stops answering and the address is released for anyone to claim. The App itself, its data and its sign-in stay.

Terminal window
npx workstudio apps delete # asks you to type the App's address first
npx workstudio apps delete --yes # without asking, for scripts

This deletes, in the environment you run it for:

  1. The App’s site. Every address stops answering and is released.
  2. The App, with every version. Its people lose their installs.
  3. The agents npx workstudio apps apply made for it. An agent the App bundles from elsewhere stays.

It can’t be undone. work.studio refuses while businesses still run the App: they must remove it first. The App’s people keep their sign-in accounts, because those aren’t yours. To delete a person’s data too, see Delete one of the App’s people. The next npx workstudio apps apply in the folder makes a new App.

Terminal window
npx workstudio apps users list # account id, status and install of each person
npx workstudio apps users delete 6f1c2a9e-… # asks you to type the id first; --yes for scripts

It needs the connection:delete permission in the App’s environment. Outside the App’s folder, name the App with --app <address>. What is erased, and how it matches the person deleting their own account, is in Delete one of your App’s people.

An App function is your own server code, served at /api/* on the App’s address. The rest of the site stays static.

Put it in functions/index.ts (or .js, .mjs) next to workstudio.app.yaml. It default-exports an object with a fetch handler:

functions/index.ts
export default {
async fetch(request: Request, env: Record<string, string>): Promise<Response> {
const user = request.headers.get('X-WS-User'); // absent when nobody is signed in
const features = (request.headers.get('X-WS-Features') ?? '').split(',');
if (!user) return Response.json({ error: 'Sign in first' }, { status: 401 });
if (!features.includes('pro')) return Response.json({ error: 'Pro only' }, { status: 403 });
const res = await fetch('https://api.example.com/v1/answer', {
headers: { Authorization: `Bearer ${env.EXAMPLE_API_KEY}` }, // a function secret
});
return Response.json(await res.json());
},
};

npx workstudio deploy bundles the file and everything it imports into one module and sends it with the static files. Functions are deployed only from the command line. They have a preview, go live and roll back together with the deployment they belong to. On a preview, functions run without the App’s secrets, so make them answer sensibly when one is missing. To use another folder, set hosting.functions; for a static-only App with a functions/ folder you don’t want deployed, set functions: false.

Functions run on a web-standard runtime (Request, Response, fetch). Node built-ins such as fs and net aren’t available.

Before your function runs, work.studio checks the person’s sign-in and tells your code who they are in these headers:

Header What it holds
X-WS-User The signed-in person’s id. Absent when nobody is signed in
X-WS-App Your App’s id
X-WS-Plan The person’s plan
X-WS-Features What their plan unlocks, comma separated
X-WS-Email-Verified true or false

Any X-WS- header a caller sends is removed first, so a function can trust these. Never check a token in your function yourself.

The headers are filled in only when the page sends the person’s token. The starter template’s api() helper does that:

// src/api.ts in the starter
const token = (await app.auth.signedIn()) ? await app.auth.token() : null;
const res = await fetch('/api/pro-tip', {
headers: token ? { Authorization: `Bearer ${token}` } : {},
});

A function reads secrets, such as a third-party API key, from env. Secrets are kept per App and environment, and can’t be read back once saved.

  • From the command line: npx workstudio secrets set EXAMPLE_API_KEY reads the value from what you type or pipe in. npx workstudio secrets list shows names only; npx workstudio secrets delete EXAMPLE_API_KEY removes one.
  • In work.studio: on Ship › Hosting, under Function secrets, type the Name and Value and select Add.

Names use capital letters, digits and underscores, starting with a letter, such as STRIPE_KEY. WS_APP_ID, WS_ENV and WS_SITE are set by work.studio. A changed secret reaches the live functions straight away. Only the live deployment’s functions receive secrets; previews don’t. Never put a secret in the front-end: everything in it is public.

Every fetch a function makes passes a filter first:

  • https:// on the standard port only, with no user name or password in the URL;
  • no IP addresses, and no private, local or cloud metadata addresses;
  • never another App on workstudio.run;
  • of work.studio’s own addresses, only https://api.work.studio;
  • a short list of known abuse targets is refused.

A blocked call gets a 403 response. Every call’s data counts toward the App’s monthly data limit.

What Limit
CPU time 50 ms per request
Calls to other services 20 per request, and 1,000 a minute per site (then 429)
Calls to your functions 600 a minute per site, across all of its addresses (then 429 with Retry-After)
Module size 1 MB, bundled
Secrets 50 per App

Each function call counts toward the site’s daily requests. Over either limit, work.studio answers 429 with {"error":"over_quota"} without running the function; over the 600 a minute, the Retry-After header says how many seconds until the next call is accepted, and the refused call doesn’t count toward the daily requests. The 600 is counted over the last minute, not per calendar minute.

An App can be for your own team: an internal tool that only your organization’s members can use.

  1. On the App’s page, open Sign-in › Sign-in page.
  2. Under Who can sign in, choose Only members of your organization (the other choice is Anyone (public)).
  3. Select Save.

With the App Kit, set it in the manifest instead and run npx workstudio apps apply:

signIn:
audience: members # public (the default) or members

A person gets in only when their verified email belongs to a member of your organization. Anyone else is turned away and sees that the App is only for your organization’s members. A members-only App stays Private: it can’t be listed in the marketplace while it’s members-only.

In a members-only App, a data collection can be shared by the team: every member who signs in reads, adds, changes and deletes the same rows. Declare it in workstudio.app.yaml and run npx workstudio apps apply:

data:
- collection: requests
shared: members # leave this out for records each person keeps to themselves
fields:
- { name: title, type: STRING, required: true }
- { name: status, type: STRING }
  • work.studio stamps each row with who added it and who changed it last: createdBy, createdByName, updatedBy and updatedByName on each record the SDK returns. Your App can’t set them.
  • Any member can change or delete any row. When two people save the same row at once, the last save wins.
  • Only a members-only App can share a collection. To open the App to the public, make every shared collection private first.
  • A collection without shared is private: each person sees only their own rows, as in any App.
  • Someone removed from your organization can keep access for up to 15 minutes, until their sign-in expires.

These limits are per organization.

What Limit
Sites (Apps with an address) 3
Storage, all deployments together 200 MB
Deployments 30 a day (UTC)
Requests 200,000 a day, per site
Data served 5 GB a month, per site, including what functions fetch
One upload 19 MB compressed, 100 MB uncompressed, 2,000 files, 25 MB per file
App functions See Function limits

When a site goes over its requests for the day or its data for the month, it shows a “too many requests” page until the period resets. The organization’s owner gets an email at 80% of a limit and again when it’s reached. Ship › Hosting shows the site’s use against its limits under Usage.

AI and data use inside the App are covered by the App’s own budgets and allowances, as for any App. Hosting adds nothing to them.

Every page is served with a fixed set of security headers. They shape what your front-end can do:

  • Scripts must be files in your build. Inline <script> blocks and scripts from other sites don’t run.
  • Calls (fetch, WebSockets) can go to your own address and to work.studio. To call another service, add its origin to hosting.connectOrigins in the manifest, such as https://api.example.com, and run npx workstudio apps apply. Up to 20 origins.
  • Fonts and stylesheets must be in your build. Images can come from any https:// address.
  • Camera, microphone, location and payment APIs are turned off. Take payments through your App’s plans.
  • Framing: other sites can’t show your App in a frame.
  • No cookies. The SDK keeps the person’s sign-in in the page, not in cookies.

Every file is checked on every upload. Each finding has a level:

Level What happens What is found
BLOCK The deployment is rejected Secret files (such as .env, .npmrc, key files, .git folders, database dumps); server code among the static files (.php, .py, .rb, .jsp, .aspx, .cgi, .sh, .ps1, .bat); executables, installers and archives; links; unsafe paths; no index.html; over a limit; a functions module that isn’t one ES module with a default export, or that imports code it doesn’t bundle
HOLD Held for a person at work.studio to review. Not served, not even as a preview, until released Look-alike sign-in pages: a password field whose form posts to another site, or that sits near brand names; forms asking for a seed phrase, private key or “verify your account”; a page that only redirects to another site; heavily obfuscated script, in pages or in functions
WARN Served as usual Source maps (anyone can read them), very large images, unusual file types

Your App never needs a password field: people sign in on work.studio’s hosted page through the SDK.

Sites on workstudio.run must follow the work.studio terms. work.studio can suspend a site that breaks them; a suspended site shows a page saying so, and nothing can be deployed to it. To report abuse on a workstudio.run site, email abuse@workstudio.run or use the contact form and choose Report abuse on a workstudio.run site. Include the full address.

Each entry starts with what you see. Messages in quotes are the text the command line or work.studio shows. For errors from sign-in and data calls in the App itself, see Troubleshoot App sign-in and data. The requests behind them are on App HTTP requests.

“Not uploaded - the built files hold what looks like a secret”

Section titled ““Not uploaded - the built files hold what looks like a secret””

A key ended up in the built front-end, usually through a VITE_* variable or a key pasted into the code. Everything in the site is public. Take the key out of the front-end: put one your App needs in a function secret (npx workstudio secrets set NAME) or in workstudio.app.yaml as ${env:NAME}. Then rotate the key, because it may already be in a build someone has. In functions/ the same finding is only a warning, but the key still belongs in a function secret.

FUNCTIONS_NOT_ENABLED: “App functions are not enabled for this organization yet”

Section titled “FUNCTIONS_NOT_ENABLED: “App functions are not enabled for this organization yet””

App functions are turned off for your organization. Deploy without them: set functions: false under hosting in workstudio.app.yaml, or delete the functions/ folder. npx workstudio secrets is refused the same way, because secrets are only for functions. If you expected functions to work, contact us.

Your site’s functions were called more than 600 times in the last minute, or the site used up its requests for the day. Wait the number of seconds in the Retry-After header and try again; with no Retry-After, the daily limit is reached and resets at 00:00 UTC. If your own front-end calls a function in a loop, slow it down.

The page called the function without the person’s token. Send it as Authorization: Bearer <token> (the starter template’s api() helper does). Signed-out calls reach the function without the identity headers.

The call was refused by the outbound filter: it isn’t https://, goes to an IP address or a private address, goes to another workstudio.run App, or goes to a blocked service. See Calls to other services.

FUNCTIONS_IMPORT or FUNCTIONS_REMOTE_IMPORT

Section titled “FUNCTIONS_IMPORT or FUNCTIONS_REMOTE_IMPORT”

The functions module imports code it doesn’t contain. Deploy with npx workstudio deploy, which bundles everything into one module, and don’t import code from a URL.

FUNCTIONS_NOT_MODULE: “must be an ES module with a default export”

Section titled “FUNCTIONS_NOT_MODULE: “must be an ES module with a default export””

functions/index.ts needs export default { fetch(request, env, ctx) { ... } }.

SECRET_FILE: “This looks like an environment file, which would be public on the site”

Section titled “SECRET_FILE: “This looks like an environment file, which would be public on the site””

A file that holds secrets, such as a .env file, a key file or a .git folder, is in the folder you uploaded. Remove it from the build output. Never put secrets in the front-end: everything in it is public.

The static files contain server code, an executable or an archive. Static files are web files only. Remove the file; server code belongs in an App function, an automation or an agent.

The folder you uploaded has no index.html at its top. Upload the built folder (dist, build or out), not the project. A single top-level folder inside a .zip is fine.

The checks found something that looks like a sign-in or credential page. Nothing is served until a person at work.studio releases it. If it’s a page of your own, remove the password field and use the SDK’s sign-in (app.auth.signIn()). Once released, make it live with Make live or npx workstudio hosting promote <id>.

A blank page, and a Content Security Policy error in the browser console

Section titled “A blank page, and a Content Security Policy error in the browser console”

A script, stylesheet or font comes from another site, or the page has an inline <script>. Bundle them into your build. For calls to another service, add its origin to hosting.connectOrigins.

Turn on the single-page app setting: spa: true under hosting (the default), or Single-page app: send addresses that are not files to index.html when you claim the address.

“…is taken by another App” or “…is reserved”

Section titled ““…is taken by another App” or “…is reserved””

Choose another hosting.slug (or Address). Reserved words include platform words and well-known brands.

Windows: “An Application Control policy has blocked this file” during the build

Section titled “Windows: “An Application Control policy has blocked this file” during the build”

Apps made with create-workstudio-app 0.6.0 or later don’t hit this: their package.json already overrides rollup with @rollup/wasm-node, so no native rollup file is installed. The rest of this section is for older Apps.

Windows Smart App Control, or another application control policy on your machine, blocks a program file it hasn’t seen before. A new release of rollup, the bundler inside Vite, ships such a file, so npm run build fails with ERR_DLOPEN_FAILED and a path ending in rollup.win32-x64-msvc.node.

npx workstudio deploy handles this for you. It says so in one line, then builds once more with @rollup/wasm-node, the WebAssembly build of the same rollup. It’s slower, and the output is the same. Nothing in your project changes.

  • To use it from the start, set WORKSTUDIO_ROLLUP_WASM=1 before npx workstudio deploy.

  • To use it everywhere, npm run build and npm run dev too, add an override to package.json with the rollup version the message names, then run npm install:

    "overrides": { "rollup": "npm:@rollup/wasm-node@4.66.0" }

There is no need to turn Smart App Control off for this.

A refused command says “(request id …)”

Section titled “A refused command says “(request id …)””

Every failed call from npx workstudio ends with the request’s id. When you write to work.studio support about it, include that id: it finds the request in our logs.

“Your organisation is at a hosting limit”

Section titled ““Your organisation is at a hosting limit””

You have 3 sites, 200 MB stored, or 30 deployments today. Take a site you no longer need offline, or deploy again tomorrow (UTC).

“Hosting is for approved organizations whose owner has verified their email”

Section titled ““Hosting is for approved organizations whose owner has verified their email””

Your organization isn’t approved yet, or its owner hasn’t verified their email address.

Sign-in fails on a site you host somewhere else

Section titled “Sign-in fails on a site you host somewhere else”

Only workstudio.run addresses are added to sign-in for you. For your own domain, add it under Sign-in › Where it runs. See App SDK.