Skip to content
Hypamoon
API docs

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.

  1. 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).

  2. Put the app live. deploy_app without a site: a name, the pages (index.html, app.js), one server module, auth with signup: "invite" and roles admin and member, and database: true. No schedule, because a recipe book has no periodic work.

  3. Create the tables. apply_app_migration with 0001_init.sql:

    sql
    CREATE 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
    );
  4. Keep recipes private. set_table_owner_column for recipes with the column user_id. In the code, env.DB.mine("recipes") only ever sees the rows of the person who is signed in.

  5. Uploads. The page asks /api/upload for a link, sends the file there with a PUT, and stores the file id in the recipe. The server side:

    Node.js
    const 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.

  6. Share by link. When the owner shares a recipe, the app copies it into shares with a random token. GET /api/share/<token> needs no sign-in, reads that one row with env.DB.prepare, and asks /app-files/<id>?public=true for a photo link that works for 15 minutes. Stop sharing by deleting the row.

  7. Check it. invite_app_user for yourself as admin, open the site, sign in with the link from your email, add a recipe with a photo. If something fails, your chat reads get_app_logs.

  8. 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), then deploy_app with all modules, then a test. If a version breaks, rollback_app puts 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, ...), reason in plain words ("The mailbox is full."), recipient_status with one line per person, and a timeline with 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.bounced with the app_id of 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:

Node.js
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.
  }
}
  • prompt or messages, optional system, schema (then you get data, checked against the schema, instead of text) and maxTokens (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 .status 402, at a limit with 429. .userMessage is 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_app and check_readiness warn 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_readiness lists 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-ancestors in /_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, .jsx or .tsx file is refused. The error says how to do the same here: logic in JavaScript with deploy_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.

More: MCP, Errors, Test mode.

Hypamoon is the infrastructure your chat or agent builds on for a business. Hypamoon API, version 1.hypamoon.com