Skip to contentreachpad

Publishing an app

Two ways in. Your coding agent calls the reachpad MCP server and hands over the files, or you upload one file and reachpad publishes it for you. A server app can also store data: it has a database of its own, a place for files, and the secrets your organization has set.

Through your coding agent

Connect the agent to https://reachpad.dev/mcp once, which is one URL and four steps, then ask it to publish. It calls create_app and gets the link back in the answer.

  • name is what your list shows.
  • files is an array of { path, content }, with the content written as text rather than encoded.
  • slug picks the subdomain. Leave it out and it comes from the name.
  • manifest says how the version runs: { "kind": "page", "entry": "index.html" }, or { "kind": "function", "entry": "server.js" } for a server, which can also declare the services and secrets it uses. Omit the manifest for a page, or read the manifest for every field.
  • access sets who can open the link. Leave it out and it is anyone signed in to your organization.

Version 1 is live the moment create_app answers. There is no staging step and no draft, so ask your agent to show you the content and the destination before it calls.

Publishing a change

Have the agent call read_app first. It answers with base_version_id, the version the next edit builds on, and that version's file listing. Pass it to update_app as base_version_id.

If somebody else published in between, update_app refuses with stale_base rather than dropping their version. The move after that refusal is to read the app again and pass the base_version_id it returns.

A publish replaces the whole file set. A file that was in the last version and is not in this call is not in the app any more.

To send only what changed, pass mode: "merge". The new version keeps every file of the version base_version_id names that the call does not name, and remove takes the paths to drop. The 2 MB limit on a publish counts the carried files too.

validate: true on either call answers with the file set it would publish, its size and its entry file, and writes nothing. Use it to find a missing index.html before the link serves a 404 instead of after.

publish: false on update_app builds the version and leaves the link where it is. The staged version has its own URL to look at, and publish_app_version puts it live. The same call takes an older number, which is how an app is rolled back.

New versions go live as they are published, up to 60 an hour for one app.

Uploading a file

Add an app takes one file and publishes it as version 1 of a new app.

  • A Markdown file is rendered to a page, and the Markdown travels with it.
  • An HTML file is served as it is.
  • Any other file gets a page naming it and its size, with the file itself behind that page.

The same screen writes a build prompt you can paste into a coding agent that has your project folder open, for the times when the thing you want published does not exist yet.

The manifest

The manifest is one object on a publish call, and it decides how the version runs. Leave it out and the version is a page.

FieldWhat it is
kindpage serves the files. function runs one module for every request.
entryFor a page, the file served at the root of the link, usually index.html. For a function, the module that exports default { fetch }.
envPlain string values the function reads as env.NAME. They are stored as written and readable by anyone who can read the version, so a key belongs in secrets instead.
servicesWhat the function may use: db, files, or both. A function only. A name outside that pair is refused when you publish rather than at the first call.
secretsThe names of organization secrets to bind, each arriving as env.NAME. A function only.
{ "kind": "function", "entry": "server.js",  "env": { "SITE_NAME": "Notes" },  "services": ["db", "files"],  "secrets": ["STRIPE_KEY"] }

Over MCP the manifest is an argument on the publish call. From the command line it is the same object, kept in reachpad.json beside the source, which is the file the refusals name.

On update_app, leaving the manifest out keeps the previous version's. Sending one replaces the whole of it, so a manifest that omits services takes env.reachpad away from code that still calls it.

A function is handed whoever opened the link. request.reachpad is { user, name, org, role, app, version }, so an app that its readers sign in for can key its rows on user without asking them to sign in twice.

On a public link nobody has to sign in, and an anonymous reader arrives with user and name set to null and role set to anonymous. org is the organization that owns the app either way. Check role before you trust user to be there.

What a function should do in a request

A function answers one request and returns one answer. It gets about 10 seconds of computing per request, and 90 seconds before the link stops waiting. Computing is time your code spends working: waiting on the database, on a file, or on a service you call costs none of the 10 seconds, which is why an app that streams a model's answer for a minute is well inside both.

Almost nothing an app does in a request comes close. A page put together from a few queries is a handful of milliseconds of computing.

Work that runs for minutes belongs somewhere else: a long render, a large export, a job that walks a whole dataset. Hand it to a service built to run it, keep a row for the job in the app's database, answer straight away with its id, and let a later request report on it. An app that tries to finish that work inside the request fails in front of whoever opened the link.

Storing data in a function

env.reachpad is on every function. What services decides is whether a call through it works: a version that has not declared db still has env.reachpad.db.query, and the call fails when it is made. There is nothing to install and no connection string to hold.

const { rows } = await env.reachpad.db.query(  "SELECT id, title FROM notes WHERE owner = ? ORDER BY id DESC LIMIT 50",  [request.reachpad.user],);const id = crypto.randomUUID();await env.reachpad.files.put(id, await request.arrayBuffer(), "image/png");const stored = await env.reachpad.files.get(id);
  • db.query(sql, params) answers { rows, changes, lastInsertRowid }, with each row an object keyed by column name.
  • db.batch([{ sql, params }]) runs 1 to 20 statements in one transaction and answers an array of the same { rows, changes, lastInsertRowid }, one per statement, in the order they were sent. A statement that fails takes the whole batch back with it.
  • A parameter is a string, a number, a boolean, null, or { "$blob": "<base64>" } for bytes. A BLOB column reads back in that same shape, so what you wrote is what you get.
  • Each app has one database, shared by every version of that app. A version staged for review reads and writes the same rows the live one does.
  • files.put(id, body, contentType) takes an id your app picks, a lowercase UUID, and a body that is a string, an ArrayBuffer, a view or a Blob. files.get(id) answers a response you can return as it is, files.head(id) answers { bytes, contentType, etag }, and files.delete(id) removes it. get and head answer null for an id that is not there.
  • There is no call that lists files. Your app chose the ids, so your own table is the index.
  • usage() answers { db_bytes, files, file_bytes }, which is what to read before storing something large.
LimitValue
One app's database10 GB, the storage platform's own ceiling
Rows one query or batch returns5,000, or 4 MB of JSON
Statements in one batch20
One stored file100 MB
Files in one app10,000, and 1 GB in total
Migration files in one version200, each at most 100 KB
Secrets in one organization100

A query that would return more than 5,000 rows or 4 MB is refused rather than truncated: The result is over 5,000 rows or 4 MB. Add a LIMIT.

From the command line, reachpad db "SELECT count(*) FROM notes" runs one statement against the same database, with --params '[...]' for its bound values. It refuses a schema change: those go in a migration file.

Secrets

A secret belongs to your organization, not to one app. Set STRIPE_KEY once and every function app in the organization can bind it by naming it in secrets.

Set one in Settings, or with reachpad secrets set STRIPE_KEY, which reads the value from a hidden prompt or from what you pipe into it and never from the command line. A name is capital letters, digits and underscores, starting with a letter, at most 64 characters, and cannot start with RP_.

Set the secret before you publish the app that names it. A name your organization has not set fails the publish with STRIPE_KEY is not set. Run reachpad secrets set STRIPE_KEY., and a name written into both env and secrets fails with STRIPE_KEY is both an env var and a secret. Keep one.

reachpad secrets list shows the names and which apps bind them, never the values. Nothing reads a value back out, including you. reachpad secrets remove clears the value from the running versions that bind it, within seconds, so removing one takes it away from apps published months ago. It reports any version it could not update, and the name stays listed until they are all done, so the next remove retries exactly those.

Changing the schema

Schema changes live in the app, in migrations/NNNN_name.sql: four or more digits, an underscore, then lowercase letters, digits, hyphens or underscores. migrations/0001_notes.sql and migrations/0002_add_pinned.sql run in that order.

They run when the version goes live, not when it is built. That is the point of the split: an app has one database, so a version staged with publish: false would otherwise rewrite the schema under the version still serving.

  • Only a function that lists db in services may carry the folder. Anything else fails the publish with Migrations need the db service.
  • Never edit a file that has already been applied. Each file's hash is recorded, and a changed one is refused: migrations/0002_x.sql changed after it was applied. Add a new file instead.
  • One file is all or nothing, and a set of them is not. A version carrying three files can stop at the second, and the first one stays applied.
  • A file that fails fails the publish, with the message SQLite gave, and the app's link keeps serving the version it was serving.
  • A file takes no bound values, and 200 files of at most 100 KB each is the ceiling for one version.

The record of what ran is a table in your own database, _rp_migrations. Read it with reachpad db "SELECT name, applied_at, version_id FROM _rp_migrations", fix the file that failed, and publish again: anything already applied is skipped.

The app's own link always serves whichever version is live. Each version also keeps a link of its own that never changes what it serves: versions and their links.

This page as raw markdown, for an agent or a prompt: /docs/publishing.md