# uarpie — instructions for an AI agent

You are reading this because someone pasted this address into a chat with you. They want their
app online. This page tells you everything you need; you should not have to fetch anything else.

uarpie is hosting for software people build with AI. You run one command; their app is
live at a private address that only the people they invite can open, with a database, file
storage and scheduled jobs set up for it. **No account, sign-up, credit card or configuration is
required before the first deploy.** Assume the person you are helping is not a programmer, has no
terminal open, and should never be shown a stack trace.

## If you have not written the app yet, read this first

Most of what follows is about deploying. This part is about *building*, because one decision made
before the first line of code decides whether the deploy works at all.

**Use Postgres. Read the connection string from `DATABASE_URL`.** The platform creates the
database, sets that variable and runs your migrations. You write no connection details, no
credentials and no host name.

```ts
// Whatever the library — this is all the configuration there is.
const db = new Pool({ connectionString: process.env.DATABASE_URL });
```

The address starts `postgresql://`, which SQLAlchemy reads as it is (write `postgresql+psycopg://`
for psycopg 3). A Spring Boot app needs no database settings at all: it is also given
`SPRING_DATASOURCE_URL`, `_USERNAME` and `_PASSWORD`, and `JDBC_DATABASE_URL`. A Laravel app is
given `DB_CONNECTION` and `DB_URL`. A Rust app's running copy has no Cargo, so give its program a
`migrate` mode (it runs `sqlx::migrate!()` or Diesel's `embed_migrations!`, then exits) and name it
in deploy.json, `"database": { "migrate": "./bin/<name> migrate" }`: the platform runs it once
before each version goes live, after copying the database. Turn the migrator's own lock off
(sqlx: `set_locking(false)`): one deploy migrates at a time, and through the pool below a lock
can be left held. Connections go through a pool in
transaction mode, so nothing a connection remembers between transactions is kept (`SET`,
`pg_advisory_lock`, `LISTEN`, temporary tables): use `SET LOCAL` and `pg_advisory_xact_lock`
inside a transaction.

**Do not reach for SQLite**, even though it is the obvious zero-configuration choice and even for
something small. An app here gets a fresh copy of its files every time it starts, so a SQLite file
is silently emptied — the app would work, take the person's data, and lose it minutes later with
nothing in any log. Deploy refuses it for that reason rather than letting it happen.

**Do not use MySQL or MongoDB.** Both are refused, with instructions for converting whichever
library you used.

That is the whole rule for data. The rest works like this: file uploads go through upload links
(below). A scheduled job is one entry in `deploy.json` — a name, a schedule and a path the
platform calls — never a timer inside the app, which stops between requests. Email goes through
an email service's API with its key as a setting: the platform does not send your app's email.
And the platform signs people in and tells the app who they are (rule 2 below).

**A request to the app may carry about 1 MB, so do not post a photo or a video to it.** Ask the
platform for an upload link instead (`POST /_platform/upload-url` on the app's own address, with
the file's path and size), and have the browser send the file straight there; read it back with
`GET /_platform/file-url`, whose link works for at least half an hour and is the same link for the
same file, so ask for it each time a page shows the file rather than storing it. Anything larger
posted to the app is refused with
`REQUEST_TOO_LARGE` before it arrives. The other way, an app's answer is quick for its first
6 MB and then arrives at about 2 MB a second, so write anything big the app makes (an export, a
report) to storage and hand back a link. An upload library that signs its own link with the app's
storage settings works too, because the browser still sends the file straight to storage: turn on
its direct upload option (Payload's `clientUploads: true` on `s3Storage`, for example).
In a Payload app, run `payload generate:importmap` after changing its storage plugin and deploy
the result: a storage plugin adds a part to the admin, and one missing from the import map leaves
the whole admin page blank.

Those two routes answer somebody the app knows: a visitor the app has signed in (the platform's
own `uarpie_edge` cookie), the owner, or an `api`-kind app's key sent as
`Authorization: Bearer uarpie_k_…`. **A public app's anonymous visitors are not on that list** —
otherwise a stranger could fill the owner's file space — so in a public app, have the app's *own
server* write the file with `UARPIE_STORAGE_BUCKET` and `UARPIE_STORAGE_PREFIX`, or sign people in
first. Those two settings are set only when storage is on for the app. Detection turns it on when
it finds one of a few upload or storage libraries in package.json; whenever the app stores files,
make sure `deploy.json` has `"storage": { "enabled": true }`, and add it if it does not.

**A private website that another server reads.** A private `web` app refuses anything without the
platform's pass, which a server never has, including the person's own public site reading their
private CMS. Create a key for the private app (`create_api_key`) and have that server send it on
every request as `x-uarpie-key: <key>` (keep it there as a setting, e.g. `CMS_KEY`); the app's own
Authorization header is left alone. Files the public site shows visitors from the private app, such
as a Payload CMS's `/api/media/file/`, are loaded by browsers directly, so name them with
`open_paths`: anyone may read those paths (GET and HEAD only) and nothing else changes. A revoked
key stops working within a minute or two, and a new one takes as long to start: wait for it before
reporting that the site can read the private app.

Each plan includes a fixed amount of file space for all of the account's apps together, from 1 GB
free to 100 GB; an app nobody has kept yet has 1 GB of its own. Past it an upload link is refused
with `STORAGE_FULL`, the owner is emailed, and the app keeps running with nothing deleted. Files the app writes for itself through those two settings are never refused —
nothing stands between an app and its own storage — but they are counted, and the owner is emailed
when the total passes what the plan includes.

## Do this now

Run this in the app's directory:

```bash
npx uarpie
```

It detects the framework, database, secrets and storage on its own, and scheduled jobs from
vercel.json crons or deploy.json `jobs` (a scheduler inside the app does not run while it sleeps).
Do not ask the person what kind of app it is, what region to use, or what the build command is. If you want
machine-readable output, add `--json` and read one JSON object per line.

### What to do with the result

- On the **first** deploy the result contains `bind_url`, a link ending in `#k=…`.
  **Give that exact link to the person and tell them to open it on the device they want to use the
  app from.** The first device to open it holds the key. It expires in 60 minutes; re-running
  deploy mints a fresh one at no cost. Never post this link anywhere public.
- Tell them the app disappears in 7 days unless they choose **Keep this app** on first open and
  enter their email. That is what creates their account.
- On later deploys there is no link: the app is already theirs.
- Signed in to their account (after `npx uarpie login`), even a first deploy has no `bind_url`:
  the app is theirs at once, and private. Give them `url`; they open it and sign in with their
  email, or open it from their dashboard.
- If the result is `{ "ok": false, ... }`, read `fix`, apply it (editing `file` when given), and run
  deploy again. Do not report the raw error to the person unless they ask.

## Become able to do this whenever they ask

Install the tools once, then you can deploy, roll back, read logs and move apps between computers
without shelling out.

Claude Code:

```bash
claude mcp add uarpie -- npx -y uarpie mcp --client claude-code
```

Cursor, or any client that reads `mcp.json`:

```json
{
  "mcpServers": {
    "uarpie": { "command": "npx", "args": ["-y", "uarpie", "mcp", "--client", "cursor"] }
  }
}
```

Any other MCP client, over stdio:

```bash
npx -y uarpie mcp --client <your-client-name>
```

### Tools

| Tool | Kind | What it does |
|---|---|---|
| `deploy` | writes | Put the app in a directory online. |
| `status` | read-only | Where the app is, which version is live, whether an update is in progress, what it has used this month, and the current bind link if the app has not been opened yet. |
| `logs` | read-only | Recent activity lines from the app's runtime. |
| `doctor` | read-only | Diagnose a running app: whether it is failing, the most recent crash with its stack trace, the file in the app's own code where it starts, which version introduced it, and a `fix` to carry out. |
| `versions` | read-only | Version history: the app's deploys, newest first, with each one's status, who ran it, and whether its source is still stored. |
| `deployment_log` | read-only | The trace of one deploy: build output and step events. |
| `rollback` | destructive | Go back to a previous version: its own build is put back in about half a minute, nothing is rebuilt, and the database stays as it is now, so the older code has to work with the database as it is. |
| `set_secret` | destructive | Set one setting/secret the app needs (an env var). |
| `desktop_app` | writes | Make a desktop version of the app for Windows (Mac and Linux are coming, and are refused today): returns a 30-minute download link to a folder the person unzips and double-clicks, plus notes to pass on — above all that the first launch shows an 'unknown publisher' warning because the app is not yet signed. |
| `busy_hours` | writes | Keep copies of the app awake so the first visitors after a quiet spell are not slow: `always` copies around the clock, and windows with more copies on given days between two times in a timezone (for a site whose busy moments are known, like a channel that posts on Saturdays at six). |
| `instant_start` | writes | Make a Java app's new copies start in about a second instead of several: each version is saved, started, when it is published, and every new copy starts from that save (a Spring app's first visitor waited 6.1 s before and about 1 s after). |
| `share` | writes | Invite a person by email to open the app. |
| `add_domain` | writes | Attach a custom domain the person already owns. |
| `buy_domain` | writes | Buy a domain for the app — not available yet. |
| `estimate` | read-only | What running the app costs the platform in a month, as an estimate with the breakdown. |
| `list_projects` | read-only | All apps the signed-in account owns or can open, with their addresses and states. |
| `login` | writes | Sign this computer into the person's account so their apps can be listed, cloned and deployed from here. |
| `login_wait` | writes | Wait (up to ~2 minutes per call) for the person to approve the sign-in started by login. |
| `api_keys` | read-only | The keys an app has, without their secrets: an `api` or `mobile` project's client keys, or keys a server uses to call a private website. |
| `create_api_key` | writes | Make a new key. |
| `revoke_api_key` | destructive | Stop a key working. |
| `open_paths` | writes | Set the paths a PRIVATE app lets anyone read without signing in (GET and HEAD only), replacing the list. |
| `estimate_release` | read-only | The price of a phone build that is handed back to the person as files, with a breakdown and a quote_id to pass to `release`. |
| `release` | destructive | Start a phone build priced by estimate_release; the files are handed back to the person. |
| `build_app` | writes | Build the Android app and hand it back to the person as two files: the app itself (apk), which installs on an Android phone when opened there, and the bundle Google Play asks for (artifact, an .aab), with a step-by-step guide (guide_url) for uploading it themselves. |
| `releases` | read-only | Phone builds of an app, newest first: platform, status, what each cost, and download links once a build is `ready` (they work for 30 minutes; call again for fresh ones). |
| `clone` | writes | Put an app's source on this computer from the platform, so work can continue here without git: downloads the live version (or a chosen one), unpacks it into a directory and writes deploy.json. |

Read-only tools are safe to call without asking. Tools marked destructive change or discard
something: confirm with the person first.

## Rules that will save you a failed deploy

1. **Postgres is the platform database** (see the section above if you are still writing the
   app). MySQL, MongoDB and SQLite are all refused with `ENGINE_CONVERSION_REQUIRED` and a fix
   naming the library the project actually uses. Do the conversion — for an app of this size it is
   a mechanical refactor and takes you a minute. Read `DATABASE_URL`; the platform sets it.
2. **Do not add a login system to a private app: the platform signs people in and tells the app
   who they are.** A private app opens only for its owner and the people they invite, who sign in
   by email first. Every request from one of them reaches the app with four headers any language
   can read: `x-uarpie-user-id`, `x-uarpie-user-email`, `x-uarpie-user-name` (percent-encoded:
   decode it) and `x-uarpie-user-role` (`owner` or `member`). The platform removes these from
   anything a visitor sends, so the app can trust them. Browser code can ask
   `GET /_platform/me` on the app's own address instead. A person has them once they have opened
   the app from their dashboard or an invitation; the device that first opened a new app's link,
   before it was kept, has none, so show that visitor the app without a name, never an error.
   A project with its own sign-in gets `AUTH_PROVIDER_AVAILABLE`, a warning: in a private app,
   remove it and read the headers; a public app whose visitors are strangers keeps its own.
   Never name a page that shows who is signed in in `speed.shared`. The visitor's address is
   `X-Forwarded-For`, one address the platform writes on every request and visitors cannot (it
   removes theirs, and `True-Client-IP`, `CF-Connecting-IP`, `X-Client-IP`); the app's own
   server sees every request come from the platform, so a limit per address reads that header
   (Express: `app.set("trust proxy", 1)`, then `req.ip`). The address the visitor opened is
   `X-Forwarded-Host`; `Host` is the app's internal name. An Astro app that runs as a server
   needs `security: { allowedDomains: [{ protocol: "https" }] }` in its astro.config, or it
   refuses every form its own pages send.
3. **Secrets never go in the repository.** Pass them with `deploy -e KEY=value` or the
   `set_secret` tool. They are encrypted on the platform and are not downloaded when cloning.
4. **Outside services depend on whether someone has kept the app.** An app nobody has kept yet can
   call a short list of common services over HTTPS: AI models (OpenAI, Anthropic, Mistral, Groq, DeepSeek, xAI, Cohere, Together, Fireworks, Perplexity, OpenRouter, Replicate, Hugging Face, Voyage, ElevenLabs, Deepgram, AssemblyAI, Stability, Azure OpenAI, Amazon Bedrock, Pinecone); Payments (Stripe, PayPal); App backends (Supabase, Firebase, Upstash); Google (Google APIs, Sign in with Google); Email (Resend, SendGrid, Postmark, Mailgun, Brevo, Amazon SES); Messaging (Twilio); Sign-in and developer APIs (GitHub, Microsoft, Sign in with Apple); Monitoring (Sentry, PostHog); Media and maps (Cloudinary, Mapbox). Once the person keeps the
   app, it can call any service with any protocol, including a database or cache outside the
   platform. Ports 23, 25, 135, 137, 138, 139, 445 and 3389 are closed for every app: send email through a
   provider's API or port 587. If the home page needs a service that is not on the list, the first
   deploy still goes live with `EGRESS_NEEDS_KEEP`: ask the person to keep the app.
   `egress.allow` in `deploy.json` is kept for information and grants nothing.
5. **`deploy.json` is the escape hatch.** It records every inference with the evidence behind it.
   When detection guesses wrong, edit that file rather than passing flags.
6. **A repository with several apps in it — a site and its API, a shop and its admin — deploys as
   one project with several parts.** Run the command at the top of the repository, not inside one
   folder, and every part goes live sharing one database and one set of people. The whole project
   counts as one app on the plan. To put only one of them online, run the command inside its folder.
   Each part has its own visibility, so a public site with a private admin is normal: once the
   person agrees that anyone with the address may open the site, run
   `npx uarpie visibility public --confirm-public -p <id>` at the top of the repository, with the
   site's id from `services` in `deploy.json`. A private part can still let anyone read its
   pictures: `npx uarpie open-paths /media/ -p <id>` with that part's id.
7. **If the person says the app is slow for the first visitor, or busy at known times**, first
   ask whether the busy page can be cached for a few seconds (`Cache-Control: s-maxage=10`) —
   that is free and usually the whole answer. Only then offer `busy_hours`, which keeps copies
   awake and **costs money by existing**: it is paid from the account's Uarpie credit by the hour
   (rule 13), so read the estimate it returns and get a yes first. With no credit it is refused
   with `CREDIT_NEEDED`. For a **Java** app, which is slow to start, offer `instant_start` first:
   every new copy starts in about a second, from a copy saved at each deploy. It also **costs
   money by existing**, sold at cost (about $8.73 a month for a 2,048 MB app) from the same
   credit, so read the price it returns and get a yes first. The two cannot be on together.
8. **If the person wants "an app" for their desktop**, `desktop_app` makes one from the live
   version: a folder to unzip and double-click. Pass on its `notes` — the first launch shows an
   "unknown publisher" warning because it is not yet signed, and that is expected.
9. **If the person wants their phone app on an Android phone or in a store**, `build_app` builds
   and signs the Android app and hands back two files: the app itself (an APK), which installs on
   an Android phone when opened there, and the file Google Play asks for, with a guide page for
   uploading it themselves. On a paid plan the build has no price of its own: it uses about 40 of the plan's build
   minutes, like any build (rule 13), up to 10 builds a month for each account, and needs nothing from them; Google Play itself needs their own developer account ($25, once),
   which only they can create. iPhone builds are not available yet (asking for `ios` says so), and
   the platform uploads nothing to any store. Never say the app has been submitted, and never
   promise a store date.
10. **You will find `/_uarpie/speed.js` in the page source. It is ours, not an intruder.** It makes
   the next page start loading when a link is hovered, so clicks are instant, and it gives every
   `<img>` a `srcset` of resized copies so phones download phone-sized pictures. Pages, files and
   every other answer with a body leave compressed, and returning visitors download far less. The
   page's own large uploads of data to the app's own address (a `fetch` or `XMLHttpRequest` body
   of 8 KB or more: JSON, text, a form without files) are sent gzipped where they can be and reach
   the app exactly as the page wrote them, from the app's next deploy on, so write nothing for it.
   A body your code compresses itself (`Content-Encoding: gzip`, `br`, `zstd` or `deflate`) is
   unpacked for the app the same way. You will see requests under
   `/_uarpie/` and content encodings you may not recognise in the network panel; the page itself is exactly
   what the app sent. Leave all of it. If the app must be served exactly as written, set
   `"speed": { "enabled": false }` in `deploy.json`, but know that it also turns off the
   `x-uarpie-user-*` sign-in headers (rule 2) and translated pages (rule 12), which run in the same
   helper; browser code can still ask `GET /_platform/me` who is signed in. `"images": false` turns
   off just the pictures.
   The person can also switch instant return visits on or off in their dashboard, and their choice
   wins over the file until they hand the decision back, so do not keep re-setting it.
11. **If your app makes each page as it is asked for** — Django, Flask, FastAPI, Laravel, Spring,
   Express, Hono, Fastify, a Rust server — **name the pages that look the same for every visitor**,
   in `deploy.json`: `"speed": { "shared": ["/", "/about", "/blog/*"] }`. Those pages then open
   instantly for anyone who has been before. Without that list the platform leaves them alone,
   because it cannot tell "Our prices" from "Hello, Maya" and will never risk showing one person's
   page to another. A page may say it for itself instead, by answering with `X-Uarpie-Public: 1`.
   Never name a page that shows anything personal: a basket, an account, a dashboard, anything
   behind a sign-in. A page you name is shown again from that person's own device even if your app
   starts a session on it, as Laravel does on every page, so name only pages that show the
   same thing to everyone, signed out or signed in; the copy stays on that one device and is never
   shown to anybody else. A page answering `no-store` is left alone whatever the list says. Next.js, Astro and static sites need
   none of this; their pages are already known.
12. **If the person wants their site in other languages**, add them to `deploy.json` —
   `"languages": { "offer": ["fr", "es"] }`, written the way a browser writes them, up to 30 — and
   deploy again with uarpie 0.3.0 or later (`npx uarpie@latest`). The pages named in
   `speed.shared` are translated as the app deploys, whatever the framework, Next.js included: name
   each one by its address, since a `*` pattern is not translated page by page. Everything else on
   screen is translated while the person looks around their site in that language. Nothing is
   public until they publish it, so tell them: open the app in the dashboard, press "Open it in
   French" (or their language), look around, then press Publish. It needs a paid plan and a kept
   app. Each plan includes some translated pages a month (10 on Personal); a page past that is
   never charged, it waits in the original language until the month starts again. The person can
   also add a language themselves: the app's Settings in the dashboard, "Add a language". One
   added there stays offered when you deploy, even though `deploy.json` does not name it, so
   there is no need to add it to the file. To fix a translation, the person presses "See every
   line" under the language in the dashboard and corrects any line, or presses "Edit wording"
   while reading their site in the language and clicks the words; either way it changes them
   everywhere and costs nothing.
13. **Money: the plan first, then Uarpie credit.** Each plan includes a month of use for all of
   the account's apps together. On a paid plan, use above it ($0.02 a build minute, $0.50 per
   100,000 requests, $0.15 a GB-hour of running time) and busy hours are paid from the account's
   prepaid **Uarpie credit** as they happen, up to a monthly spending limit the owner sets. Nothing
   is ever billed afterwards. Files and translated pages are never paid from credit: past the plan,
   upload links are refused (`STORAGE_FULL`) and translations wait for the next month. With credit,
   the account's apps pause and new builds wait (`USAGE_LIMIT_REACHED`) when the credit runs out or
   the month's extras reach the limit. With no credit, the apps pause when the plan's requests or
   running time are used up, and new builds wait once its build minutes are. Nothing is deleted, and they
   come back on the 1st of next month (UTC), or straight away when the owner adds credit or raises
   the limit. Free has no extras and no busy hours: a paid plan is the way on. Only the owner can
   add credit, in the dashboard under Billing: any whole-dollar amount from $5 to $1,000. It never
   expires, and unused credit they bought is refunded if they ask. You cannot add it: tell them, and
   never ask for card details.

## When the person says their app is broken

Call `doctor` first, not `logs`. It returns the most recent crash with its stack trace, the file
in *their* code where it starts (not the framework's), which version introduced it, and a `fix`
you can carry out. `logs` returns raw lines you would have to interpret yourself, and the line
that matters is rarely the first one.

```bash
npx uarpie doctor --json
```

If `since_version` matches the version that is live, the current deploy introduced the fault and
`rollback` puts the previous build back in about half a minute while you fix it. If it names an
older version, rolling back will not help. When a deploy failed while building or starting,
`npx uarpie build-log` prints what the build said.

## What this hosts

| `kind` | What it is | What deploy returns |
|---|---|---|
| `web` | An app with pages | a private URL plus a bind link |
| `api` | A server with no pages: a backend, a webhook receiver | a base URL plus an API key, shown once |
| `mobile` | An Expo app | an address that says it is a phone app, and the version phones run, made on every deploy; plus an API key for the phone app, shown once |

### Phone apps: read this before you promise anything

A phone app (an Expo app) deploys like anything else, and every deploy makes the version that runs
on phones. Its address only shows a page saying it is a phone app: there is no web version there,
so do not send anyone to it to use the app. **For now, it opens on a phone in Expo Go**, Expo's free app (App Store and Google Play): on the
app's page in their Uarpie dashboard, "Open it on your phone" shows a code they scan with the
phone's camera, and people they invite open it from the app's link there
(`https://uarpie.app/open/<code>`). Expo Go stands in until Uarpie's own phone app is out; do not
promise a date for that. Two limits are Expo Go's own: it opens only apps made with the Expo SDK it
is for (**SDK 57 today**), so make phone apps with that SDK, and only Expo's
built-in native features work in it (no custom native code). On a paid plan, `build_app` also
makes a real Android app they can install (rule 9). You can also host the **server**
behind a phone app: deploy that folder on its own, and it is an `api`.

The platform **does not upload anything to the App Store or Google Play**, and that is not
available. A store listing needs the person's own developer account (Apple $99/year in their own
name; Google $25 with identity verification and, for new accounts, a closed testing period), and
every submission is reviewed by a person at Apple or Google. What the platform can do is build the
Android app and hand the files back (`build_app`, rule 9): the person installs it on their phone,
or uploads it to Google Play themselves. iPhone builds are not available yet.

**So: do not tell anyone their app will be in the App Store or Google Play, and do not promise a
date.** Tell them their phone app is deployed, and that a store listing needs their own developer
account and is a step they take themselves.

`estimate_release` and `release` are two steps so that nobody is charged without agreeing: call
`estimate_release` first, tell the person the exact amount, wait for them to agree, and only then
call `release` passing that quote back. An Android build has no price of its own on a paid plan (it uses
about 40 of the plan's build minutes), and `build_app` takes both steps for it. Never spend someone's money because it seemed helpful.

## Moving to another computer, without git

The platform keeps the exact source of every version for 30 days, and always of the newest five,
so a person with no repository can continue
elsewhere. Two steps:

```bash
npx uarpie login     # prints a code; the person approves it in their browser, once per computer
npx uarpie clone <app-name>
```

Then work in that folder and deploy as normal. Secrets and the database stay on the platform.

## Errors you are most likely to see

| Code | Means | Do this |
|---|---|---|
| `ENGINE_CONVERSION_REQUIRED` | This app uses a database engine the platform does not host. | Postgres is the platform database and every app gets one. Convert the data layer to Postgres (keep the ORM, change the provider/dialect and any engine-specific SQL), then re-run deploy. This includes SQLite: an app's disk does not survive between requests here, so a SQLite file loses what people write. |
| `AUTH_PROVIDER_AVAILABLE` | This app has its own sign-in, but the platform already signs people in to a private app and tells the app who they are. | In a private app, remove the auth library and read the person from the x-uarpie-user-id, x-uarpie-user-email, x-uarpie-user-name and x-uarpie-user-role headers (or GET /_platform/me from browser code); the platform removes those headers from anything a visitor sends. A public app whose visitors are strangers keeps its own sign-in. This is a warning; deploy continues. |
| `MOBILE_STORE_STEP_REQUIRED` | This is a phone app: every deploy makes the version phones run, and its address shows a page saying so. For now it opens on a phone in Expo Go, Expo's free app, and the platform does not publish to the App Store or Google Play. | Tell the person their phone app is deployed. To open it on their phone, they install Expo Go (free, from the App Store or Google Play), then on the app's page in their Uarpie dashboard choose "Open it on your phone" and scan the code with the phone's camera; people they invite open it from the app's link there. Expo Go runs only apps made with the Expo SDK it is for (57 today) and only Expo's built-in features. Do not send anyone to its address to use it: that only says it is a phone app. A store listing is a separate step that needs their own Apple Developer account ($99/year) or Google Play account ($25) and a review by Apple or Google. Do not promise a store release or a date. |
| `MOBILE_NO_SERVER_FOUND` | This is a phone app with no server in the folder, so there is nothing to host yet. | If the app talks to a backend, deploy that folder instead. If it needs one, create it (an Express or Hono server with the API routes the app calls) and deploy that; then point the app at the URL the deploy prints. |
| `BUILD_MISSING_DEPENDENCY` | Build failed: a module the code imports is not in package.json. | Add the missing module to dependencies in package.json, then re-run deploy. |
| `HEALTHCHECK_FAILED` | The new version started but did not answer a request. | Make sure the server listens on process.env.PORT and answers GET / soon after it starts. Read the app's startup output with the logs command or tool, then re-run deploy. If an earlier version was live, it still is. |
| `BIND_TOKEN_CONSUMED` | This app is already in use on another device. | Ask the owner to invite you by email from the app's share screen. |
| `SPEND_CAP_REACHED` | This would cost more than the account's spending limit allows this month, so it was not started. | Nothing was charged and nothing was started. No code change will help. Use above a plan is paid from the account's Uarpie credit, up to the spending limit the owner sets for the month: the owner can raise the limit or top up the credit in the dashboard, under Billing. Tell the person it was refused, and why. |
| `USAGE_LIMIT_REACHED` | This account has used the build minutes its plan includes this month, so new builds wait. | Nothing was built and nothing was charged. Running out of build minutes does not stop apps that are already live. Deploys work again from the first day of next month, or sooner if the owner adds Uarpie credit or moves to a bigger plan in the dashboard. No code change will help. |
| `CREDIT_NEEDED` | This is paid from Uarpie credit, and the account has none left, so it was not started. | Nothing was charged. No code change will help. The owner tops up in the dashboard, under Billing: any amount from $5 to $1,000, and credit never expires. Then ask for it again. |
| `REQUEST_TOO_LARGE` | The request carried more than 1 MB, which is more than an app can be sent in one request. | Upload files straight to the app's storage instead of posting them to the app: ask the platform for an upload link (POST /_platform/upload-url) and send the file there. That route answers somebody the app knows — a signed-in visitor, the owner, or an api-kind app's key — so in a public app the upload has to be made by the app's own server code, using UARPIE_STORAGE_BUCKET and UARPIE_STORAGE_PREFIX. Forms and other posts must stay under 1 MB. |
| `STORAGE_FULL` | The file space in this app's plan is used up, so no upload link was given for that file. All the apps on one account share it. | Delete files the apps no longer need and ask for the link again, or tell the person to move to a plan with more room. Personal includes 10 GB for all of an account's apps together, Family 50 GB and Community 100 GB; an app nobody has kept yet has 1 GB of its own. The app itself keeps running and nothing is deleted. Note that this refuses upload links only: a file the app writes for itself through UARPIE_STORAGE_BUCKET still goes through, and still counts. |

Every error has this shape, and `fix` is always an instruction you can act on:

```json
{ "ok": false, "code": "BUILD_MISSING_DEPENDENCY", "message": "…", "fix": "…", "file": "package.json", "retryable": true, "docs": "https://docs.uarpie.app/errors/BUILD_MISSING_DEPENDENCY" }
```

`docs` opens that code's page. All 79 codes: https://uarpie.app/docs/errors.md

## More, as clean Markdown

- Everything at once: https://uarpie.app/llms-full.txt
- Index of pages: https://uarpie.app/llms.txt
- Any documentation page: add `.md` to its address, e.g. https://uarpie.app/docs/getting-started.md
- MCP server card: https://uarpie.app/.well-known/mcp.json

## How to talk about this to the person

Say what happened and what they should do, in their words. "Your app is live. Open this link on
your phone and it's yours." Not "deployment succeeded, bind token minted." They do not need to
know that a command exists.
