Skip to content

Cloudflare: Server-side

Astro (Cloudflare adapter) phaze-cloudflare
─────────────────────────── ─────────────────
request → Astro app entry generated worker entry
↓ ↓
middleware (src/middleware.ts) middleware.onRequest (same shape)
↓ ↓
Astro router (regex table) matchRoute (regex table)
↓ ↓
page renderer (Astro components) endpoint OR phaze SSR (renderToString + streaming)
↓ ↓
session + image + island manifest — nothing else —
↓ ↓
response ← bundled astro/runtime/server.js handleRequest returns Response (stream-first)

The 50 % worker delta is mostly: (a) no devalue (action wire format), (b) no session driver, (c) no image service, (d) no per-island manifest.

This is the cloudflare adapter’s surface of Phaze Transport — Phaze’s typed data + API layer. You define a capability with newAction and use it from the phaze:transport virtual module; the call site compile-inlines a fetch arrow. The reference page covers the full model — the declarative field stack (flatbuffer: / sign: / encrypt:), the binary wire, and the cross-runtime story. The table below compares the action surface against Astro’s.

Astro (astro:actions)phaze-cloudflare (phaze:transport)
Authoringsrc/actions/index.ts + defineActionsrc/transport/actions.ts OR src/actions.ts + newAction (identical surface)
Server importimport { defineAction, ActionError } from 'astro:actions'import { newAction, ActionError } from '@madenowhere/phaze-cloudflare/actions'
Client surfaceimport { actions } from 'astro:actions'import { actions } from 'phaze:transport' (the typed proxy) + import { useAction } from '@madenowhere/phaze-cloudflare/actions' (the reactive wrapper)
Wire formatdevalue (handles Date, Map, Temporal) — ~3 KB brotli of client runtimeplain JSON — 0 B; actions.X(input) per-call-site compile-inlines a fetch arrow
newAction runtime costwrapper function is callable runtime codecompile-stripped to the bare object literal
ActionError runtime costclass instance + serialisercompile-stripped to plain throw { type: 'PhazeActionError', code, ... } — dispatcher matches by err.type
Per-action middlewaren/a (use the global request middleware)use: [...] array composing sequentially before the handler
Reactive consumptionn/a — actions.X(input) returns a plain PromiseuseAction(actions.X) returns { pending, error, data, phase, execute, abort } signalsphase is a MatchSignal ('idle' | 'pending' | 'success' | 'error'); pending is the boolean alias for phase.is('pending')
Cancellationnot built inuseAction(...).abort() + dedupe: 'cancel-prior' | 'parallel' via AbortController plumbing (default 'cancel-prior')
useAction field DCEn/a — fixed shapeshipped — phaze-compile per-call-site walks references and emits an inline IIFE: USE_ACTION_LEAN_IIFE ({pending, execute} only) when the consumer reads only those fields, USE_ACTION_IIFE (full state machine) otherwise. The shared useAction runtime import drops on Program.exit when every call site rewrote.

Why this matters for bundle size: the action surface is types-only at runtime. newAction is a function call pre-compile, vanishes post-compile. ActionError is a class pre-compile, vanishes post-compile. Removing devalue is the single biggest line item on the client-bundle delta vs Astro’s actions. For form / RPC workloads, JSON is sufficient — Date survives as ISO string, Map is rare in HTTP payloads, Temporal is opt-in if needed.

Endpoints — src/api/*.ts + src/pages/<name>.<content-ext>.ts

Section titled “Endpoints — src/api/*.ts + src/pages/<name>.<content-ext>.ts”

Two kinds of endpoint discriminated by directory:

  • src/api/** — HTTP handlers (JSON APIs, webhooks). The directory IS the opt-in; every .ts/.js file becomes a route at URL /api/<file-path>. src/api/users.ts/api/users; src/api/webhooks/stripe.ts/api/webhooks/stripe. The /api/ URL prefix is baked in.
  • src/pages/<name>.<content-ext>.ts — content endpoints whose URL needs the filename’s content-ext suffix (robots.txt, sitemap.xml, feed.xml, og.png, etc.). The basename pattern <name>.<content-ext> IS the discriminator — robots.txt.ts routes to /robots.txt; helpers.ts (no content-ext) is ignored. Recognised content extensions: txt, xml, json, png, ico, webmanifest, rss, atom, svg.

Both expose export const GET = (ctx) => Response. Phaze-cloudflare’s context is slimmer:

interface EndpointContext<Bindings> {
params: Record<string, string>
env: Bindings
request: Request
ctx: ExecutionContext
cookies: Cookies
}

vs Astro’s APIContext, which carries locals / currentLocale / redirect / rewrite / session / params / cookies / request. Parity in capability; phaze’s smaller context means no locals-as-magic-bag — env and cookies are top-level fields. For redirect/rewrite, return a plain new Response(null, { status: 302, headers: { location: '/x' } }).

Astrophaze-cloudflare
Export shapeonRequest(ctx, next)onRequest(ctx, next) (identical)
DiscoveryAstro integration scans src/middleware.tsplugin scans src/middleware.ts
Absent middleware costAstro inserts a noop middlewaregenerated entry passes middleware: null; handleRequest short-circuits — zero overhead per request
Shared cookiesyessame Cookies instance flows through middleware, action handler, page loader, endpoint — pre- AND post-next() writes both reach the response
URL parsingre-parsed per accessparsed once on the per-request MiddlewareContext.url
src/middleware.ts
import type { MiddlewareHandler } from '@madenowhere/phaze-cloudflare'
export const onRequest: MiddlewareHandler<Env> = async (ctx, next) => {
if (!ctx.cookies.get('session')) return Response.redirect('/login', 302)
const response = await next()
response.headers.set('x-served-by', 'phaze')
return response
}

Identical surface to Astro — .get(name) / .set(name, value, opts) / .delete(name) — with three behavioural improvements:

  • Lazy parsing.get() only parses the inbound Cookie header on first read. Requests that don’t touch cookies pay zero parse cost. Astro parses eagerly.
  • Partitioned (CHIPS) supported in CookieSetOptions.
  • RFC 6265 first-instance-wins on duplicate names.

The Cookies instance is shared across middleware → action handler → page loader → endpoint, so cookies set anywhere in the request chain merge into the final response in one pass.

Astro (astro:env)phaze-cloudflare (phaze:env)
Schema locationenv: { schema: { … } } in astro.configsrc/env.ts with Env({ public, private }) — a real source file
ValidatorsenvField.string({ context, access }) — Astro’s DSLanything .parse(raw)-shaped — zod, valibot, ad-hoc validators all fit
Public env client costbundled astro:env/client virtual + value lookupenv.public.X inlined as literals at build — client bundle ships zero validator runtime
Server validation timingeager at requestlazy on first property access via Proxy — cold-start cost is zero until the handler actually reads env.private.X
Type extractionAstro-generated .astro/types.d.tsplugin writes .phaze/types.d.ts
src/env.ts
import { Env } from '@madenowhere/phaze-cloudflare/env'
import { z } from 'zod'
export default Env({
public: { // client-inlined → env.public.X
SITE_URL: z.string().url(), // sourced from .env's PUBLIC_SITE_URL
GA_ID: z.string().optional(), // sourced from .env's PUBLIC_GA_ID
},
private: { // server-only → env.private.X
DATABASE_URL: z.string().url(),
API_SECRET: z.string().min(32),
},
})
// src/pages/index.tsx — one unified import
import { env } from 'phaze:env'
// env.public.SITE_URL → inlined literal, safe everywhere (client + server)
// env.private.API_SECRET → worker-only (a throwing stub on the client)