Hosting
Muhkoo can host your app’s frontend too. Every app gets a DNS subdomain —
https://<slug>.apps.muhkoo.dev (the <slug> is your app’s slug) — and you
deploy your built static site there. No separate hosting account, no DNS to wire
up. Deploys are content-addressed (only changed files upload), instant (an atomic
release flip), and rollback-able. Each app also has a test site at
https://<slug>.test.apps.muhkoo.dev; see Environments & deploy.
Deploy auth — the app secret key
Section titled “Deploy auth — the app secret key”Deploys are authorized by your app’s secret key (mk_live_sk_… /
mk_test_sk_…) as a bearer token (a developer session also works). The secret key
is server-side only — never ship it in the browser bundle (that’s the
publishable pk). In CI it’s a repository secret. Rotate or revoke it in the
portal like any key.
Deploy
Section titled “Deploy”The simplest path is the muhkoo CLI (npm i -g @muhkoo/cli). It
walks dist/ (or the folder you give --dist), uploads only changed files,
commits a release, and prints the URL:
npm run buildmuhkoo deploy --app <appId> --key mk_live_sk_…# or, from a provisioned app dir (.muhkoo-app.json present): muhkoo deploy# the app-builder templates wire `npm run deploy` to run this for youFrom a provisioned directory, a bare muhkoo deploy uses the test secret key
and lands on the test site; pass a live secret key to publish production.
Limits
Section titled “Limits”index.htmlmust be in the release.- At most 2,000 files per release and 25 MiB per file.
- Hosted bytes count against your storage quota: an upload over it is refused
(
402on a blob,413on the release).
GitHub CI/CD
Section titled “GitHub CI/CD”Deploy on every push to main with the bundled .github/workflows/deploy.yml. Set
these repository secrets (Settings → Secrets and variables → Actions):
| Secret | What |
|---|---|
MUHKOO_DEPLOY_KEY | app secret key (mk_live_sk_…) — authorizes the deploy |
MUHKOO_APP_ID | the app id |
VITE_MUHKOO_KEY | app publishable key (mk_live_pk_…) — baked into the bundle |
The deploy API
Section titled “The deploy API”If you’re rolling your own deploy, the API is under /api/apps/:appId/hosting,
authorized by Authorization: Bearer <app sk | dev session>:
| Method | Path | Body | Purpose |
|---|---|---|---|
| PUT | /hosting/blob/:sha | raw file bytes | Upload one file; the server verifies sha256(body) === sha and dedups. |
| POST | /hosting/releases | { manifest: { "<path>": "<sha>" } } | Commit a release (validates blobs, enforces quota, flips live). Returns { releaseId, url, bytes, files, env }. |
| GET | /hosting | — | Site status: url, current release, bytes, history. |
| POST | /hosting/rollback | { releaseId } | Re-point to a prior release. |
| DELETE | /hosting/releases/:releaseId | — | Delete a non-current release from history. |
| DELETE | /hosting | — | Unpublish. |
The manifest maps each file’s path (relative to dist/, forward slashes,
index.html at the root) to its blob sha. The key you authorize with decides the
environment: a test secret key commits to the test site, a live key to
production.
How paths resolve
Section titled “How paths resolve”- An exact match in the manifest is served. A path ending in
/serves that directory’sindex.html(/docs/servesdocs/index.html). - Any other path without a file extension falls back to the root
index.html, which is what single-page-app routing wants. Hosting does not resolve extensionless paths to files:/aboutserves the rootindex.htmleven if the release holdsabout/index.htmlorabout.html. For a multi-page static site, link to directory-style URLs with a trailing slash (/about/). - A missing path with a file extension (a stale
/assets/old.js) returns a404rather than serving HTML as JavaScript.
Caching
Section titled “Caching”HTML, *.webmanifest, everything under /.well-known/, sw.js,
service-worker.js and registerSW.js are served max-age=0, must-revalidate,
so a deploy is live immediately. Every other file is served
public, max-age=31536000, immutable (one year). That is safe for fingerprinted
bundles (index-ABC123.js, which Vite emits), but a file with a stable name and
changing content (logo.png, data.json) gets pinned in visitors’ browsers. Give
such files a hashed or versioned name.
The platform keeps the last 10 releases per app (the live one is always kept); older releases auto-prune on deploy, and you can delete any non-current release.
Cross-origin isolation
Section titled “Cross-origin isolation”Some browser features — SharedArrayBuffer, and therefore multi-threaded
WebAssembly (e.g. ffmpeg.wasm, some game engines, OpenCV) — only work on a
cross-origin-isolated page. That requires the document to be served with the
Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy response headers,
which you can’t set from static files.
Muhkoo makes this a per-app, opt-in hosting setting. Turn it on and the platform serves your app with:
Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: credentiallessCross-Origin-Resource-Policy: same-origincredentialless is used (rather than require-corp) so your cross-origin
resources still load — CDN scripts, images, fonts, and CORS API calls — without
every third party having to send Cross-Origin-Resource-Policy. In the browser,
self.crossOriginIsolated becomes true and SharedArrayBuffer is available.
Enable it in any of three ways:
- Provision spec — add
"crossOriginIsolation": trueto yourapp.json(top level, or under a"hosting"object) and runmuhkoo provision. - Portal — App Detail → Hosting → toggle Cross-origin isolation.
- API —
PATCH /api/apps/:appId(see below).
| Method | Path | Body | Purpose |
|---|---|---|---|
| PATCH | /api/apps/:appId | { "crossOriginIsolation": true } | Turn isolation on/off. Authorized by Authorization: Bearer <dev session>. |
The change takes effect immediately — no redeploy needed.
Custom domains
Section titled “Custom domains”Serve your app on your own domain — app.yourcompany.com instead of
<slug>.apps.muhkoo.dev. Muhkoo issues and auto-renews the TLS certificate;
you keep your domain at whatever DNS provider you already use, and Muhkoo never
touches your DNS.
-
In the portal, open your app → Custom domains → enter your hostname (e.g.
app.yourcompany.com) → Add domain. -
The portal shows two DNS records. Add them at your DNS provider:
Type Name Value Why CNAME app.yourcompany.comcname.muhkoo.ioRoutes traffic to your app. CNAME _acme-challenge.app.yourcompany.com(shown in portal) One-time; lets Muhkoo issue + auto-renew the cert. Must be DNS-only — see below. The portal may also show a third,
TXTrecord that verifies you own the domain. Add it too if it appears. -
Muhkoo verifies ownership from those records, then the domain flips to Active and serves your app over HTTPS (usually a few minutes). It only goes live after verification — an unverified domain is never served. The cert renews itself from then on.
Root (apex) domains
Section titled “Root (apex) domains”The traffic record is a CNAME, and DNS doesn’t allow a CNAME on a root (apex)
domain — yourcompany.com with nothing in front. Most registrars, GoDaddy
included, reject it outright (“invalid record”). Use a subdomain and forward the
root to it:
-
Add
www.yourcompany.com(not the bareyourcompany.com) as the custom domain, and add the two records above for thewwwhost. -
At your registrar, turn on domain forwarding for the root:
yourcompany.com→https://www.yourcompany.com, 301 / permanent. On GoDaddy this is Domain → Forwarding (not a DNS record) — GoDaddy manages the root’s A records for you.
Now www.yourcompany.com serves your app directly and the bare yourcompany.com
redirects to it. Nothing leaves your registrar and no DNS transfer is needed.
From the terminal, muhkoo domains add <appId> app.yourcompany.com attaches a
domain and prints the DNS records to add, muhkoo domains ls <appId> shows each
domain’s status and certificate state, and muhkoo domains rm <appId> <hostname>
detaches it. Over the API, under /api/apps/:appId/hosting/domains
(Authorization: Bearer <app sk | dev session>): POST { hostname } to attach,
GET to list with live status + the records to add, DELETE /:hostname to detach.
- One site per app (the subdomain is the app’s slug, reserved to that app — two apps can never collide on a host).
- The hosted page calls
api.muhkoo.devwith your publishable key from<slug>.apps.muhkoo.dev; the default CORSallowedOrigins: "*"permits it. If you tighten CORS, add the hosting origin. - Rollback is a pointer flip — instant. The last 10 releases stay available to roll back to (or delete); the live release is never pruned.