runlot
Get started

Frameworks

Hono and static sites deploy without an adapter, and for Next.js the @runlot/next adapter handles the build.

The basic unit of a runlot deploy is a worker plus a static assets directory. Frameworks build on top of that.

Hono

No adapter is needed. A Hono app provides a fetch handler, so export default app is all it takes.

src/index.ts
import { Hono } from "hono";
import { Pool } from "@runlot/pg";

const app = new Hono<{ Bindings: Env }>();

app.get("/posts", async (c) => {
  const user = await c.env.auth.user(c.req.raw);
  const db = new Pool({ db: c.env.db });
  const { rows } = await db.query(
    "select id, title from posts where owner = $1 limit 20",
    [user?.id ?? null],
  );
  return c.json(rows);
});

export default app;
runlot.json
{ "name": "my-app", "main": "src/index.ts", "assets": "public" }

Static sites

Specify only assets without main and the project deploys as a static site.

runlot.json
{ "name": "my-site", "assets": "dist" }

/ and extension-less paths resolve to index.html, and Content-Type is set from the extension. If you use client-side routing (React Router and the like), add "notFound": "spa" — extension-less paths missing from the assets answer with /index.html (the same knob as wrangler's not_found_handling: "single-page-application").

runlot.json
{ "name": "my-site", "assets": "dist", "notFound": "spa" }

For static sites with a build step, such as Vite, Astro, or 11ty, run the build first and point assets at the output directory.

npm run build
runlot deploy

A static site is just a runlot deploy that contains nothing but static assets. If you add worker code later, you can keep deploying from the same project without moving services.

Next.js

Choosing the next template in npm create runlot@latest generates the setup below. Verification against the real runtime environment is still in progress. If you run into a problem during your first deploy, let us know.

Set "framework": "next" in runlot.json and runlot deploy runs next build and the OpenNext build. main and assets are generated during the build, so you don't set them yourself. The project must have next and @runlot/next installed.

runlot.json
{ "name": "my-app", "framework": "next" }

In Next.js code you use env, which doesn't require passing the request object around. The adapter takes care of the current request's context.

app/page.tsx
import { env } from "@runlot/next";
import { Pool } from "@runlot/pg";

export default async function Page() {
  const user = await env.auth.user();
  const db = new Pool({ db: env.db });
  const { rows } = await db.query("select id, title from posts limit 20");
  return <main>{user?.email} · {rows.length}</main>;
}

env is only available while a request is being handled. Reading it at the top level of a module, or during static rendering at build time, raises an error.

ISR uses env.storage and tag revalidation uses env.db. Declare both in runlot.json with "database": true and "storage": true. Without them, the static asset cache is used instead and a note appears in the deploy log. No separate config file (wrangler.toml) is created. .open-next/ and .runlot-next/ are build output and are already listed in the template's .gitignore.

Image optimization with next/image is not supported yet. Images are served as the original files.

Frameworks that build their own worker

runlot does not run your framework's build. runlot deploy bundles the file named by main with esbuild, so a build-time virtual module — virtual:react-router/server-build and the like — cannot be resolved from the source entry point.

Build first, then point main at the built worker. A framework whose server build comes out as a single ESM file exporting fetch needs no adapter from us. React Router's Cloudflare target produces exactly that shape.

npm run build
runlot.json
{ "name": "my-app", "main": "build/server/index.js", "assets": "build/client" }

One thing differs from wrangler: when main is present, the worker is the entry point and static assets are not served ahead of it. Serve them from your worker before handing the request to the framework — see Static assets.

const url = new URL(request.url);
if (url.pathname.startsWith("/assets/")) return env.assets.fetch(request);
return handler(request);
We have confirmed that a React Router SSR server build passes through the deploy bundler unchanged. A full deploy has not been verified end to end yet. If you run into a problem, let us know.

SvelteKit and Nuxt need an adapter that targets this shape, and one does not exist yet. A setup that only produces a client build (an SPA plus a separate API worker) can be deployed as a static site.

Scheduled execution

runlot.json takes triggers, and a worker's scheduled handler runs on that schedule — see Scheduled execution.

runlot.json
{ "name": "my-app", "main": "src/index.ts", "triggers": { "crons": ["30 18 * * *"] } }

On this page