Início
Instalação
Monte o Runlight no Next.js, Nuxt, SvelteKit, Astro, Remix, Express, NestJS, Fastify, Koa, Hono, Bun, Deno ou Cloudflare Workers, ou encontre o guia da sua linguagem.
Toda instalação precisa de uma instância (veja Primeiros passos), das rotas dela montadas em /runlight e da tag do script nas suas páginas. Só as rotas mudam de um framework para outro.
rl.routes() retorna um handler Fetch, handler(request), e também a mesma função com o nome de cada método, então qualquer framework construído sobre Request e Response web pode usá-lo diretamente. Mude o caminho com rl.routes({ basePath: "/stats" }), e a tag do script passa a ser /stats/s.js.
Next.js
No App Router, adicione uma rota catch-all.
app/runlight/[[...path]]/route.ts
import { rl } from "@/lib/runlight";
export const { GET, POST, PUT, PATCH, DELETE, OPTIONS } = rl.routes();Adicione o script ao seu layout raiz.
app/layout.tsx
<head>
<script defer src="/runlight/s.js"></script>
</head>O Runlight precisa do runtime Node (ele usa SQLite, Postgres ou MySQL), que é o padrão dos route handlers.
Express e Node puro
@runlight/sdk/node adapta o handler ao módulo http do Node e, com isso, ao Express, Connect e Koa (por meio de ctx.req e ctx.res).
server.ts
import express from "express";
import { toNodeHandler, observer, shortLinks } from "@runlight/sdk/node";
import { rl } from "./runlight.js";
const app = express();
app.use(shortLinks(rl)); // short links at /go and on custom domains
app.use(observer(rl)); // AI agents reading your pages; optional
app.use(toNodeHandler(rl.routes().handler));Ele pode ir antes ou depois de qualquer body parser, porque o adaptador também lê um corpo que o Express já processou, e deixa sem ler os corpos das requisições do seu próprio app. shortLinks vai antes das suas próprias rotas, para que um domínio de links chegue primeiro ao Runlight.
NestJS
O Nest roda sobre o Express por padrão, então o mesmo middleware vai em main.ts, antes de listen.
src/main.ts
import { NestFactory } from "@nestjs/core";
import { toNodeHandler } from "@runlight/sdk/node";
import { AppModule } from "./app.module";
import { rl } from "./runlight";
const app = await NestFactory.create(AppModule);
app.use(toNodeHandler(rl.routes().handler));
await app.listen(3000);Adicione app.use(shortLinks(rl)) antes dele para os links curtos.
Fastify
O Fastify lê os corpos das requisições por conta própria, então dê ao Runlight um escopo de plugin próprio, sem body parsers, e entregue a ele a requisição bruta.
ts
import Fastify from "fastify";
import { toNodeHandler } from "@runlight/sdk/node";
import { rl } from "./runlight.js";
const app = Fastify();
const runlight = toNodeHandler(rl.routes().handler);
app.register(async (scope) => {
scope.removeAllContentTypeParsers();
scope.addContentTypeParser("*", (_req, _payload, done) => done(null));
scope.all("/runlight", (req, reply) => { reply.hijack(); return runlight(req.raw, reply.raw); });
scope.all("/runlight/*", (req, reply) => { reply.hijack(); return runlight(req.raw, reply.raw); });
});Os links curtos vão num hook onRequest, como mostra Links curtos.
Koa
ts
import Koa from "koa";
import { toNodeHandler } from "@runlight/sdk/node";
import { rl } from "./runlight.js";
const app = new Koa();
const runlight = toNodeHandler(rl.routes().handler);
app.use(async (ctx, next) => {
if (ctx.path !== "/runlight" && !ctx.path.startsWith("/runlight/")) return next();
ctx.respond = false;
await runlight(ctx.req, ctx.res);
});Para os links curtos, adicione o middleware mostrado em Links curtos antes deste.
Hono
ts
import { Hono } from "hono";
import { rl } from "./runlight";
const app = new Hono();
const runlight = rl.routes().handler;
app.all("/runlight/*", (c) => runlight(c.req.raw));
app.all("/runlight", (c) => runlight(c.req.raw));Nuxt e Nitro
Coloque a instância em server/utils/runlight.ts (o Nitro importa tudo o que está ali para você) e depois adicione esta rota.
server/routes/runlight/[...path].ts
export default defineEventHandler((event) => rl.routes().handler(toWebRequest(event)));Adicione o mesmo arquivo como server/routes/runlight.ts para que o próprio /runlight também responda. A tag do script vai em app.head, no nuxt.config.ts.
nuxt.config.ts
export default defineNuxtConfig({
app: { head: { script: [{ src: "/runlight/s.js", defer: true }] } },
});SvelteKit
src/routes/runlight/[...path]/+server.ts
import { rl } from "$lib/runlight";
const { handler } = rl.routes();
export const GET = ({ request }) => handler(request);
export const POST = GET;
export const PUT = GET;
export const PATCH = GET;
export const DELETE = GET;
export const OPTIONS = GET;Astro
O Astro precisa de um adapter para saída no servidor.
src/pages/runlight/[...path].ts
import { rl } from "../../lib/runlight";
export const prerender = false;
export const ALL = ({ request }) => rl.routes().handler(request);Remix e React Router
app/routes/runlight.$.ts
import { rl } from "~/runlight.server";
const { handler } = rl.routes();
export const loader = ({ request }) => handler(request);
export const action = ({ request }) => handler(request);Bun
O Bun não consegue carregar o better-sqlite3, então use o SQLite embutido dele por meio de @runlight/sdk/bun.
server.ts
import { runlight } from "@runlight/sdk";
import { bunSqlite } from "@runlight/sdk/bun";
const rl = runlight({ store: bunSqlite({ path: "./data/runlight.db" }), site: { hostnames: ["example.com"] } });
const runlightRoutes = rl.routes().handler;
Bun.serve({
port: 3000,
fetch(request) {
const { pathname } = new URL(request.url);
if (pathname === "/runlight" || pathname.startsWith("/runlight/")) return runlightRoutes(request);
return new Response("Your app");
},
});O Elysia e outros frameworks para Bun passam o request do mesmo jeito.
Deno
O Deno serve Requests web diretamente. Use libSQL (um arquivo local ou Turso) ou Postgres como store.
main.ts
import { runlight } from "npm:@runlight/sdk";
import { libsql } from "npm:@runlight/sdk/libsql";
import { createClient } from "npm:@libsql/client";
const rl = runlight({ store: libsql({ client: createClient({ url: "file:runlight.db" }) }), site: { hostnames: ["example.com"] } });
const runlightRoutes = rl.routes().handler;
Deno.serve((request) => {
const { pathname } = new URL(request.url);
if (pathname === "/runlight" || pathname.startsWith("/runlight/")) return runlightRoutes(request);
return new Response("Your app");
});Cloudflare Workers
Workers não têm disco, então guarde os números no D1. A localização vem dos próprios cabeçalhos da Cloudflare, sem configuração.
src/index.ts
import { runlight } from "@runlight/sdk";
import { d1 } from "@runlight/sdk/d1";
let routes: ReturnType<ReturnType<typeof runlight>["routes"]> | undefined;
export default {
fetch(request: Request, env: { DB: D1Database; RUNLIGHT_TOKEN: string }) {
// Made once per Worker instance, so the tables are checked when the instance starts.
routes ??= runlight({ store: d1({ database: env.DB }), site: { hostnames: ["example.com"] } }).routes({ token: env.RUNLIGHT_TOKEN });
return routes.handler(request);
},
};Um Worker guarda os segredos em env, que o Runlight não consegue ler sozinho, então passe o token para routes() como acima. Defina-o com wrangler secret put RUNLIGHT_TOKEN.
Roteie só /runlight/* para este Worker (ou verifique o caminho primeiro, como no exemplo do Bun) e agende a verificação de hora em hora com um Cron Trigger que chame rl.check().
Qualquer outro caso
Se o seu servidor entrega um Request web, passe-o para rl.routes().handler e retorne a Response. Se ele entrega req e res do Node, use toNodeHandler de @runlight/sdk/node.
Outras linguagens
O Runlight tem uma versão para cada uma destas linguagens, com o mesmo painel e as mesmas tabelas. Cada página traz a instalação, as rotas para os frameworks da linguagem, a verificação agendada e o que difere da biblioteca TypeScript.
| Linguagem | Instalação | Roda em |
|---|---|---|
| PHP | composer require runlight/runlight |
Laravel, Symfony, PHP puro e um drop-in para um domínio próprio |
| Python | pip install runlight |
Django, Flask, FastAPI e qualquer app WSGI ou ASGI |
| Rails | bundle add runlight |
Rails 7.2, 8.0 e 8.1, por meio de uma engine e um gerador |
| Ruby | bundle add runlight sqlite3 |
Sinatra, Hanami, Roda e qualquer outro app Rack |
| Go | go get runlight.sh/go |
net/http, chi, Echo e qualquer router que aceite um http.Handler |
| Java | sh.runlight:runlight do Maven Central |
O servidor do próprio JDK, servlet containers e Spring Boot |
| .NET | dotnet add package Runlight.AspNetCore |
ASP.NET Core e qualquer outro app .NET |
| Elixir | {:runlight, "~> 0.1"} em mix.exs |
Phoenix e qualquer app Plug |
| Rust | cargo add runlight runlight-sqlx |
axum, hyper e qualquer servidor que aceite um tower service |
Todas as linguagens, exceto Elixir e Rust, também têm um servidor independente próprio, e a página do servidor independente cobre o de Node.
Atrás de um proxy
O Runlight lê o endereço do visitante na última entrada de X-Forwarded-For, a que o seu proxy adiciona, e depois em X-Real-IP ou CF-Connecting-IP. Isso está certo atrás da Vercel, Netlify, Cloudflare e da maioria dos load balancers. Quando uma requisição passa por dois proxies, como a Cloudflare na frente do nginx, informe o cabeçalho que traz o endereço do próprio visitante, como trustProxy: "cf-connecting-ip". O endereço é usado para o hash diário do visitante e para descobrir a localização, e o limite de requisições o conta em hash. Ele nunca é guardado. Se o seu app fica exposto diretamente, sem proxy, defina trustProxy: false.