Start for free. Sign up without a card and you'll get a test project and a test key right away. Once your company (or, for a personal account, you) is verified, you can create a live key in the portal.
Build a full app
A real app with accounts, data per user, uploads and share links, built by your chat on Hypamoon. What runs here, what does not and what to do instead, and the recipe in eight steps.
Your chat can build a real app on Hypamoon: people sign in, keep their own data, upload photos and voice memos, and share things by link. No server to rent, no database to set up. This page tells you, and your chat, how that works and where the edges are, so you build the real thing from the first step instead of testing whether it works.
The example is a recipe book for a family: recipes with ingredients, photos and voice memos, scaling a recipe to more people, sharing one recipe by link, and signing in per family member.
What runs here, and what to do instead
An app on Hypamoon is a site plus server code on the same address. The pages
are static files. Everything under /api runs your code.
| You had in mind | On Hypamoon |
|---|---|
| PHP, Python, Ruby or another server language | The same logic in JavaScript under /api, deployed with deploy_app. |
Your own server, Express, or Node with npm install |
One JavaScript module with export default { fetch }. Route on the path of the request. |
| React, Vue or TypeScript with a build step | Send the built files (the dist folder), or load React as an ES module from a CDN, no build needed. Server code that uses TypeScript or npm packages is bundled into one .js file first. |
| MySQL or Postgres | The app's own database, created with database: true. Your chat creates and changes the tables with apply_app_migration. You never touch the database by hand. |
| A login system of your own | Sign-in is built in: an email link, roles, open sign-up or invite only. Never check passwords in your own code. |
| Users uploading files to a folder | Uploads go through /app-files from the app's code. Photos (also from an iPhone), PDFs, video and voice memos. |
API keys in a .env file |
request_secret: you paste the key in the portal, the app uses it by name, the chat never sees it. |
| A cron job on a server | Schedules on the app (every 5 minutes or slower) and a queue for slower work. |
Not available: WebAssembly, and anything that needs a long-running process. Each request gets a small amount of CPU time, which is plenty for an app like this.
The recipe in eight steps
Tell your chat what you want, and point it to this page. It does these steps itself, one at a time.
Ask what matters. Your chat calls
get_project, then asks you at most three things: who may sign in (anyone, or only people you invite), which files people upload, and what is public (a shared recipe).Put the app live.
deploy_appwithout a site: a name, the pages (index.html,app.js), one server module,authwithsignup: "invite"and rolesadminandmember, anddatabase: true. No schedule, because a recipe book has no periodic work.Create the tables.
apply_app_migrationwith0001_init.sql:sqlCREATE TABLE recipes ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, title TEXT NOT NULL, servings INTEGER NOT NULL DEFAULT 4, ingredients_json TEXT NOT NULL, steps TEXT, photo_file_id TEXT, memo_file_id TEXT, created_at TEXT NOT NULL ); CREATE INDEX recipes_user ON recipes (user_id, created_at); CREATE TABLE shares ( token TEXT PRIMARY KEY, recipe_id TEXT NOT NULL, title TEXT NOT NULL, servings INTEGER NOT NULL, ingredients_json TEXT NOT NULL, steps TEXT, photo_file_id TEXT, created_at TEXT NOT NULL );Keep recipes private.
set_table_owner_columnforrecipeswith the columnuser_id. In the code,env.DB.mine("recipes")only ever sees the rows of the person who is signed in.Uploads. The page asks
/api/uploadfor a link, sends the file there with aPUT, and stores the file id in the recipe. The server side:Node.jsconst asUser = (request) => ({ "content-type": "application/json", "x-hypamoon-user": request.headers.get("x-hypamoon-user"), "x-hypamoon-user-sig": request.headers.get("x-hypamoon-user-sig"), }); export default { async fetch(request, env) { const url = new URL(request.url); if (url.pathname.startsWith("/api/share/")) return share(url, env); const user = env.HYPAMOON.requireUser(request); if (user instanceof Response) return user; const recipes = env.DB.mine("recipes"); if (url.pathname === "/api/recipes" && request.method === "GET") { return Response.json(await recipes.select({ orderBy: "created_at desc" })); } if (url.pathname === "/api/upload" && request.method === "POST") { const { name, content_type, size } = await request.json(); const r = await fetch(env.HYPAMOON_API + "/app-files", { method: "POST", headers: asUser(request), body: JSON.stringify({ name, content_type, size }), }); return new Response(r.body, { status: r.status, headers: { "content-type": "application/json" } }); } return Response.json({ error: "Not found" }, { status: 404 }); }, };A voice memo from the browser recorder works as it is. Scaling a recipe to more people is plain JavaScript in the page: multiply each amount by new servings divided by old servings.
Share by link. When the owner shares a recipe, the app copies it into
shareswith a random token.GET /api/share/<token>needs no sign-in, reads that one row withenv.DB.prepare, and asks/app-files/<id>?public=truefor a photo link that works for 15 minutes. Stop sharing by deleting the row.Check it.
invite_app_userfor yourself asadmin, open the site, sign in with the link from your email, add a recipe with a photo. If something fails, your chat readsget_app_logs.Grow it, one part at a time. Every next feature is a new migration (
0002_add_tags.sql, never a change to an old one), thendeploy_appwith all modules, then a test. If a version breaks,rollback_appputs the previous one back.
Build in small steps
A big app is a row of small ones. Ask your chat to finish one part, put it
live, try it in the browser and read the logs before it starts the next part.
That way you always have something that works, and a new chat can pick up
where the last one stopped: get_app and list_app_users show what is
there.
Skip the throwaway test app. The first real part (sign-in plus one table) is the test, and it stays.
Email from the app, and whether it arrived
The app sends with await env.HYPAMOON.sendEmail({ to, subject, text }) (the
Mail module must be on) and gets back { id, status }. Keep the id.
await env.HYPAMOON.getEmail(id)shows that email again:status(sent,delivered,bounced,complained, ...),reasonin plain words ("The mailbox is full."),recipient_statuswith one line per person, and atimelinewith the time of each step.await env.HYPAMOON.listEmails({ to, status, since })lists the emails of this app, newest first. An app only sees its own emails.- To react when one bounces, ask your chat for a webhook on
email.bouncedwith theapp_idof this app (webhooks). You get one event per recipient.
In the portal, Mail shows the same: filter on the app, a person, a status or a period, and open an email for its timeline. Users shows the sign-in emails of the app the same way.
AI in the app (early access, not on yet)
Sometimes the app's own logic is not enough: a recipe pasted from a chat message, with emoji and half sentences, that the parser cannot split into ingredients and steps. The app can then ask a model itself, without a key of its own:
const user = env.HYPAMOON.requireUser(request);
if (user instanceof Response) return user;
let draft = parseRecipe(text); // your own parser first
if (draft.ingredients.length < 2) {
try {
const { data } = await env.HYPAMOON.ai.generate({
system: "Turn the text into a recipe. Use only what is in the text.",
prompt: text,
schema: { type: "object", required: ["title", "ingredients", "steps"], properties: {
title: { type: "string" }, ingredients: { type: "array", items: { type: "string" } },
steps: { type: "array", items: { type: "string" } } } },
maxTokens: 1500,
});
draft = { ...draft, ...data };
} catch (e) {
// e.userMessage is safe to show; keep the parser's draft.
}
}promptormessages, optionalsystem,schema(then you getdata, checked against the schema, instead oftext) andmaxTokens(at most 4,000). At most 50,000 characters in.- Only for a signed-in user of the app, so not from
scheduled(). The fast model only. - It comes off the project's AI credit, at what the model cost times 5 and at least 0.001 euro a call. Turning a messy recipe into a clean one costs about 0.002 euro.
- Limits: 1 euro of AI per app per day, and 20 calls per user per day.
- Nothing of the text or the answer is stored or logged; only counts and costs.
- When the credit is used up the call fails with
.status402, at a limit with 429..userMessageis a calm sentence in the app's language that says nothing about credit or money. Keep a way without AI, and let people check the result before it is saved.
Personal data and admins
- One row per record, with an owner. Keep what people store in tables
with an owner column, marked with
set_table_owner_column. Never put everyone's data in one JSON row. Only per-user tables are covered when someone asks to see or delete their data (export_app_user,delete_app_user_data).deploy_appandcheck_readinesswarn when an app with sign-in has none. - Clean up copies. A table you made during an import (
users_old,backup_20261015,legacy_accounts) stays until you drop it.check_readinesslists tables that look like a copy; ask the user, then drop them with a migration. - Admins sign in more often. The admin role signs in again after 14 days
without a visit, everyone else after 90. Change it with
auth.admin_session_days. - No frames on other sites. Pages of an app with sign-in cannot be loaded
in a frame on another site. To let a partner embed it, set your own
frame-ancestorsin/_headers. - Acting as someone else. Hypamoon has no "view as" for app users. If your app lets an admin look or act as another user, log every start of it in a table of the app (who, as whom, when), ask for a reason before acting for someone, and show a clear banner while it is on.
Limits worth knowing
- Files per upload: 5 MB on the free plan, 25 MB on a paid plan. Shrink photos in the browser before the upload.
- Server code: 1 MB on the free plan, 3 MB on a paid plan.
- Each request has a small CPU budget and a few outbound calls. Slow work goes to the queue or a schedule.
- A schedule keeps running until you remove it. Add one only when there is real periodic work.
When something goes wrong
Every error from Hypamoon says what happened, a sentence for you, and the next step for your chat. A few you may see while building:
- A
.php,.py,.jsxor.tsxfile is refused. The error says how to do the same here: logic in JavaScript withdeploy_app, or the built files of your front end. - The code imports a package that is not there. Bundle it into the module, or write that part without the package.
- A file is too large. The app can shrink it in the browser first.