laravel-workersv0.4.10
documentation

docs / Routes & ports

Routes & ports

Every route gets a decision before the build finishes: a compiled handler, a snapshot, a static file, a port — or a declared gap. Silence is a failed build.

What --strict enforces

The route list comes from Laravel itself (route:list, dumped at build time). For each route the compiler asks: can this handler be compiled? If yes, it is. If not, the route must be declared — a static file, a port, or an explicit ignore. An undeclared route fails the build; a declared one ships and is named in the verdict.

That is the whole guarantee. “Runs on the edge” never means “every route was compiled”: it means every route has a decision, and the ignored ones are printed where a reviewer can see them.

registry.json

One file at the app root, optional. init scaffolds it; the build reads it if present.

registry.jsonjson
{
  "autoSnapshot": true,
  "static":  { "GET /health": "public/health.html" },
  "ports":   { "POST /hooks/stripe": "stripe-webhook" },
  "ignore":  { "GET /admin/legacy": "served by a separate Worker" },
  "views":   {},
  "guards":  { "seal": "seal-guard" },
  "config":  ["needs"]
}
keymeaning
autoSnapshotrender clean GET routes at build time into static snapshots (the 0-invocation tier) unless a route opts out
staticroute → file in public/: served as an asset, never invokes the worker
portsroute → port name: the module in ports/<name>.mjs answers that route
ignoreroute → reason: declared away. It answers 404 at the edge and is listed in the READINESS block
guardsauth driver → port name, for a driver the app registers itself (Auth::extend)
configconfig groups the runtime may read. Without a declaration, a config('group.key') the build cannot answer fails loud

A port

A port is an ES module with a fetch(ctx) method — ordinary JavaScript at the edge, with access to the app's compiled bindings. The build imports it statically; dynamic module loading is forbidden in the Workers runtime.

ports/stripe-webhook.mjsjavascript
// ports/stripe-webhook.mjs
export default {
  async fetch(ctx) {
    const body = await ctx.request.text();
    if (!validSignature(body, ctx.app.env.STRIPE_SECRET)) {
      return new Response('bad signature', { status: 400 });
    }
    await ctx.app.db.prepare('insert into events (body) values (?)').bind(body).run();
    return new Response('ok');
  },
};

build --scaffold writes a 501 stub for every route that needs a port, so the gap becomes a file you can see instead of a surprise in production. A guard port is written by hand: it implements user(ctx), not fetch — seeauth & sessions.

Guard ports

A non-session guard driver the app registers itself (Auth::extend('seal', …)) is PHP the compiled rung cannot run — and answering “no user” would let a guarded route through. The refusal is therefore config-level, before routes are compiled: declaring the driver (guards → "seal": "seal-guard") and providing ports/seal-guard.mjswith async user(ctx) turns it green. The middleware, auth()->user(), the 401 and the redirect stay compiled; only the driver is yours.

When a route changes shape

If two routes would collide at the edge (same method and pattern after compilation), the build names both and suggests declaring one in registry.json. It does not pick a winner: the order a framework resolves routes in is part of its contract, and guessing it is exactly the class of silent difference this toolchain refuses.

Source: README.md §Compiles today, §A guard port; tools/init.mjs; tools/ports.mjs — repository ba1e9a5