Skip to content
flypod
Concepts

How it works

flypod has two planes. A deploy pipeline ingests a zip into an immutable version, and a serve plane routes a subdomain to that version's files.

flypod is two planes. The deploy pipeline turns an uploaded zip into an immutable version and points a site at it. The serve plane maps an incoming request's subdomain to that site and returns its files. Everything else (versioning, TTL, ownership) hangs off these two.

The deploy pipeline

A deploy is a single POST /sites with a zip body (or a JSON map of file names to text). Before anything is read, the request passes the per-IP rate limit and, where one is configured, a bot challenge. flypod then processes it in order:

Ingest the zip. Files are unpacked under size and count caps (on flypod.dev: 200 files, 10 MB of files, a 12 MB upload), with a path-traversal guard so no entry can escape the site root. An empty upload is refused.

Render Markdown (if needed). If the upload has no index.html but does contain Markdown files, flypod renders them into a small styled site.

Fill in a missing index.html. If there is no root index.html but exactly one HTML file, that file is also served as the index, so a lone page works at /. Otherwise a missing index is reported as a missing-index warning.

Detect SPA. flypod inspects the site to decide whether it's a single-page app. This flag drives the serve-plane fallback (below).

Abuse scan. Contents are checked against abuse rules before anything is stored. A hard match refuses the deploy (403); a softer one publishes it with a shorter expiry.

Store an immutable version. The processed content is hashed and stored as a version that never changes. Every update is scanned the same way before it is stored.

Create or point the site. A new site is created (a folder's first deploy, or --new) or an existing one's live pointer is moved (flypod update, or flypod again in the same folder) to serve that version.

The response carries the live URL, the credentials (tokens), the expiry, anything flypod repaired during ingest (healed), and any warnings, each carrying a fix_recipe a caller can act on. It also carries a render key, which on flypod.dev is always {"status": "pending"} — the hosted service runs no headless browser, so there is no screenshot and no console capture. See the HTTP API for the full response shape.

The serve plane

Each site lives at https://<site_id>.flypod.page. The subdomain selects the site. That's the entire routing model. A request to / serves the site's /index.html.

How a path becomes a file

flypod resolves a request the way a static export expects:

RequestServed
/about.htmlabout.html
/aboutabout.html (clean URL)
/blog/blog/index.html
/blog301 to /blog/, when the file is blog/index.html
/about/301 to /about, when the file is about.html
anything elsethe site's own 404.html if it ships one, with a 404 status

The redirect, rather than serving the index in place, keeps relative URLs in the page working. A site's 404.html is used for missing pages; a missing asset such as /app.js gets a short plain 404, so a script never receives HTML.

Two domains, one rule

Deployed sites are served on flypod.page. The API, the docs, the marketing site, the account pages, and flypod's email all live on flypod.dev, which never serves uploaded content.

The rule has no exceptions: every site, anonymous or claimed, answers on <site_id>.flypod.page. Claiming a site removes its expiry; it does not move the site, so the URL you were handed is the URL you keep. The split is a design decision, not a caveat — a deploy requires no account, so keeping all uploaded content on a separate domain means one bad upload can damage the reputation of flypod.page only, and never the domain the API and docs depend on.

Whether a site is indexable is a separate question from which host serves it, and still follows ownership — see SEO and indexing.

Site URLs issued before 2026-09-14 used <site_id>.flypod.dev. Those 301 to the same id on flypod.page until 2026-12-13, after which they 404.

Responses are cacheable and revalidate cheaply:

ETag: W/"<live_version_id>:<path>"
cache-control: public, max-age=60, must-revalidate

The ETag is the live version id plus the path, so a request that matches gets a 304 Not Modified. Because the version id changes whenever the content does, a new version automatically invalidates old cache entries. The tag is weak (W/) for text, HTML, CSS, JavaScript, JSON and SVG, which the edge compresses, and strong for everything else (images, video, fonts), which byte ranges need.

Files whose name carries a build hash are different. app.3f9a2c1b.js or assets/index-BjK3x9aZ.css can never change under that name, so they are served to be kept for a year:

cache-control: public, max-age=31536000, immutable

The same applies to anything under a framework's content-addressed directory (_next/static/, _astro/, _app/immutable/, _nuxt/, _expo/static/, _build/). HTML is never cached this way, since it is what names the hashed files. A not-found page is sent with cache-control: no-cache, so it never outlives the deploy that adds the page.

Non-HTML files also answer byte-range requests (206 Partial Content), which is what lets <video> and <audio> play on Safari and iOS.

SPA fallback

If a request path has no file extension and no file matches, flypod falls back to serving index.html, but only if the live version was detected as a single-page app. This lets client-side routers handle /about without a real about.html. Static sites get a normal 404 for the same request.

Status codes

StatusWhen
200File found and served
206A byte range of a non-HTML file (Range)
301A trailing-slash redirect, see above, or a legacy flypod.dev site URL
304ETag matched (If-None-Match)
400A malformed path (bad percent-encoding, or a .. segment)
404Unknown host or site, or a missing file on a non-SPA site
405Any method other than GET or HEAD: a site is read-only
410 GoneThe site expired
416A byte range outside the file
451The site was disabled by an operator

SEO and indexing

flypod injects invisible attribution into served HTML: meta tags and JSON-LD, no visible badge. Anonymous sites (any site with an expiry) are additionally served with x-robots-tag: noindex, keeping ephemeral deploys out of search engines. So are not-found pages and a version's own timeline address.

It also fills in the link-preview tags a page lacks. Where a page has no og:title, flypod adds one from its <title> (or first <h1>); where it has no og:description, one from its meta description or first paragraph. When it adds either, it also adds og:type and twitter:card if they are missing, so the card actually shows. A pasted URL then unfurls with the page's own title and summary instead of a bare address. Tags you wrote are never changed, no title or description is made up, and <meta name="flypod" content="none"> turns all of it off.

llms.txt

Every site answers /llms.txt, the file an AI agent asks for to learn what a site contains without crawling it. If the deploy ships its own, that file is served unchanged. Otherwise flypod generates one from the site's HTML pages and their titles (up to 200 pages listed, titles read for the first 25). It always describes the live version, including straight after an update or a rollback (it is served cache-control: no-cache), and it adds nothing to a deploy: it is built the first time it is requested.

A browser that opens an address with no site gets a short page explaining that preview links last 14 days and how the owner keeps one. Expired sites are removed within minutes of expiring, so this is what an old temporary link shows. Anything that does not ask for HTML still gets the plain Not found.

On this page