Cloudflare: Server-side
Server-side request lifecycle
Section titled “Server-side request lifecycle” 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.
Phaze Transport
Section titled “Phaze Transport”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) | |
|---|---|---|
| Authoring | src/actions/index.ts + defineAction | src/transport/actions.ts OR src/actions.ts + newAction (identical surface) |
| Server import | import { defineAction, ActionError } from 'astro:actions' | import { newAction, ActionError } from '@madenowhere/phaze-cloudflare/actions' |
| Client surface | import { actions } from 'astro:actions' | import { actions } from 'phaze:transport' (the typed proxy) + import { useAction } from '@madenowhere/phaze-cloudflare/actions' (the reactive wrapper) |
| Wire format | devalue (handles Date, Map, Temporal) — ~3 KB brotli of client runtime | plain JSON — 0 B; actions.X(input) per-call-site compile-inlines a fetch arrow |
newAction runtime cost | wrapper function is callable runtime code | compile-stripped to the bare object literal |
ActionError runtime cost | class instance + serialiser | compile-stripped to plain throw { type: 'PhazeActionError', code, ... } — dispatcher matches by err.type |
| Per-action middleware | n/a (use the global request middleware) | use: [...] array composing sequentially before the handler |
| Reactive consumption | n/a — actions.X(input) returns a plain Promise | useAction(actions.X) returns { pending, error, data, phase, execute, abort } signals — phase is a MatchSignal ('idle' | 'pending' | 'success' | 'error'); pending is the boolean alias for phase.is('pending') |
| Cancellation | not built in | useAction(...).abort() + dedupe: 'cancel-prior' | 'parallel' via AbortController plumbing (default 'cancel-prior') |
useAction field DCE | n/a — fixed shape | shipped — 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/.jsfile 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.tsroutes 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' } }).
Middleware (src/middleware.ts)
Section titled “Middleware (src/middleware.ts)”| Astro | phaze-cloudflare | |
|---|---|---|
| Export shape | onRequest(ctx, next) | onRequest(ctx, next) (identical) |
| Discovery | Astro integration scans src/middleware.ts | plugin scans src/middleware.ts |
| Absent middleware cost | Astro inserts a noop middleware | generated entry passes middleware: null; handleRequest short-circuits — zero overhead per request |
| Shared cookies | yes | same Cookies instance flows through middleware, action handler, page loader, endpoint — pre- AND post-next() writes both reach the response |
| URL parsing | re-parsed per access | parsed once on the per-request MiddlewareContext.url |
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}Cookies
Section titled “Cookies”Identical surface to Astro — .get(name) / .set(name, value, opts) / .delete(name) — with three behavioural improvements:
- Lazy parsing —
.get()only parses the inboundCookieheader on first read. Requests that don’t touch cookies pay zero parse cost. Astro parses eagerly. Partitioned(CHIPS) supported inCookieSetOptions.- 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.
Env (typed env vars)
Section titled “Env (typed env vars)”Astro (astro:env) | phaze-cloudflare (phaze:env) | |
|---|---|---|
| Schema location | env: { schema: { … } } in astro.config | src/env.ts with Env({ public, private }) — a real source file |
| Validators | envField.string({ context, access }) — Astro’s DSL | anything .parse(raw)-shaped — zod, valibot, ad-hoc validators all fit |
| Public env client cost | bundled astro:env/client virtual + value lookup | env.public.X inlined as literals at build — client bundle ships zero validator runtime |
| Server validation timing | eager at request | lazy on first property access via Proxy — cold-start cost is zero until the handler actually reads env.private.X |
| Type extraction | Astro-generated .astro/types.d.ts | plugin writes .phaze/types.d.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 importimport { 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)