Library
The flypod npm package deploys a folder of static files to a live URL from Node or Bun. Reference for deploySite, collectDist, resolveDeployDir, and the DeployOptions / DeployResult types.
flypod is a tiny, dependency-light npm package that turns a folder of static
files into a live URL from JS/TS. It's the same deploy primitive the
CLI wraps, exposed as functions you can call directly.
npm install flypod # or: bun add flypodEverything ships from one entry point. It's ESM-only and runs on Node 18+ and Bun:
import { deploySite, collectDist, resolveDeployDir, BUILD_DIR_CANDIDATES } from "flypod";
import type { DeployResult, DeployOptions } from "flypod";The mental model
A deploy is two steps: read files into a map, then ship the map.
const files = await collectDist("./dist"); // { "index.html": Uint8Array, ... }
const { url, manage_token } = await deploySite({ files });deploySite creates a new site every time it's called. Save the
manage_token it returns — that token is what lets you update or roll back the
site later.
This is the one place flypod tells you to keep a token, and the reason is
specific: the library has no registry. The CLI saves each
site's manage_token to projects.json (mode 0600) keyed by folder, which is
why it hides the token and why flypod login can claim every remembered site in
one step. Calling deploySite directly, you are that registry. Nothing else
holds what it returns, and the server keeps only a hash — so if you drop it, the
site can never be updated, rolled back, or claimed.
Two more things worth knowing up front:
- Deploys are anonymous by default (public, expires in ~14 days). Pass an account token to make a site owned and permanent — see Owned deploys.
- The library only deploys. There's no
update()orrollback()function. To ship a new version to an existing site or roll one back, use the HTTP API (POST /sites/:id/deploys) or the CLI with themanage_token.
Want to run something now? The TypeScript demo is a downloadable, working example you can deploy in one command.
deploySite
deploySite(opts: DeployOptions): Promise<DeployResult>Zips a files map, uploads it as a new site, and returns the live URL plus the
tokens you need to manage it. Throws Error("deploy failed: <status>") if
the server responds with a non-2xx status, so wrap it in try/catch if you want
to handle failures.
DeployOptions
| Field | Type | Required | Description |
|---|---|---|---|
files | Record<string, Uint8Array> | Yes | Relative path → file bytes. Build it with collectDist, or construct it by hand. |
apiKey | string | No | An account token. Pass it to make the deploy owned and permanent. See Owned deploys. |
baseUrl | string | No | API host to deploy against. Defaults to https://flypod.dev; override it to target a self-hosted server. |
handle | (req: Request) => Promise<Response> | No | Advanced: a custom transport used in place of global fetch (for tests, in-process calls, or a proxying fetch). Defaults to fetch. Most callers never set this. |
apiKey is a bit of a misnomer — it's sent verbatim as Authorization: Bearer <value>, so the value is an account session token (from flypod login or
FLYPOD_TOKEN), not a separate API key. The field name is kept for backward
compatibility. See Tokens & ownership.
DeployResult
| Field | Type | Description |
|---|---|---|
url | string | The live URL, e.g. https://ab12cd34.flypod.dev. Serves immediately. |
manage_token | string | Save this. Bearer token that authorizes updates, rollback, and reads for this one site. |
site_id | string | Stable site identifier. Used in HTTP API paths (/sites/:id/...). |
slug | string | The site's URL slug (the subdomain in url). |
version_id | string | Identifier of the version this deploy created — pass it to rollback later. |
expires_at | number | null | Epoch-ms expiry for anonymous deploys; null when the site is owned (never expires). |
owner_account_id | string? | The owning account's ID. Present only on owned (authenticated) deploys — the key is omitted, not null, on anonymous ones. |
claim_token | string? | Anonymous deploys only (omitted, not null, when owned). A fallback for claiming this site from a different machine via flypod claim. The usual login flow attaches sites with the manage_token and never needs it, and it can't re-point a site that already has a different owner (409). |
next_actions | unknown[] | Suggested follow-up actions returned by the server (informational). |
Example
import { deploySite, collectDist } from "flypod";
const files = await collectDist("./dist");
const result = await deploySite({ files });
console.log(result.url); // https://ab12cd34.flypod.dev — live now
console.log(result.manage_token); // save this to update / roll back latercollectDist
collectDist(path: string): Promise<Record<string, Uint8Array>>Reads files off disk into the { path → bytes } map that deploySite expects.
Pass a directory and it's walked recursively (paths are relative to the
directory, with / separators); pass a single file and you get a
one-entry map.
const site = await collectDist("./dist"); // whole folder
const one = await collectDist("./index.html"); // { "index.html": Uint8Array }You don't have to use collectDist — any Record<string, Uint8Array> works, so
you can generate files in memory (see the inline demo).
resolveDeployDir
resolveDeployDir(opts?: { cwd?: string; explicit?: string }): Promise<string>Figures out which folder to deploy so callers don't have to hardcode dist.
It's async. Behavior:
- With
explicitset, that path is returned as-is. - Otherwise it scans
BUILD_DIR_CANDIDATESin order (relative tocwd, defaultprocess.cwd()) and returns the first folder that exists and contains anindex.html. - If nothing matches, it throws — pass the folder explicitly in that case.
import { resolveDeployDir, collectDist, deploySite } from "flypod";
const dir = await resolveDeployDir(); // auto-detect, e.g. "/proj/dist"
const files = await collectDist(dir);
await deploySite({ files });BUILD_DIR_CANDIDATES
The ordered list resolveDeployDir searches. public ranks low because some
tools use it for source assets — requiring an index.html disambiguates a
built site from source.
["dist", "build", "out", ".output/public", ".vercel/output/static", "public", "_site"]Owned deploys
By default a deploy is anonymous: public, noindex, and it expires in ~14 days.
To make a site owned and permanent, pass an account session token as
apiKey. The standard source is the FLYPOD_TOKEN environment variable, which
flypod login and CI both use:
const result = await deploySite({
files,
apiKey: process.env.FLYPOD_TOKEN,
});
// result.expires_at === null, result.owner_account_id is setSee Tokens & ownership for how tokens and ownership fit together, and Accounts & claiming to attach sites you already deployed anonymously.