Put a thing on the internet.
It'll be lovely.
Two products, one machine. Free puts a file online in seconds — it lives for seven days. Managed builds a durable app with a private database. Pick one; the docs never mix them.
What you can publish
Every content type has a working starter: see the showcase and open one in a click.
Everything is one self-contained file. Paste it in, get a URL back, share it.
Web apps & pages
A single HTML file becomes a live page: a landing, a tool or a game.
Markdown documents
Write in Markdown, ship a typeset page. Tables and code render for you.
Mermaid diagrams
A flowchart or architecture in plain text, published as a zoomable visual.
Data explorers
JSON or CSV in, instant explorer out: search, filter, sort and chart.
Set a password and visitors are asked for it before the page
opens. Set one_time: true and the page deletes itself after the
first visit. Project pages take passwords too — at create, or later from
the page's settings or PUT …/frontends/{target}/password.
Quickstart
Make something
One self-contained file.
Publish it
The machine, one curl, or your agent.
Share the URL
sites.hostan.app/<slug> — works immediately.
Forget about it
Self-deletes after seven days.
curl -X POST https://api.hostan.app/api/v1/deploy \
-H "Content-Type: application/json" \
-d '{"type": "markdown", "content": "# Notes\n\nEverything I remember."}'
# → 201 Created
{ "slug": "notes", "url": "https://sites.hostan.app/notes",
"expires_at": "…seven days later…" }
Connect your agent
Wire Host An App into your assistant once, then just ask it to publish. Three ways in — the MCP server is the one we recommend, because it hands your agent the whole platform as tools. The skill works without MCP; the REST API needs no agent at all.
MCP server
One command adds the server, and your agent gains the publish tools —
plus everything managed: projects, backend, frontends. Any MCP
client can use https://api.hostan.app/mcp as a remote
HTTP server; these are the ones with a command-line add:
# Claude Code
claude mcp add --transport http hostan https://api.hostan.app/mcp
# Codex
codex mcp add hostan --url https://api.hostan.app/mcp
# VS Code
code --add-mcp '{"name":"hostan","type":"http","url":"https://api.hostan.app/mcp"}'
Free quick shares need no credential. For managed
projects, connect over OAuth — the consent page names
the workspaces the client may act in, and it is the only path that
works for consumer clients like ChatGPT connectors, Claude and
Perplexity. Codex authenticates with
codex mcp login hostan; Claude Code and VS Code start
the flow on first use. A headless or CLI setup can paste a workspace
credential instead: Authorization: Bearer hstn_… on the
same server entry. A client with no command-line add (Cursor,
OpenCode) works the same way — add the URL in its config file or
UI.
Agent skill
One file that teaches an agent to publish, for harnesses with no MCP
support. Download it and save it in whichever folder your harness
reads skills from — ~/.agents/skills/hostan/,
~/.claude/skills/, ~/.codex/skills/.
Download
SKILL.md — or read it
first. It ends by pointing at the full contract
(/llms.txt), so it can never drift into a second
source of truth.
REST API or the browser
POST a file's content and get a URL back — no agent, no account — or publish straight from the playground. See the quickstart above.
Limits
Deliberately small: fast to publish, easy to forget.
| Surface | Rate limit |
|---|---|
| POST /api/v1/deploy | 30 req/min per IP, burst 15 |
| POST /mcp | 30 req/min per IP, burst 15 |
| POST /api/v1/auth/<slug> | 10 attempts/min per IP |
| /<slug>/__visit | 60 beacons/min per IP |
| POST /api/v1/reports | 10 reports/min per IP |
| Static content | No rate limit |
Over a limit: 429 with Retry-After: 60 —
back off and retry. Platform full: 503 with a reason. Slugs are
lowercase, hyphenated, 3–63 chars; a few names are reserved.
The model
Managed is one durable envelope: a project holds your pages and a private database, and everything shares its clock. Who gets what:
no token
Managed is self-serve: sign in and a personal workspace is created for you on the spot — free, with 1 project. Hobby (€5/month, tax included) raises the cap to 5 projects. A workspace credential from the operator remains the path for machine callers.
Workspaces & the dashboard
A workspace is the ownership boundary: it holds your projects and bills your plan. A credential authenticates a user, and that user's membership in a workspace carries the role they act with there — one account can belong to several workspaces, each with its own role.
| Role | What it grants |
|---|---|
| owner | Everything: projects, members, billing and deletion. A workspace always keeps at least one. |
| admin | Projects and members below owner — it cannot grant or demote owner. |
| editor | Create and change projects, their frontends and backends. |
| viewer | Read-only. |
Every account gets a personal workspace at sign-in (they own it); owners add teammates there or to a shared workspace. Authority is read live on each request, so removing a member takes effect immediately — no re-login and no token to re-mint.
Plans
A workspace holds exactly one plan, and the plan is what caps its projects — except the free one, whose single project is counted per account, so founding more workspaces does not buy more free projects. Upgrades are a payment, not a setting: a paid plan can never be asserted — it is granted when the payment provider confirms the subscription, and never before. See the full pricing page for what each plan includes; the numbers there are generated from the same catalogue the platform enforces.
| Plan | Projects | Price |
|---|---|---|
| free | 1 per account | — |
| hobby | 5 | €5.00 / month, tax included |
| pro | 25 | €15.00 / month, tax included |
| unlimited | No ceiling | €25.00 / month, tax included |
# owner floor: returns a hosted checkout URL for a human to open
POST /api/v1/workspaces/{id}/billing/checkout → { "url": "https://checkout.stripe.com/…" }
POST /api/v1/workspaces/{id}/billing/checkout { "plan": "pro" } # optional: which tier to buy
# the plan moves when the provider confirms the subscription — not before
GET /api/v1/workspaces/{id} → "plan": "pro"
# asserting a paid plan is refused, on purpose
PATCH /api/v1/workspaces/{id} { "plan": "pro" } → 403, naming the price
# already paying? move the subscription onto another tier (prorated);
# 204 — the change is applied server-side, there is no URL to open
POST /api/v1/workspaces/{id}/billing/change { "plan": "unlimited" }
# the way out: the provider's own portal, where the owner cancels
POST /api/v1/workspaces/{id}/billing/portal → { "url": "https://billing.stripe.com/…" }
# deleting a workspace: the subscription is scheduled to end at the
# period's close first, then every project in it is torn down
DELETE /api/v1/workspaces/{id} → 204
Cancel or let a subscription lapse and the workspace returns to free — your projects are never deleted. The plan is a limit, not a deletion: the workspace just cannot create more projects than the free allowance until it upgrades again. Both ends are dashboard buttons: Upgrade on a free card opens checkout, a paid card offers one button per other tier — which re-prices the existing subscription, prorated — and Manage billing on a paid one opens the provider's portal, where the owner changes the card or cancels. Cancelling starts at the provider on purpose: the subscription ends there, and the workspace returns to free when the provider reports it.
manage_workspace { "action": "list" } # the workspaces you belong to, with your role
manage_workspace { "action": "get", "workspace": "acme" }
manage_workspace { "action": "add_member", "workspace": "acme",
"user_id": "usr_…", "role": "editor" }
manage_workspace { "action": "set_role", "workspace": "acme",
"user_id": "usr_…", "role": "viewer" }
# or over REST: GET /api/v1/workspaces (only your memberships)
Members and roles
Owners and admins invite people into a workspace and set each one's role; the last owner can never be removed, and no one may grant a role above their own.
The dashboard
auth.hostan.app/dashboard is the human console: sign in through the platform's own authorization server, pick a workspace in the switcher to see its projects, then manage plan usage, billing, members and API keys. Teammates are invited by email — search for the address they sign in with, pick the role, and grant; nobody has to copy an account id.
Open a project to see it as a section rail: Overview (its 30-day numbers, a live preview of the published page, recent activity and a getting-started checklist), Pages, Data, API, Payments, History (activity and per-day traffic in 7/30/90-day windows) and Settings — each view holds one thing instead of one long scroll. From the data and API tabs an editor can provision the tier, run migrations, edit the public actions, set secrets and flip realtime without the agent. Lists filter, sort and page through plain URLs, and a secret or a URL is one click away from the clipboard.
On a phone the section rail wraps instead of scrolling, each list keeps its own row control — Open, Remove — in reach and drops one secondary column, and the toolbar folds the search under the brand. A new account gets a three-step getting-started flow (create a project, connect your agent, publish) with the paste-ready MCP command. The console is server-rendered with a session cookie; no token ever reaches the browser.
Agents that connect over OAuth meet a consent page first: it names the client, the requested scope and the workspaces the client could act in. Deny cancels the connection (error=access_denied) and grants nothing. This dashboard is the platform's own client, so its sign-in skips that step; third-party tools still ask. The token identifies the user, so a workspace created later is reachable without re-authorizing, and a revoked membership stops working on the very next request.
Build an app in three calls
Create the project
the envelope
Attach the backend
the database
Publish the pages
wired automatically
# once: connect the hostan server entry
# the client signs in over OAuth (recommended), or
# paste a workspace credential for headless setups:
"hostan": { "url": "https://api.hostan.app/mcp",
"headers": { "Authorization": "Bearer hstn_…" } }
manage_project { "action": "create", "name": "Tasks" }
manage_backend { "action": "create", "project_id": "0193…",
"ddl": "CREATE TABLE todos (id INTEGER PRIMARY KEY, title TEXT NOT NULL);" }
manage_frontend { "action": "create", "project_id": "0193…",
"type": "html", "content": "<h1>Hi</h1>", "requested_slug": "tasks-ui" }
Pages published after the backend exists get the
hostan.db runtime automatically; earlier ones need one
manage_frontend update. Evolve later with
manage_backend update — pages pick up changes on their next call.
For agent-managed edits, use the revision loop:
create_revision publishes a change as an unlisted preview
(your live page is untouched), and promote releases it.
Nothing reaches the canonical URL until you say so, and
rollback undoes a bad release. Rollback reaches the newest
seven revisions; older previews are pruned to keep storage bounded
(their history entries remain, marked expired), so the window is not
unlimited.
To change part of a page without resending it,
search finds a match and reports its line and its
exact byte range; create_revision and update
then take edits ({old,new}, a line range, or
a byte range) instead of full content, and every write
answers with the change as a small diff. On a minified single-line
page every match shares line 1, so use the byte range: a long line
comes back as an excerpt around the match, not the whole
100 KB line.
A preview renders layout and copy and carries
no runtime, so it proves a page renders, not
that it works. To prove it works before promoting, start a
preview run (preview_run, or
POST …/frontends/{slug}/runs): the draft gets a
throwaway SQLite tier replayed from your backend's migration ledger
and a private run URL — open it in a browser and the run collects
console output, uncaught errors, and every
hostan.db/hostan.fetch call with its
outcome, while production data stays untouched. Read the verdict
with mode: result, end the run with
mode: stop; runs expire after 30 minutes. A published
page still has no console, so have your app surface its own errors
into the DOM.
Every content read returns the source's
content_sha256, the same fingerprint a write returns.
Pass it back as known_content_sha256 to make the read
conditional: a match answers unchanged with no body, so
you can confirm a publish without reading the page back.
Your address and your root page
Every project gets a friendly subdomain (botiga.hostan.app) and can name one of its pages as the homepage, served at the bare address — no /landing to remember.
By default the address comes from the project name. Rename it whenever
you like — in the project's Settings tab, or with
manage_project update {host_label} — and the new name is live
at once. A rename moves the address: links to the old name
stop working, so re-share if you have published one. Reserved names and
the platform's own addresses (api,
auth, sites and friends)
can't be claimed.
To make a page the homepage, open the project's Pages tab
and use Set as homepage — or
manage_frontend set_mount {mount: "root"}. It is then served at
botiga.hostan.app while its siblings stay at
botiga.hostan.app/dashboard. Only one page is the
root at a time; naming a new one returns the previous to its
/<slug> address. A root page is still a
single page — use sibling pages for the rest of the site. Root serving
applies on the project's own subdomain; the shared
sites.hostan.app address always uses
/<slug>.
hostan.db — actions, not SQL
Visitors never send SQL and never see a credential. Pages call named actions you define; raw SQL stays owner-only.
const res = await hostan.db.op("add_task", { title });
// res.rows[0].title — rows come back as objects
Tokenless for visitors
No URLs, IDs or keys in the page — connectivity is bound by the project.
Versioned actions
Checksummed, one gated statement each; a call may pass only the
statement's declared binds. Opt in to a declared
param_schema (name, type, required) and calls are
type-checked. Business rules live in the SQL and constraints.
Retire an action you no longer want: the ledger keeps its
history, but pages calling it get a clear 410 Gone —
and defining it again brings it back as a new version.
Owner-only raw SQL
/api/query and /api/schema answer only to your
workspace credential — never to a visitor.
No publish needed to try an action: your agent can
test it directly with manage_backend
test_action — rows for a read, the exact SQL error for a
bad statement, the same strict param contract. A write action
tested this way writes real rows: there is no scratch copy of
your database.
hostan.fetch — server-side egress
Pages cannot call external APIs that need a
secret, or that lack CORS headers. Your backend declares named
fetches — fixed method plus https URL each — and
pages invoke them by name. Secrets live server-side, never in a page.
For public, CORS-friendly endpoints a direct browser
fetch is allowed and is not subject to the caps below;
declared fetches are the only server-side egress, and the only
place a secret may be used.
const res = await hostan.fetch("summarize", { body: { text } });
// res.status — upstream status; res.data — parsed JSON, or text
Named endpoints only
Your agent defines each fetch once (method, URL, headers). Pages never construct URLs — arbitrary egress is impossible.
Secrets stay server-side
Header values reference secrets as ${NAME}. You set
the values in the dashboard (project → Database tab →
Secrets) — they are never shown back, never in a page. Your
agent gets a link to that exact field when one is missing,
and can read readiness back without ever seeing a value.
Guarded egress
Public https hosts only — private and metadata
addresses are refused. 5 s timeout, 256 KB cap, no
redirects, GET/POST only. A
POST fetch always carries a JSON body, so a declared
Content-Type must be a JSON type or absent — declaring
a form type makes the upstream misparse the request.
A page calling a fetch whose secret is not set yet gets a clear error naming the secret — set it in the dashboard and the same page works, no republish needed.
When a call fails, the error tells you why: every
egress failure is a structured 503 whose reason
distinguishes non_2xx (the upstream's own status and a
bounded body excerpt come with it), timeout,
too_large, dns, refused,
tls, or unreachable, alongside
elapsed_ms, bytes_received, and the limits in
force. The rejected hostan.fetch promise carries the same
fields on the error, so page code can branch on the cause.
Fetches can be exercised before publishing, too:
manage_backend test_fetch makes the real call with the
real secrets — never echoed back — and returns the same envelope on
failure, so a broken fetch is named before a visitor ever hits it.
Visitor sign-in
Frontends can sign visitors in with Google or GitHub — run
by the platform, no per-app OAuth setup, no session code. Pages read the
visitor through the injected hostan.auth runtime, and each app
owner picks which methods its frontends offer.
const user = hostan.auth.user(); // null, or { id, email?, name?, picture?, provider?, locale?, expiresAt? }
hostan.auth.login(); // redirect into sign-in, back to this page
hostan.auth.login('github'); // or force a specific method
hostan.auth.providers(); // the app's allowed sign-in methods
await hostan.auth.logout(); // revoke, then reload
| Field | What it is |
|---|---|
| id | Stable platform user id (usr_…) — store it as the ownership column. |
| email · name | Present when Google verified or asserted them. |
| picture | The provider avatar URL — drop it straight into an <img>. |
| provider | The authenticating provider, e.g. google. |
| locale | The visitor's language tag, when the provider asserts one. |
| expiresAt | The session's hard end (RFC3339 UTC); expired sessions resolve back to null. |
Sign-in finishes on the platform's identity origin
(auth.hostan.app) and returns the visitor to the origin
they started from — the app's own subdomain, not a shared one. Sessions are
per-frontend and project-scoped — signing in on one
frontend never signs the visitor in on a sibling. Actions can bind the reserved
:hostan_user_id / :hostan_user_email: the platform injects
the signed-in visitor's identity, so rows are scoped per user with zero session
code.
Real-time rooms
A backend with realtime: true gets one
project-scoped WebSocket relay and an injected hostan.ws runtime:
chat, presence, shared cursors, live dashboards and lightweight games — no
external service.
const room = hostan.ws.connect('game-abc');
room.on('open', async () => reloadStateFromDb());
room.on('message', (env) => { // env = { v, type, data, sender, room, ts }
if (env.type === 'move') renderMove(env.data);
});
room.on('presence', (p) => updateCount(p.members));
room.on('close', () => showReconnecting());
room.send('move', { row: 1, column: 2 });
room.close();
Enable it at create (manage_backend with
realtime: true) or flip it later with a standalone
update {"realtime": true|false}. Frontends published after the
capability was enabled carry the runtime; older pages need one republish.
Rooms are strictly scoped: same backend + same room, never across backends,
and the hostan:* event family belongs to the platform.
The relay is deliberately ephemeral: no history, no replay,
no delivery receipts, no offline inbox. A restart ends all sockets — the
runtime reconnects with capped backoff, and your app reloads authoritative
state from hostan.db on open. Realtime is public
(it is not authentication): keep the source of truth in the database, use
:hostan_user_id actions for identity, and broadcast only
transient state. Limits: 16 KiB frames, ~10 messages/second per socket,
100 sockets and 100 rooms per backend, 20 sockets per visitor IP.
Managed payments
Frontends can collect payments through the platform's managed Stripe Connect integration — hosted checkout, verified webhooks, and the entitlement verdict, all operated by the platform. No per-app keys, no webhook code, no amounts in the browser.
Each workspace connects its own Stripe account once, from the workspace page in the console (Payments section → Connect Stripe). Every project in that workspace charges directly on the connected account: the money never passes through Host An App, and refunds, disputes, and negative balances belong to the connected account. A project whose workspace has not connected an account fails closed (checkout answers 503) — the platform's own account is never a project's merchant.
hostan.payments.checkout('price_…'); // redirect into hosted checkout
const pays = await hostan.payments.status();
// [{ price, status: 'paid', amount, currency, user_id?, email?, matched_by, created_at }]
Define prices with
POST /api/v1/projects/{id}/payments/catalog
({product_name, label, amount, currency, recurring?} — amount in
minor units) or the manage_payments MCP tool
(add_price, catalog, deactivate_price,
sessions). The returned price_… id is the only
thing a page ever handles: the visitor picks a price, never an amount.
A price flagged recurring bills as a monthly subscription;
otherwise it is a one-time payment. The entitlement is granted on the
first verified success webhook either way, and the platform enforces
the lifecycle both ways — see below.
The same catalogue lives in the tenant console: a project's page has a
Payments section listing its prices and its recent
checkouts with their verdicts, where an editor can add a price (integer
minor units, currency, recurring flag) or deactivate one. Nothing there
changes the runtime contract — a page still names a
price_… id and still verifies with
hostan.payments.status() on load.
Checkout creates one hosted Stripe session and redirects the visitor. The
verdict comes only from a signature-verified, deduplicated webhook that
marks the session paid and materializes a row into the project's
database. Reads through status() are authoritative platform
state; the success redirect lands on a confirmation page at
/<slug>/payments/success (canceled when the visitor
abandons) and is UX, not proof.
For signed-in visitors, payments match the platform user. An anonymous
visitor is recognized only through this browser's checkout cookie
(30 days), so a payment made without signing in is invisible from
another browser or device. Each entry carries matched_by
('user_id', 'email' or 'cookie',
strongest wins) so an app can tell a platform-identity match from the
email convenience: the email match lets a later sign-in claim an
anonymous checkout, and because addresses get recycled, two accounts
sharing a provider-verified email both resolve paid on it — require
matched_by === 'user_id' to trust platform identity only.
The lifecycle is enforced both ways: a full refund or a terminated
subscription revokes the entitlement (it disappears from
status()), while a failed renewal is dunning — access
continues while Stripe retries.
The platform also writes a reserved
hostan_payments table into your database for JOINs and
composability — a derived view you can query in your own actions, but treat
status() as the hardened gate. The hostan_ table
prefix is reserved platform-wide: your DDL may not create
hostan_* tables. Where the platform has payments unconfigured,
or the project's workspace has no connected account, checkout answers 503 —
the feature fails closed, it never half-exists. Disconnecting keeps the
price list but closes checkout until an account is connected again;
reconnecting — the same account or a different one — needs no catalog
work, because prices are platform-side rows the checkout line item is
built from on whichever account is connected. Operators configure
Connect once (an application id and the Connect endpoint's webhook
secret); app payments and the platform's own plan billing use separate
webhook endpoints and separate secrets. That webhook secret gates both
directions — while it is unset, deliveries are rejected and
checkout refuses, because a charge nobody could verify into an
entitlement must not be taken.
Limits
| What | Rule |
|---|---|
| Project lifetime | You choose: a TTL or permanent (default). Members inherit — no per-page TTL. |
| Backend size | 5 MB, enforced by SQLite itself — an over-cap write fails atomically with 413. |
| Actions API | 120 req/min per IP (burst 30) + 300 req/min per deployment. |
| Projects per plan | 1 per account on free, 5 on hobby, 25 on pro, no ceiling on unlimited. Creating past the limit is a 403 naming the plan and the console page where you pick a bigger tier. A workspace holds one subscription, so a tier change goes through the billing portal, not a second checkout. |
| Changing plan | A paid tier is granted by a subscription, never asserted (a plan update to one is a 403). Change tier or cancel in the provider's billing portal; the workspace returns to free with its projects intact. |
| Schema changes | Additive only — no DROP, no type changes, no destructive ALTER. |
Full technical reference
Every tool call, endpoint, parameter, schema and error code — for both products — lives in the reference your agents use.
Reliability
99.9% monthly availability target — under 44 minutes of downtime per month. Static-first architecture on Cloudflare and Hetzner; a target, not a contractual guarantee.
FAQ
Seven days, then it removes itself. Managed projects live as long as you chose — permanent by default.
Not for Free: no signup, no API key. Managed is self-serve now: sign in and a personal workspace is created for you on the spot — free, with 1 project.
Yes, set a password when you publish and visitors are asked for it before they can open the page. Durable project pages support the same gate — set it at publish or later in the dashboard; the password covers the page on every origin (the shared one and the project's own subdomain), raw sources and previews included. The runtime API of a project (its declared actions) is not password-gated.
Yes. One-time deployments remove themselves after the first visit — handy for sharing something sensitive once.
Add api.hostan.app/mcp as an MCP server in Claude Code, Cursor
or VS Code, and your agent can publish for you. Agents that don't
speak MCP can install the agent
skill instead.
Head to the interactive reference — endpoints, schemas, the Projects contract and every error code.