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.
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;{ "name": "my-app", "main": "src/index.ts", "assets": "public" }Static sites
Specify only assets without main and the project deploys as a static site.
{ "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.
{ "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 deployA 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
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.
{ "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.
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, both of which every project has. If you turned either off, the static asset cache is used instead and a note appears in the deploy log. runlot.json is the only configuration file — no extra build config is generated. .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. Set the framework's server target to a workerd bundle — React Router, Astro, SvelteKit and Nuxt each ship one — and run its build.
npm run build{
"name": "my-app",
"main": "build/server/index.js",
"assets": "build/client",
"runWorkerFirst": false
}runWorkerFirst: false is the line that matters. runlot's default is worker-first: with main present, the worker is the entry point and static files are not served ahead of it. A framework's server build assumes the other order and never asks for its own files, so at the default a React Router build answers its own 404 page for /favicon.ico — a file sitting right there in build/client.
With runWorkerFirst: false, an exact file wins and everything else goes to the worker. You write no pass-through.
/favicon.ico is served from build/client.The same three keys work for any framework that builds a workerd server bundle — a single ESM file exporting fetch — with Astro (dist/server, dist/client), SvelteKit and Nuxt included. What runlot does not do is run your framework's build, so a build-time virtual module has to be resolved before runlot deploy sees it.
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.
{ "name": "my-app", "main": "src/index.ts", "triggers": { "crons": ["30 18 * * *"] } }