apphaven — guide for AI agents
apphaven hosts web apps for people who aren't developers. You write the app's files through these tools; apphaven gives it a real backend (database, sign-in, access rules) and hosts it.
- Frontend: your code. The usual shape is a Vite project (React, Vue, Svelte, Tailwind…) made installable, so the user has their app on their phone's home screen like any other app (see "Make it installable" below). Plain HTML/CSS/JS pages suit a website: content that loads fast and that search engines read. The two combine: pages next to the app. No server-side rendering, no Node server code.
- Backend: no server code. You describe data, access rules and operations in YAML files in
apphaven/. apphaven turns them into a database and a REST API. - Accounts: every app gets email + password sign-in, address confirmation and password
reset for its users, built in. Email sign-in links are optional (
auth.magic_link). No external auth or database provider setup is needed.
Workflow
app_status(no app given lists them) /create_app— one app per project. The user's apps are private to them. An account holds at most five apps that aren't live yet (never published, or dark since their plan ended);create_appsays so when it's full. Before starting a new app, checkapp_status: if the user's idea fits an app they already have, build there, and if they're at the limit, ask them to delete one they no longer need from the console, or publish one. Rarely, when Apphaven is near its capacity,create_appasks to try again tomorrow; nothing else is affected.get_docs— readconfigbefore writing YAML, andsdkbefore writing frontend data code (andfileswhen the app takes photos or documents from its users).write_files— write the frontend and theapphaven/*.yamlconfig. Send several files per call. For small changes to an existing file,edit_file(exact find-and-replace) instead of resending it.validate— checks config and shows the database changes a deploy would make. Fix every error.deploy_preview— builds, migrates the preview database, and publishes to the preview URL. Read the summary: it lists warnings and database changes.Both also scan the files and refuse to deploy (and going live refuses too) when they find a secret: API keys, tokens, connection strings, private keys,
.envfiles. Everything in the app's files is served to every visitor, so there is nowhere safe for a secret in a frontend; remove it and tell the user to revoke it. (Server-side connectors for things like payments are coming; until then, don't call third-party APIs that need a secret.) The scan also stopshttp://scripts and public access to fields likepasswordorssn, and warns abouteval, HTML built from data (XSS), public personal fields, andupdate: true/delete: true. A documented placeholder can be accepted withscan-ignoreon its line.Test it yourself before handing it over:
test_api(reads) andtest_api_write(POST, PATCH, PUT, DELETE) — call the API as test users (as: "alice",as: "bob") or signed out, to check each access rule from both sides (alice can't read bob's data, bob can't change alice's, a visitor can submit the form, etc.).view_page— screenshot a page (optionally signed in as a test user) with console errors and failed requests. Check the main screens on a phone (device: phone, tablet, desktop;full_page: truefor the whole page). Addstepsto use the page like a person first ({"click_text": "Add task"},{"fill": "#title", "value": "Milk"},{"click": "form button"},{"wait_for": ".task"},{"hover": ".card"},{"press": "Tab"},{"scroll_to": "#footer"}), so forms and buttons get tested, not just first screens. "Are you sure?" boxes are answered OK (dialogs: "dismiss"for Cancel). One page at a time: a second screenshot asked for while one is still opening can be refused; wait for the first, then ask again.query_data/change_data(insert, update, delete),get_logs(recent API errors).- Test ordinary sign-up and address confirmation separately, without
as: test users are already confirmed. Follow "Testing sign-in in preview" inget_docstopicsdk. Fix and redeploy as often as needed, then give the user the preview URL (theurlin the deploy_preview result) to open and try; they come back to the chat with what should change.
Going live is the user's step. The first time, they publish from the dashboard (
publish_urlinapp_status; give them that link), where they choose whether the live app starts with the test version's accounts and data. After that,deploy_livepushes later changes to the live app when the user asks. Changes that would delete live data need the user's approval in the dashboard; tell them so. Publishing needs an active plan or a publishing allowance:app_statusexplains when the app lacks that access. Explain the current availability factually; building and testing remain available without publishing. Billing belongs to the owner in Apphaven. Do not recommend a plan, promote an upgrade or start checkout through the connector.When a piece of work is done,
send_feedbackis a courtesy, never a requirement: a short note to the Apphaven team on what blocked you, broke, confused, would help, or worked well. It shapes what you can do for your users here. Nothing is answered in the conversation.
What tool permissions mean
Hosts may ask for permission according to everything a tool can do, even when this call only takes a screenshot or requests a dry run. Permission to work in preview does not authorize publishing, changing live data, or sending information to someone else.
write_files,edit_fileandrestore_versioncan overwrite or remove workspace files.deploy_previewreplaces the test version and can remove preview data. npm builds run the app's build script and can contact external services; the test version remains private.test_apiuses GET, butascan create/confirm an account and sign it in, and auth links can confirm accounts or consume a sign-in token. It is not a read-only tool.test_api_writecan also overwrite/delete data, upload sample files and trigger preview mail. Only the owner's own address is eligible for delivery from preview.view_pageruns the app and its scripts in a browser. Its actions can change/delete data, upload files, send mail and follow links or contact services outside Apphaven.deploy_livechanges a publicly available app.change_dataandmanage_userscan remove data or accounts in the selected environment; their existing production safeguards apply.send_feedbackshares a note with the Apphaven team outside the owner's workspace. These tools cannot recall it. Never include personal details or the app's data.
The retrieval tools (get_docs, app_status, read_files, query_data, list_users,
get_logs, read_emails) and validate are read-only. create_app adds an app without
replacing an existing one. Tool permission labels do not replace the owner's authorization.
Your own domain
The owner can put the live app on an address they own, like www.theirshop.com. They enter it in
the console's Settings (not you: it's their domain); from then on app_status shows the domain's
state, the one record to add, and the reason when it's stuck, and the owner may paste you a request
to help. Walk them through it:
- Ask where the domain is registered if you don't know (GoDaddy, Namecheap, Squarespace, Cloudflare, Wix, IONOS…). The record goes in that domain's DNS settings there, nowhere else.
- Give the record exactly as
app_statusshows it: a CNAME, namewww.theirshop.com(some providers want onlywww), pointing to<slug>.customers.onapphaven.com. Say which screen it's on at their provider ("DNS", "DNS records", "Manage DNS") and that any existing record forwwwmust go. Nothing else changes; no A record, no nameserver change. If their DNS is at Cloudflare, the record must be DNS only (grey cloud): a proxied record hides the CNAME andapp_statusthen reports an address record instead. - The bare domain (
theirshop.com) can't point here directly, so people who type it need a forwarding rule at the same provider, fromtheirshop.comtohttps://www.theirshop.com(GoDaddy and Squarespace call it Forwarding, Namecheap "Redirect Domain", Cloudflare a Redirect Rule). Suggest it once the www address is live. - Check, don't promise.
app_statusre-checks the record each time you call it (at most once a minute):waiting_dnsmeans the record isn't visible yet (minutes usually, up to a day at some providers; thedetailsays if it points somewhere else),issuingmeans it's in place and the certificate is coming,activemeans done. Then the live address is the domain, and the onapphaven.com address sends people there.
Test version and sign-in
There are three identities to keep separate:
| Identity | What it gives access to |
|---|---|
| The owner's Apphaven account | The dashboard and the private preview. Signing in to Apphaven opens the preview; it does not sign them in inside their app. |
| An account inside the app | The app's features, data and roles. The owner signs up here separately, even if they use the same email address as their Apphaven account. Passwords and sessions are separate. |
A test user (as: "alice") |
An app account the tools create and sign in automatically in preview. Its address is already confirmed, so it checks authenticated screens and access rules, but does not exercise ordinary sign-up or confirmation. |
Preview and production have separate databases and separate user accounts. Preview starts as
a copy of production (change_data with action: reset_preview refreshes it).
Reset prepares a separate copy, removes copied passwords, provider tokens, sessions and verification
tokens, then applies the test version's schema before switching it over. If preparation fails, the
existing test version stays usable with its data unchanged. A message saying the reset was
interrupted means you can retry the same action to finish it safely. Sign in afterward with fresh
test users or new sign-ups; copied production accounts cannot sign in with their live passwords.
Handing the test version to the owner. Give them the preview URL and explain the steps:
open it while signed in to Apphaven, sign up inside the app using their Apphaven account's
email address, open its confirmation email, then return to the app and sign in. Only that
address is eligible for real email delivery from preview; other addresses' mail is captured
for you to inspect with read_emails. Check the mail status if it does not arrive. The owner
chooses their own app password; you do not need it.
The owner is not automatically an app admin. When the app has an admin/organiser role, use
manage_users (action: set_role) on their app account after they sign up, so they can try
their own controls. Do the same on production after publishing (unless you launch with
preview data, below).
Cleaning up before going live. manage_users with action: delete removes leftover test accounts (records that
need them go too: check what it reports). Delete made-up sample records too. When the owner
publishes from the dashboard, they can bring the test version's accounts and data along (same
passwords and roles; @apphaven.test users are left out), so make sure what's there is real.
Once the live app has its own data, the two databases stay separate.
The preview is private
Only the app's owner can open the preview, signed in to apphaven: the preview URL sends anyone
else to a sign-in page, and its API answers 401 preview_locked. So a preview link is for the
owner to look at their own app, not something to hand to testers or members; when other people
should use the app, the owner publishes it. Your test_api and view_page calls get in automatically, and
the test users you create sign in inside the preview as before.
The confirmation and reset links in the preview's sign-in mail work on any device, because the
owner often opens their own mail on a phone that isn't signed in to Apphaven: each carries a pass
for that one link. A confirmation link confirms the address there, and the phone then says to go
back to the test version and sign in. A reset link opens Apphaven's built-in page
(/_apphaven/reset-password, the SDK's default) and the new password is set there; a reset page
of the app's own is an ordinary preview page, so it asks the owner to sign in to Apphaven first.
A sign-in link (magic link), or any other address the phone opens, asks them to sign in to
Apphaven first too.
When sign-in fails or another method is requested
Read the failing endpoint, response code and message before changing anything: preview_locked
is the private preview gate, an unconfirmed address needs its confirmation link, and a model's
forbidden response may be an access rule. A 401 or 403 alone does not identify the cause. The
sdk docs have the troubleshooting steps and a complete sign-up test.
Apps currently support email/password and optional email sign-in links. Google or Apple sign-in
for an app is not provided; signing in to the Apphaven platform with Google is separate.
Explain that capability gap and offer the supported flow. Keep the working Apphaven auth and
data in place when diagnosing preview restrictions. Moving to an external provider is a
separate choice for the owner, with setup and data implications to explain first; placeholder
provider settings do not make a sign-in option work. You can report the missing capability
through send_feedback.
Files
package.json ← a Vite project: apphaven builds it and serves dist/
vite.config.js, index.html, src/...
public/ ← files served as-is next to the app (plain pages, icons, robots.txt)
apphaven/app.yaml ← backend config (all apphaven/*.yaml files are merged)
- Vite/npm: if
package.jsonhas abuildscript, apphaven runsnpm installandnpm run buildand servesdist/(change withfrontend.output_dir). Don't uploadnode_modules.@apphaven/clientis pre-installed. Unchanged frontend inputs can reuse the last successful build for up to an hour, including when onlyapphaven/backend settings change. Editing frontend files (including a dependency lockfile) triggers a new build. An Apphaven platform update also invalidates saved builds. - Plain files, no build: with no
package.json, every file exceptapphaven/and dotfiles is served as-is (index.html,app.js,style.css…). The sdk is at/_apphaven/sdk.js. - Either way,
/aboutservesabout.htmlorabout/index.html. Unknown paths fall back toindex.html(setfrontend.spa: falseto get 404s instead; a404.htmlis used if present).
Make it installable
An app the user opens every day belongs on their phone's home screen. A Vite app becomes one
with vite-plugin-pwa: a manifest (name, colours, icons), a service worker that keeps the app
opening instantly, and nothing else to set up; apphaven serves the manifest and the worker with
the right headers. The recipe is in the examples (get_docs topic examples, "Installable").
Two rules keep it honest:
- Use
registerType: "autoUpdate"and keep/apiand/_apphavenout of the worker's navigation fallback, exactly as the recipe shows. Then every deploy reaches installed phones on their next open, and sign-in and data never go through a cache. - Icons are square PNGs (192 and 512). Ask the user for their logo through the Add files page and make the icons from it, or draw a simple one; a made-up path is a blank icon.
Install from the live app, not the preview (the preview is private and signs the owner in each time). Tell the user how: on an iPhone, open the live address in Safari, Share, "Add to Home Screen"; on Android, the browser offers "Install app" or it's in the menu.
A website and an app together
A business usually wants both: pages that load fast and that search engines read (home, about,
prices), and the app behind a sign-in. Put the pages in public/ as plain HTML (public/about.html
is served at /about) next to the Vite app, which takes / or /app; one project, one deploy,
one domain. Keep the pages' links to the app relative (/app) and the shared look in one CSS file.
Files from the user (logo, photos, documents)
These are the owner's own files for the app (its logo, the menu photos). Files the app's users send it (a profile photo, a receipt, a CV) are image and file fields instead: get_docs "files".
Don't ask the user to paste or attach files in the chat: chat attachments never reach you as bytes,
and typing a picture out as base64 is slow and error-prone. The same goes for photos you find on
the web: fetching a page gives you text, never the image's bytes, so write_files with base64
can't carry them (it's for small files you make yourself, like an SVG icon), and a made-up path
is a broken image. Photos come from the user: send them to the app's
Add files page (files_url in create_app / app_status, which is
<dashboard>/apps/<id>/files?add=files):
I need your logo. Drop it in here: https://…/apps/app_123/files?add=files — I'll wire it in.
What they drop lands in the workspace under uploads/ (they can pick another folder), the test
version is redeployed so the files are served at /uploads/<name> (in a Vite app too: Apphaven
adds the owner's files to the built output at their own path, unless the build already serves
that path; hidden paths are excluded). Whole-project imports are build inputs and do not get
this extra serving step. Photos lose their location data on the way in. Your next tool result,
whatever tool it is, ends with a note listing the new paths and image sizes. read_files shows
them too, marked [added by the user]. Then reference them by path (<img src="/uploads/logo.png">)
and deploy_preview.
For files you make yourself, write_files with encoding: base64 works, and a bad string gets an
error naming the offset. Prefer SVG for icons and artwork: it's text, so nothing can be mistyped.
Apps send email with no setup, from "<App name>" <<slug>@viaapphaven.com> (the test version
adds +preview):
- Sign-in mail is automatic: password reset (
app.auth.requestPasswordReset), sign-in links (auth.magic_link: true, no passwords at all) and address confirmation. Every password sign-up confirms its address before it can sign in:signUpreturns the user but no session, so show "check your email". Test users (@apphaven.test) are confirmed on creation in the preview; the live app refuses them. See the sdk docs. - Operations can email:
email: { to, subject, template | body }on a create/update operation sends once per record, to the owner, the signed-in user, or addresses in the record (customer.email). Templates are HTML files inapphaven/emails/with{{ placeholders }}. These are fully supported; sign-in messages use fixed Apphaven copy with the app's name. The owner's Mail page previews the deployed templates with sample details at phone and desktop sizes, without sending anything. See the config docs. - Preview delivers only to the owner's Apphaven account email address. Other recipients'
mail is
captured, which is expected: it is kept for inspection, not sent to their inboxes. Every message is kept in the app's mail log:read_emailsto check its status, and with anidto read one rendered. Owner delivery can also fail or be limited; check the actual status. Use them the way you useview_page, and open the links they contain (the confirmation link included) withview_pageto test the whole flow. Live apps send for real: up to 500 a day once established (100 a day for the first week, 250 until day 30), with a per-address limit for addresses nobody confirmed; they skip addresses that bounced or unsubscribed, and sending pauses by itself on a spam complaint or too many bounces (read_emailssays so). - Not for marketing: one event, one message, to the people in it. Newsletters and campaigns need the owner's own email service.
The backend in one example
# apphaven/app.yaml
models:
Task:
fields:
title: { type: string, required: true, max_length: 200 }
done: { type: boolean, default: false }
owner: { type: user, default: $user.id, readonly: true, required: true }
access:
read: owner == $user.id
create: $user != null
update: owner == $user.id
delete: owner == $user.id
import { createClient } from "/_apphaven/sdk.js"; // Vite: from "@apphaven/client"
const app = createClient();
await app.auth.signUp({ email, password, name }); // then "check your email": no session until the link is opened
const { items } = await app.db.Task.list({ filter: "done == false", sort: "-created_at" });
await app.db.Task.create({ title: "Buy milk" });
Rules of thumb
- Access is denied unless a rule allows it. Every model needs
accessrules, or the app can't touch it. Think about who may read/create/update/delete each model, and write it down. - For "things a user owns", use the
ownerpattern above (type: user, default: $user.id, readonly: true), so the creator is recorded and can't be faked. - Never put secrets or API keys in frontend files — everything there is public.
- Keep the UI working when signed out: show a sign-in form, and handle 401/403 errors.
- Renaming a field or model? Use
renamed_fromso data is kept. Removing one deletes its data. - Every deploy saves the files as a version:
restore_version(the versions are inapp_status) undoes a bad change (files only — data isn't rolled back). - The user is not technical: explain what you built in plain words, give them the preview URL, and don't ask them to edit code or YAML.