# heremyapp docs heremyapp publishes static websites and app installers (.pkg, .dmg, .zip) built by AI coding agents. Your agent uses a small HTTP API (a site deploy is one request) and gets a permanent public URL. No hosting, DNS or SSL setup. ## Concepts | Term | Meaning | |---|---| | Account | Your username, for example `peter`. It is part of every URL. | | Space | One website, for example `mac-utility`. Its public URL never changes. | | Deployment | One upload of a site's files. The published one is live; older ones can be restored. | | Artifact | One version of an installer file, with permanent download URLs. | | Token | A bearer token (`hma_...`) that authorizes API calls. | ## URLs | What | URL | Notes | |---|---|---| | Public site | `https://heremyapp.com/ACCOUNT/SPACE` | Share this one. It redirects to the site's own isolated origin, which may change. | | Latest installer | `https://heremyapp.com/dl/ACCOUNT/SPACE/latest/FILENAME` | Always the most recently published version | | Fixed installer version | `https://heremyapp.com/dl/ACCOUNT/SPACE/v/VERSION/FILENAME` | Never changes | | API | `https://api.heremyapp.com/v1` | JSON in and out | ## Quickstart Set up the shell once (see [Authentication](https://heremyapp.com/docs/authentication.md)): ```bash export HEREMYAPP_TOKEN="${HEREMYAPP_TOKEN:-$(cat ~/.config/heremyapp/token 2>/dev/null)}" API=https://api.heremyapp.com/v1 AUTH="Authorization: Bearer $HEREMYAPP_TOKEN" ``` Create a space, deploy a folder that contains `index.html`, and open the public URL: ```bash curl -sS -X POST "$API/spaces" -H "$AUTH" -H "content-type: application/json" \ -d '{"slug":"my-app","name":"My App"}' -w '\nHTTP %{http_code}\n' # 201 created, 409 already exists tar -czf - -C dist . | curl -sS --fail-with-body -X POST "$API/spaces/my-app/deployments" \ -H "$AUTH" -H "content-type: application/gzip" --data-binary @- ``` The deploy response contains `public_url`, for example `https://heremyapp.com/peter/my-app`. ## Pages - [Authentication](https://heremyapp.com/docs/authentication.md): tokens, scopes, where agents find the token - [Static sites](https://heremyapp.com/docs/sites.md): deploying, file rules, URL resolution, rollback - [Installers](https://heremyapp.com/docs/installers.md): publishing .pkg, .dmg and .zip files with permanent download links - [Releases and versioning](https://heremyapp.com/docs/releases.md): build numbers and semantic versions, managed by the agent on every deploy - [Landing page checklist](https://heremyapp.com/docs/landing.md): contact details, what to include, what people forget, multiple languages - [Privacy policy and terms](https://heremyapp.com/docs/legal.md): when you need them, templates, `/privacy` and `/terms`, app store fields - [For AI agents](https://heremyapp.com/docs/agents.md): what to tell your agent, the recommended workflow, the agent skill - [API reference](https://heremyapp.com/docs/api.md): every endpoint, error code and limit Every page is also plain Markdown: add `.md` to its URL (for example `https://heremyapp.com/docs/sites.md`). All pages in one file: `https://heremyapp.com/llms-full.txt`. ## Conventions - UPPERCASE words such as `ACCOUNT`, `SPACE` and `ARTIFACT_ID` are values taken from earlier responses. - Commands use the `API` and `AUTH` shell variables from the quickstart. - `curl -sS --fail-with-body` prints the error body and exits non-zero on an HTTP error (curl 7.76 or newer). - Responses are JSON. Read the fields directly, or use `jq -r .id` if `jq` is installed. --- # Authentication Every API call needs `Authorization: Bearer `. Tokens start with `hma_`. There is no self-signup yet: the account owner creates tokens and gives them to people or agents. ## Where the token comes from Agents resolve the token in this order: 1. Environment variable `HEREMYAPP_TOKEN` 2. File `~/.config/heremyapp/token` 3. Otherwise, ask the user for a token. Do not try to create an account. Never print, log or commit the token, and never put it in site files. ```bash export HEREMYAPP_TOKEN="${HEREMYAPP_TOKEN:-$(cat ~/.config/heremyapp/token 2>/dev/null)}" API=https://api.heremyapp.com/v1 AUTH="Authorization: Bearer $HEREMYAPP_TOKEN" curl -sS --fail-with-body "$API/me" -H "$AUTH" ``` ```json {"account":{"id":"acc_9x2k...","username":"peter"},"token":{"id":"tok_4m8q...","scope":"account"}} ``` `account.username` is the `ACCOUNT` part of every URL. To store a token for agents on this computer: ```bash mkdir -p ~/.config/heremyapp && chmod 700 ~/.config/heremyapp pbpaste > ~/.config/heremyapp/token && chmod 600 ~/.config/heremyapp/token # macOS, token copied to clipboard ``` ## Scopes | Scope | Can do | |---|---| | `account` | Create spaces, deploy and roll back any space, upload installers, create and revoke tokens | | `space` | For one space only: view it, deploy and roll back, upload and delete installers | A space token gets `403 forbidden` for any other space, and for creating spaces or managing tokens. If `/me` reports `"scope":"space"`, run `GET $API/spaces`: it returns the one space the token is for. If the user wants a different space, ask them for an account token. ## Creating and revoking tokens With an account token, create a token limited to one space (for CI or a single project's agent): ```bash curl -sS --fail-with-body -X POST "$API/tokens" -H "$AUTH" -H "content-type: application/json" \ -d '{"name":"my-app agent","space":"my-app"}' ``` The response contains `token` once; it cannot be shown again. List tokens with `GET $API/tokens` and revoke one with `DELETE $API/tokens/TOKEN_ID`. Revoked tokens stop working immediately. --- # Static sites A space holds one static website. Each deploy uploads the whole site as one archive and publishes it atomically. ## Create a space The slug is part of the permanent URL, so use the product's name: 1-30 characters, lowercase letters, digits and single hyphens (`my-app`, not `My_App` or `my--app`). ```bash curl -sS -X POST "$API/spaces" -H "$AUTH" -H "content-type: application/json" \ -d '{"slug":"my-app","name":"My App"}' -w '\nHTTP %{http_code}\n' ``` ```json {"id":"spc_7t1v...","slug":"my-app","name":"My App","status":"active","active_deployment_id":null, "public_url":"https://heremyapp.com/peter/my-app","site_url":"https://peter--my-app.heremyapp.com/","created_at":"2026-10-08T06:15:13.492Z"} ``` `409 space_exists` means the space is already there: use it. `GET $API/spaces` lists your spaces. Share `public_url`; `site_url` is the origin it redirects to and may change. ## Deploy Put the built site in a folder with `index.html` at its root and send the folder as a tar.gz body: ```bash tar -czf - -C dist . | curl -sS --fail-with-body -X POST "$API/spaces/my-app/deployments" \ -H "$AUTH" -H "content-type: application/gzip" --data-binary @- ``` ```json {"id":"dep_5adj...","space":"peter/my-app","status":"published","version":"1.0.0","build":1,"notes":null, "file_count":12,"total_size":845120, "public_url":"https://heremyapp.com/peter/my-app","site_url":"https://peter--my-app.heremyapp.com/", "skipped":[".DS_Store"],"stripped_prefix":null,"created_at":"2026-10-08T06:15:15.476Z", "advice":[{"code":"missing_terms","message":"No terms of use at /terms. ...","docs":"https://heremyapp.com/docs/legal"}]} ``` - Every deploy gets the next build number and a version. Add `bump` (`major`, `minor` or `patch`) and `notes` to the deploy URL; see [Releases and versioning](https://heremyapp.com/docs/releases.md). Without `bump`, the patch version goes up. - The response's `advice` list points out things people often miss (privacy policy, terms, link previews, page language, favicon). It never blocks a deploy; see [Privacy policy and terms](https://heremyapp.com/docs/legal.md#deploy-advice). - Publishing is atomic. A failed deploy leaves the live site untouched. - Redeploying keeps the same URL, and the change is visible immediately. - The server detects tar or tar.gz from the bytes; send `content-type: application/gzip` or `application/x-tar`. - If the archive wraps everything in a single folder (for example `dist/`), that folder is unwrapped. ## File rules - `index.html` must exist at the site root. - Allowed extensions: html, htm, css, js, mjs, json, map, webmanifest, txt, md, xml, pdf, wasm, png, jpg, jpeg, gif, webp, avif, svg, ico, woff, woff2, ttf, otf, mp4, webm. - If any file has another extension, or no extension, the **whole deploy fails** with `unsupported_file_type`, and `details.files` lists the offending files. Remove or rename them and deploy again. - Installers (.pkg, .dmg, .zip) are never site files. Publish them as [installers](https://heremyapp.com/docs/installers.md). - Hidden files and folders (`.DS_Store`, `._*`, `.git/`) and `__MACOSX/` are skipped and listed in `skipped`. - Paths with `..`, absolute paths, backslashes and duplicates are rejected (`invalid_path`, `duplicate_path`). Symlinks and other special files are rejected with `unsupported_entry`: copy the real files into the folder instead. - Limits: 20 MiB of files in total, 1,000 files, 25 MiB compressed upload. ## How URLs map to files The site is served at the root of its own origin, so relative (`assets/app.css`) and root-absolute (`/assets/app.css`) paths both work. | Request | File served | |---|---| | `/` | `index.html` | | `/style.css` | `style.css` (exact path first) | | `/about` | `about.html`, otherwise `about/index.html` (redirects to `/about/`) | | `/about/` | `about/index.html` | | anything missing | your `404.html` with status 404, if it exists | ## Verify ```bash curl -sIL https://heremyapp.com/ACCOUNT/my-app | grep -iE "^HTTP|^location" # 302 to the site origin, then 200 ``` ## Stage, inspect and roll back - Stage without publishing: add `?publish=false` to the deploy URL, then publish it with `POST $API/deployments/DEPLOYMENT_ID/publish`. - `GET $API/spaces/my-app/deployments` lists the 50 most recent deployments, newest first. The live one has `"status":"published"`. - `GET $API/deployments/DEPLOYMENT_ID` shows one deployment with its file list. - Roll back by publishing an older deployment: ```bash curl -sS --fail-with-body -X POST "$API/deployments/OLD_DEPLOYMENT_ID/publish" -H "$AUTH" ``` --- # Installers Publish app installers (.pkg, .dmg, .zip) with permanent download links. Each upload is one immutable version. ## Before uploading (macOS) heremyapp does not sign, notarize or vouch for installers. Sign the app and the .pkg with Developer ID, notarize and staple before uploading, then check: ```bash spctl --assess --type install -vv MyApp.pkg # expect "accepted" and "source=Notarized Developer ID" ``` Agents: if this check fails, stop and ask the user before uploading. Unsigned installers are blocked by Gatekeeper on users' Macs. Notarizing needs credentials stored in the keychain (`xcrun notarytool store-credentials PROFILE_NAME ...`, then `xcrun notarytool submit MyApp.pkg --keychain-profile PROFILE_NAME --wait`). If no profile exists, ask the user to run `store-credentials` themselves in a terminal. It uses their Apple ID app-specific password or an App Store Connect API key; never ask them to paste those into the conversation. ## Names and versions - `filename`: the bare file name, without folders. 1-100 characters of letters, digits, `.`, `_` or `-`, ending in `.pkg`, `.dmg` or `.zip`. Keep it the same across releases (`MyApp.pkg`, not `MyApp-1.2.0.pkg`). - `version`: a [semantic version](https://semver.org) such as `1.2.0`, normally the app's `CFBundleShortVersionString`. It must be higher than the previous release of the same file. - `build` (recommended): the app's build number, such as `CFBundleVersion` (`46` or `1.2.46`). It must also increase. - `notes` (optional): a one-line summary of what changed. Show it on the landing page as release notes. - See [Releases and versioning](https://heremyapp.com/docs/releases.md) for how to choose the next version and build. - `latest` means the most recently published version, not the highest version number. Because the filename is stable, the latest link is predictable before you upload: `https://heremyapp.com/dl/ACCOUNT/SPACE/latest/FILENAME`. Publishing the installer before the site means the link works as soon as the site is live. Always use this **absolute** URL in the site. The site runs on its own origin, so a relative link such as `/dl/...` would point at the site itself and return 404. ## 1. Measure the file ```bash FILE=./build/MyApp.pkg NAME=$(basename "$FILE") SIZE=$(stat -f%z "$FILE" 2>/dev/null || stat -c%s "$FILE") SHA=$(shasum -a 256 "$FILE" | cut -d' ' -f1) ``` ## 2. Register it ```bash curl -sS --fail-with-body -X POST "$API/spaces/my-app/artifacts" -H "$AUTH" -H "content-type: application/json" \ -d "{\"filename\":\"$NAME\",\"version\":\"1.2.0\",\"build\":\"46\",\"notes\":\"Faster sync\",\"size\":$SIZE,\"sha256\":\"$SHA\"}" ``` Success is `201` with the artifact in `"status":"pending"`: ```json {"id":"art_3kq9...","space":"peter/my-app","filename":"MyApp.pkg","version":"1.2.0","size":18234567, "sha256":"4583...75fa","status":"pending", "download_url":"https://heremyapp.com/dl/peter/my-app/v/1.2.0/MyApp.pkg", "latest_url":"https://heremyapp.com/dl/peter/my-app/latest/MyApp.pkg", "upload":{"mode":"single","method":"PUT","url":"https://api.heremyapp.com/v1/artifacts/art_3kq9.../content", "command":"curl -sS --fail-with-body -T MyApp.pkg -H \"Authorization: Bearer $HEREMYAPP_TOKEN\" https://api.heremyapp.com/v1/artifacts/art_3kq9.../content"}} ``` For a larger file, `upload` looks like this instead: ```json {"mode":"multipart","part_size":52428800,"part_count":3, "part_url":"https://api.heremyapp.com/v1/artifacts/art_3kq9.../parts/{part_number}","complete_url":"https://api.heremyapp.com/v1/artifacts/art_3kq9.../complete", "command":"split -b 52428800 -a 3 MyApp.pkg /tmp/art_3kq9....part. && ..."} ``` `upload.mode` tells you which upload to do. `upload.command` is a ready-made shell command that contains no secrets. It refers to the file by its bare name, so run it from the folder that contains the file (`cd "$(dirname "$FILE")"`), with `HEREMYAPP_TOKEN` set. Or follow the steps below with your own path. Registering the same filename and version again while it is still pending replaces the pending one. ## 3a. Upload in one request (`"mode":"single"`, up to 95 MiB) ```bash curl -sS --fail-with-body -T "$FILE" -H "$AUTH" "$API/artifacts/ARTIFACT_ID/content" ``` The response is the artifact with `"status":"active"`. ## 3b. Upload in parts (`"mode":"multipart"`, up to 500 MiB) `upload.part_size` (in bytes; use it as `PART_SIZE` below) and `upload.part_count` describe the parts. Parts are numbered from 1, and every part except the last must be exactly `part_size` bytes. A failed part can simply be sent again. ```bash split -b PART_SIZE -a 3 "$FILE" /tmp/heremyapp-part. n=1; ok=1 for f in /tmp/heremyapp-part.*; do curl -sS --fail-with-body -T "$f" -H "$AUTH" "$API/artifacts/ARTIFACT_ID/parts/$n" || { ok=0; break; } n=$((n+1)) done [ "$ok" = 1 ] && curl -sS --fail-with-body -X POST -H "$AUTH" "$API/artifacts/ARTIFACT_ID/complete" rm -f /tmp/heremyapp-part.* ``` `complete` returns the artifact with `"status":"active"`, or `missing_parts` listing parts still to send. ## Verification The server checks the exact size, the SHA-256 and the file signature (pkg = xar archive, dmg = UDIF image, zip). If a check fails (`checksum_mismatch`, `invalid_file_content`), the artifact is discarded: fix the file or the values and register again. Published versions are immutable, and each release must have a higher version and build than the last one (`409 version_not_increasing` or `build_not_increasing`, with suggested values in `details`). ## Download links - `latest_url`: `https://heremyapp.com/dl/ACCOUNT/SPACE/latest/FILENAME`. Use this for the download button. - `download_url`: `https://heremyapp.com/dl/ACCOUNT/SPACE/v/VERSION/FILENAME`. This exact version, forever. Downloads use `Content-Disposition: attachment`, support resumable Range requests and HEAD, and send the SHA-256 in `ETag` and `X-Checksum-SHA256`. ```bash # quick check without downloading: which version latest points to, and its hash curl -sIL https://heremyapp.com/dl/ACCOUNT/my-app/latest/MyApp.pkg | grep -iE "^location|^x-checksum-sha256" # full check: download and hash curl -fsSL -o /tmp/check.pkg https://heremyapp.com/dl/ACCOUNT/my-app/latest/MyApp.pkg && shasum -a 256 /tmp/check.pkg ``` ## Manage - `GET $API/spaces/my-app/artifacts` lists installers with `downloads` and `bytes_served`. - `GET $API/artifacts/ARTIFACT_ID` shows one, including upload instructions while it is pending. - `DELETE $API/artifacts/ARTIFACT_ID` removes a version. Its URL then returns `410 Gone`, `latest` falls back to the previous version, and the version cannot be reused. --- # Releases and versioning Every site deploy and every installer release carries a **version** and a **build number**. Agents manage both automatically on every deploy, so the user never has to think about them. ## Policy - **Build number**: goes up on every deploy. heremyapp assigns site builds itself (1, 2, 3, ...). For installers the build is the app's own build number (for example `CFBundleVersion`), which you increase before building. - **Version**: [Semantic Versioning](https://semver.org) `MAJOR.MINOR.PATCH`, bumped by what changed since the previous release: | Bump | Apps and installers | Landing pages | Example | |---|---|---|---| | major | Incompatible or large changes: removed features, a new minimum OS, data or file formats older versions cannot read, a paid upgrade | Redesign or rebrand | 1.4.2 → 2.0.0 | | minor | New features or improvements: dark mode, a new setting, a new integration, noticeably faster | New sections, pages or languages | 1.4.2 → 1.5.0 | | patch | Fixes: bug and crash fixes, hotfixes, translation fixes | Updating release info (version, size, notes), typos, broken links, small style fixes | 1.4.2 → 1.4.3 | When a release mixes changes, use the largest bump that applies: bug fixes plus dark mode is **minor**. Decide the bump yourself from the changes you made or can see (your edits, `git diff`, `git log`). Do not ask the user to pick a version; tell them the result in your report. Use a pre-release such as `2.0.0-beta.1` only when the user asks for a beta. ## Before every deploy: check the current versions ```bash curl -sS --fail-with-body "$API/spaces/my-app/versions" -H "$AUTH" ``` ```json {"policy":"Every deploy gets the next build number automatically. ...", "site":{"live":{"deployment_id":"dep_5adj...","version":"1.4.2","build":17,"notes":"Fix typo","created_at":"..."}, "latest":{"deployment_id":"dep_5adj...","version":"1.4.2","build":17,"notes":"Fix typo","created_at":"..."}, "next":{"build":18,"patch":"1.4.3","minor":"1.5.0","major":"2.0.0"}}, "artifacts":[{"filename":"MyApp.pkg","latest":{"id":"art_3kq9...","version":"1.2.0","build":"45","status":"active"}, "live":{"id":"art_3kq9...","version":"1.2.0","build":"45"}, "next":{"patch":"1.2.1","minor":"1.3.0","major":"2.0.0","build":"46"}}], "contact_status":"provided"} ``` - `site.live` is what visitors see now; `site.latest` is the newest deploy (they differ after a rollback). Next numbers always continue from `latest`. - For each installer file, `latest` is the highest version ever published (including deleted ones, whose numbers cannot be reused) and `live` is what the `latest` download link serves now. - `contact_status` tells you whether to ask the user for contact details (see [Landing page checklist](https://heremyapp.com/docs/landing.md)). ## Deploy a site with its version Add `bump` (and a one-line `notes` summary) to the deploy URL. curl's `--url-query` encodes the values for you: ```bash tar -czf - -C dist . | curl -sS --fail-with-body -X POST "$API/spaces/my-app/deployments" \ --url-query "bump=minor" --url-query "notes=Add Japanese page" \ -H "$AUTH" -H "content-type: application/gzip" --data-binary @- ``` - curl older than 7.87 has no `--url-query`: put the values in the URL instead and percent-encode the notes, for example `"$API/spaces/my-app/deployments?bump=minor¬es=Add%20Japanese%20page"`. - The response includes `version` and `build`. Without `bump`, heremyapp bumps the patch version. - Send `version=X.Y.Z` instead of `bump` to set an exact version; it must be higher than the current one (`409 version_not_increasing` lists suggestions). - The first deploy of a space is `1.0.0`, build 1. - Every page of the live site carries `X-Heremyapp-Version` and `X-Heremyapp-Build` headers, so you can confirm the new version is live: ```bash curl -sIL https://heremyapp.com/ACCOUNT/my-app | grep -iE "^x-heremyapp-(version|build)" ``` ## Release an installer with its version 1. Read the app's current version and build from the project: - Xcode: `MARKETING_VERSION` and `CURRENT_PROJECT_VERSION` build settings (`CFBundleShortVersionString` and `CFBundleVersion`). With Apple Generic Versioning: `agvtool what-marketing-version`, `agvtool what-version`. - Electron or Node: `version` in `package.json`; a build number if the project keeps one. 2. Work out the new numbers from `artifacts[].latest` (the published release): - Version: bump the published version by the policy above. If the project's version is already higher than that (the user or an earlier step set it and it was never published), keep the project's version. - Build: one more than the published build, or the project's build if that is already higher. - Build numbers compare part by part as numbers (`46 < 47`, `1.2.9 < 1.2.10`). Keep one format per app. 3. Write them into the project (for example `agvtool new-marketing-version 1.3.0` and `agvtool new-version -all 46`, or edit the build settings), then build, sign and notarize. 4. Register the installer with `version`, `build` and `notes` ([Installers](https://heremyapp.com/docs/installers.md)). heremyapp rejects a version or build that does not increase (`version_not_increasing`, `build_not_increasing`, with suggestions). 5. Update the version, size and release notes shown on the landing page, and deploy the site (usually `bump=patch`, or `minor` if you added content). The landing page should show the **app's** version (from the installer). The site's own version tracks changes to the page itself. --- # Landing page checklist heremyapp exists so people can put their app or web service online quickly, without setup chores or things they need to know in advance. A good landing page is part of that. Agents: use the whole checklist when you **create or redesign** a landing page, and fill in what the user would otherwise miss. For a **routine release**, update only the release information (version, size, date, release notes, structured data) and fix anything clearly broken; do not add languages or sections unless the user asks. ## Contact and business details (ask once) ```bash curl -sS --fail-with-body "$API/spaces/my-app/contact" -H "$AUTH" # {"status":"unknown" | "provided" | "skipped", ...} ``` - `unknown`: ask the user **once**, in their language, and say that skipping is fine. For example: "Do you want contact details on the page? Tell me any of: email, business name, support phone, address. You can also skip this." If another of the account's spaces already has contact details (`GET /spaces/OTHER/contact`), offer to reuse them. When you are creating a page, ask about languages in the same message (see below), so the user answers once. - Save the answer so nobody asks again. Each `PUT` replaces all saved details and sets `status` to `provided` (or `skipped`), so send every field you want to keep: ```bash curl -sS --fail-with-body -X PUT "$API/spaces/my-app/contact" -H "$AUTH" -H "content-type: application/json" \ -d '{"business_name":"Acme Inc.","contact_email":"support@acme.example","phone":"+82-2-123-4567","country":"KR"}' curl -sS --fail-with-body -X PUT "$API/spaces/my-app/contact" -H "$AUTH" -H "content-type: application/json" \ -d '{"status":"skipped"}' ``` - `provided` or `skipped`: do not ask again unless the user brings it up. - Fields: `business_name`, `representative`, `contact_email`, `phone`, `support_url`, `address`, `country` (two letters, e.g. `KR`), `registration_number`. - Show them in a footer or a "Contact" section. Use a `mailto:` link for email. - Some countries require business details on sites that sell to consumers (for example South Korea's e-commerce law asks sellers to show business name, representative, registration number, address and contact; Germany requires an Impressum). If `country` suggests this, mention it to the user. Do not present it as legal advice. ## What the page should contain - App name, icon and a one-sentence value proposition at the top - What it does: three to six key features, screenshots with alt text, optionally a short video (mp4 or webm) - A download button pointing at the absolute `latest_url`, with version, file size, release date and requirements (minimum OS, Apple silicon or Intel) - Install steps and first-launch notes; how to uninstall - Release notes from the installer's `notes` - SHA-256 checksum for people who verify downloads - Contact or support, and pricing if any - A privacy policy at `/privacy` and terms of use at `/terms`, linked from the footer (or header) of every page ([Privacy policy and terms](https://heremyapp.com/docs/legal.md)) ## Often missed - `` and `<meta name="description">`, written for each language - Link previews: `og:title`, `og:description`, `og:image` (1200×630, absolute URL), `og:url` (the public URL) and `twitter:card`, so shared links look right in Slack, KakaoTalk, X and messengers - `favicon.ico` or an SVG favicon, and a 180×180 `apple-touch-icon.png` - Structured data: a JSON-LD `SoftwareApplication` block (`name`, `operatingSystem`, `applicationCategory`, `softwareVersion`, `downloadUrl`, `offers` with the price) for search engines and AI assistants - An `llms.txt` at the site root that describes the product in plain Markdown for AI assistants - A `404.html`, a layout that works on phones, readable contrast, and the page itself following the system dark mode - No secrets, tokens or private keys in any file ## Multiple languages Make the page easy to share worldwide: - When creating a page, publish in English and in the user's own language unless they say otherwise. Offer more languages (for example Japanese, Simplified Chinese, Spanish) in the same question as the contact details. If the user skips or has no preference, use the default and go on. - Later releases keep the languages the site already has, and update every language version. - Write installer `notes` in English; translate them on each language version of the page. - Put the default language at `/` and the others in folders named with language codes: `/ko/`, `/ja/`, `/zh-Hans/`. - Set `<html lang="...">` on every page, add a visible language switcher with plain links (never redirect by location), and add `hreflang` alternates with absolute URLs based on the space's `site_url`, including `x-default`. - Translate everything visible, including the title, description and link-preview text. Keep the same download link. - Adding a language is a `minor` release. --- # Privacy policy and terms Before a solo developer or freelancer shows a service to the world or submits an app for review, they need a privacy policy and usually terms of use. heremyapp agents create them from templates, publish them at `/privacy` and `/terms`, and link them from every page, so the user does not have to remember. ## Do you need them? Default: **create both**, and keep them short when the app is simple. | Page | When | Why | |---|---|---| | Privacy policy (`/privacy`) | Always for apps in the App Store or Google Play; any site or app that collects personal information (forms, accounts, emails, analytics, crash reports, cookies) | Apple requires a privacy policy URL for every app; Google Play asks for one in the Data safety section; privacy laws (GDPR, CCPA, South Korea's PIPA) require one when personal information is processed. Even "we collect nothing" is worth stating. | | Terms of use (`/terms`) | Accounts, payments, user content or an online service; recommended for free apps as a license and disclaimer | Sets the license, limits liability, and answers reviewers and customers | ## How agents create them 1. Find out what the app and site really do with data: read the code for network requests, analytics or crash-reporting SDKs, accounts, payments, permissions and the files it reads. Do not guess. 2. Fetch the templates and fill them in: ```bash curl -fsSL https://heremyapp.com/docs/templates/privacy.md curl -fsSL https://heremyapp.com/docs/templates/terms.md ``` 3. Take the operator name and contact email from the space's contact details ([Landing page checklist](https://heremyapp.com/docs/landing.md)). If they are unknown, ask for them in the same single contact question. A privacy policy needs a way to reach the operator; if the user skips, use the support page or ask whether a contact form or email can be added. 4. Publish them as `privacy.html` and `terms.html` at the site root (served at `/privacy` and `/terms`), styled like the rest of the site. With multiple languages, add `ko/privacy.html` and so on; the Korean privacy policy should follow the headings noted at the end of the privacy template. 5. Link both from **every page**, in the footer (and optionally the header): "Privacy" and "Terms" in the page's language. 6. Tell the user which facts you assumed and that the documents are templates, not legal advice. Suggest a review by a professional if the service takes payments or handles sensitive data. Adding the legal pages is a `minor` release. When you change them later, update the effective date. ## App store and review fields Use the permanent public URLs. They keep working when heremyapp moves sites to a new domain. | Field | URL | |---|---| | App Store Connect: Privacy Policy URL; Google Play: Privacy policy | `https://heremyapp.com/ACCOUNT/SPACE/privacy` | | Terms of use or EULA link (if the store asks) | `https://heremyapp.com/ACCOUNT/SPACE/terms` | | Support URL | `https://heremyapp.com/ACCOUNT/SPACE` with a contact section, or a support page | | Marketing URL | `https://heremyapp.com/ACCOUNT/SPACE` | What the policy says must match the store's privacy questionnaire (Apple's App Privacy details, Google Play's Data safety form). ## Deploy advice Every deploy response has an `advice` list that never blocks the deploy. It reports `missing_privacy`, `missing_terms` and `legal_not_linked`, as well as landing page basics (`missing_link_preview`, `og_image_not_absolute`, `missing_lang`, `missing_description`, `missing_favicon`). Each item has a `message` and a `docs` link. Fix what applies and deploy again; an empty list means the checks passed. --- # For AI agents heremyapp is built to be used by coding agents (Claude Code, Codex, Cursor and others) with nothing more than a shell: `curl`, `tar` and `shasum`. No SDK or plugin is required. ## For people: what to tell your agent Give the agent a token first (see [Authentication](https://heremyapp.com/docs/authentication.md)), then ask in plain words, for example: ```text Deploy this project's landing page to heremyapp. Read https://heremyapp.com/llms.txt first. ``` ```text Build MyApp.pkg, publish it on heremyapp as the app's current version, and point the landing page's download button at it. Read https://heremyapp.com/docs/installers.md and https://heremyapp.com/docs/sites.md first. ``` Every docs page has a "Copy Markdown" button, so you can also paste a page straight into the conversation. ## Agent skill Agents that support skills (for example Claude Code) can install the heremyapp skill: ```bash mkdir -p ~/.claude/skills/heremyapp && curl -fsSL https://heremyapp.com/skill.md -o ~/.claude/skills/heremyapp/SKILL.md ``` ## Machine-readable discovery | URL | What it is | |---|---| | `https://heremyapp.com/llms.txt` | Index of these docs ([llms.txt](https://llmstxt.org/)) | | Any docs page with `Accept: text/markdown` | The page as Markdown, same as adding `.md` | | `https://heremyapp.com/openapi.json` | OpenAPI 3.1 description of the API | | `https://heremyapp.com/.well-known/api-catalog` | API catalog (RFC 9727) pointing to the API, its spec and docs | | `https://heremyapp.com/.well-known/agent-skills/index.json` | Agent Skills index with the heremyapp skill | | `https://heremyapp.com/.well-known/auth.md` | How agents authenticate | Pages also send `Link` headers to these resources. ## Recommended workflow 1. Resolve the token (`HEREMYAPP_TOKEN`, then `~/.config/heremyapp/token`, else ask the user) and call `GET /me`. Note `account.username`. If the token scope is `space`, use the space from `GET /spaces`. 2. Create the space, or reuse it on `409 space_exists`. 3. Check versions: `GET /spaces/SPACE/versions` ([Releases and versioning](https://heremyapp.com/docs/releases.md)). Decide the bump from what changed, without asking the user. 4. Contact details: if `contact_status` is `unknown`, ask the user once (skipping is fine) and save the answer ([Landing page checklist](https://heremyapp.com/docs/landing.md)). 5. If there is an installer: set the next version and build in the project, build, check its signature (macOS: `spctl`; ask the user if it fails), then publish it with `version`, `build` and `notes` ([Installers](https://heremyapp.com/docs/installers.md)). The latest link is `https://heremyapp.com/dl/ACCOUNT/SPACE/latest/FILENAME`. 6. Prepare the site with the [Landing page checklist](https://heremyapp.com/docs/landing.md): content, often-missed items, languages, contact details, and the app's version, size and release notes. If a landing page exists, update it instead of replacing it. A new site gets `/privacy` and `/terms` from the templates, linked from every page ([Privacy policy and terms](https://heremyapp.com/docs/legal.md)). 7. Deploy the site with `bump` and `notes` ([Static sites](https://heremyapp.com/docs/sites.md)). Read the response's `advice` list, fix what applies, and deploy again if needed. 8. Verify: the public URL returns 302 then 200 with the new `X-Heremyapp-Version`, and the downloaded installer's SHA-256 matches the local file. 9. Report the public URL (`https://heremyapp.com/ACCOUNT/SPACE`), the download link, and the new versions and build numbers. ## Rules - Never print, log or commit the token, and never put it into site files. - Do not create accounts or guess tokens. If there is no token, ask the user. - Share the public URL, not the site origin it redirects to. - When a request fails, read `error.message`: it says what to change. `error.docs` links back to these docs. - Do not upload an installer that fails signature checks unless the user explicitly says so. - Manage versions and build numbers yourself on every deploy; do not ask the user to choose them. - Ask for contact details at most once per space; respect `skipped`. ## Troubleshooting | Symptom | What to do | |---|---| | `Could not resolve host` for a heremyapp host | Usually a stale local DNS cache. Resolve it directly and pin it: `IP=$(dig +short api.heremyapp.com @1.1.1.1 \| head -1)`, then add `--resolve api.heremyapp.com:443:$IP` to curl. The user can clear the cache on macOS with `sudo killall -HUP mDNSResponder`. | | `401 unauthorized` | The token is missing, mistyped, expired or revoked. Ask the user for a valid token. | | `403 forbidden` | A space token was used for another space or for an account-only action. | | `unsupported_file_type` | Remove the files listed in `details.files` from the site folder, or publish installers as artifacts. | | `409 artifact_exists` | That version is already published. Use a new version. | --- # API reference Base URL: `https://api.heremyapp.com/v1`. Requests and responses are JSON, except site uploads (tar or tar.gz) and installer uploads (raw bytes). Authenticate with `Authorization: Bearer hma_...`. ## Endpoints | Method | Path | Purpose | Token | |---|---|---|---| | GET | `/me` | Account and token scope | any | | POST | `/spaces` | Create a space `{slug, name?}` | account | | GET | `/spaces` | List spaces | any | | GET | `/spaces/{slug}` | Space details, including `active_deployment_id` | any | | POST | `/spaces/{slug}/deployments` | Deploy a tar or tar.gz body. `?bump=major\|minor\|patch` or `?version=`, `?notes=`, `?publish=false` stages it. The response's `advice` lists non-blocking suggestions | any | | GET | `/spaces/{slug}/deployments` | 50 most recent deployments, newest first | any | | GET | `/spaces/{slug}/versions` | Live and latest versions, next build and version suggestions, `contact_status` | any | | GET | `/spaces/{slug}/contact` | Contact details (`status`: unknown, provided, skipped) | any | | PUT | `/spaces/{slug}/contact` | Save contact fields, or `{"status":"skipped"}` | any | | GET | `/deployments/{id}` | Deployment details and file list | any | | POST | `/deployments/{id}/publish` | Make a deployment live (also rolls back) | any | | POST | `/spaces/{slug}/artifacts` | Register an installer `{filename, version, build?, notes?, size, sha256}`; `201` with `upload` | any | | GET | `/spaces/{slug}/artifacts` | Installers with `downloads` and `bytes_served` | any | | GET | `/artifacts/{id}` | Installer details, with `upload` while pending | any | | PUT | `/artifacts/{id}/content` | Upload an installer of up to 95 MiB in one request | any | | PUT | `/artifacts/{id}/parts/{n}` | Upload part `n` (from 1) of a larger installer | any | | POST | `/artifacts/{id}/complete` | Finish a multipart upload | any | | DELETE | `/artifacts/{id}` | Remove an installer version (its URL returns 410) | any | | POST | `/tokens` | Create a token `{name, space?}`; `space` limits it to one space | account | | GET | `/tokens` | List tokens (values are never shown again) | account | | DELETE | `/tokens/{id}` | Revoke a token | account | "any" means an account token, or a space token for that space. `GET https://api.heremyapp.com/` without a token returns links to these docs. ## Errors ```json {"error":{"code":"unsupported_file_type","message":"Unsupported file types: notes.docx. ...", "details":{"files":["notes.docx"]},"docs":"https://heremyapp.com/llms-full.txt"}} ``` | Code | Status | Meaning | |---|---|---| | `unauthorized` | 401 | Missing, wrong, expired or revoked token | | `https_required` | 403 | The request used plain `http://`; use `https://` (the token may have been exposed, so consider revoking it) | | `forbidden` | 403 | Space token used for another space, or for an account-only action | | `space_suspended` | 403 | The space has been suspended | | `space_not_found`, `deployment_not_found`, `artifact_not_found`, `token_not_found` | 404 | Wrong slug or ID, or not yours | | `not_found` | 404 | No such endpoint | | `space_exists` | 409 | The space already exists; use it | | `invalid_slug`, `invalid_json`, `invalid_artifact` | 400 | Fix the request body; `message` says how | | `empty_body`, `invalid_archive`, `invalid_path`, `duplicate_path` | 400 | Fix the site archive; `message` names the file | | `unsupported_entry` | 400 | The archive contains a symlink or special file; include real files instead | | `unsupported_file_type` | 400 | Site contains files it cannot contain; see `details.files` | | `missing_index` | 400 | No `index.html` at the site root | | `site_too_large`, `too_many_files`, `upload_too_large` | 413 | Over the site limits | | `checksum_mismatch`, `invalid_file_content` | 400 | Installer failed verification and was discarded; register again | | `size_mismatch`, `invalid_part` | 400 | The upload sent a different number of bytes than expected | | `length_required` | 411 | Send `Content-Length` (`curl -T` does) | | `missing_parts` | 400 | Send the parts in `details.missing`, then complete again | | `use_multipart`, `use_single_upload` | 400 | Wrong upload mode; `details.upload` has the right one | | `artifact_exists`, `already_uploaded` | 409 | That version is already published; use a new version | | `version_not_increasing`, `build_not_increasing` | 409 | Use a higher version or build; `details` has suggestions | | `invalid_release` | 400 | Bad `bump` or `version` on a deploy | | `deploy_conflict` | 409 | Another deploy finished at the same moment; deploy again | | `invalid_contact` | 400 | Unknown field or invalid value; `details.allowed_fields` lists the fields | | `internal_error` | 500 | Unexpected server error; retry later | ## Limits | Resource | Limit | |---|---| | Site | 20 MiB of files, 1,000 files, 25 MiB compressed upload, paths up to 512 bytes | | Installer | 500 MiB per file; single upload up to 95 MiB, larger files in 50 MiB parts | | Installer types | .pkg, .dmg, .zip | | Space slug | 1-30 characters: lowercase letters, digits, single hyphens | | Account name | 3-30 characters: lowercase letters, digits, single hyphens |