Astro SSR on Cloudflare Pages is a convenient way to render pages on the server, but there’s a catch. Environment variables via the usual import.meta.env aren’t directly accessible at runtime and they can only be obtained from locals.runtime.env at the request handler level. To use them in nested functions, you’d have to pass them through props, which is inconvenient, considered an anti-pattern, and will eventually give you a headache.
In this article, we’ll explore two ways to solve this: using middleware (recommended) and a custom context wrapper.
Approach 1: Middleware
Concept
- We create a middleware that wraps each incoming request.
- Inside the middleware, we extract the runtime environment variables from locals.runtime.env.
- We store these variables in an AsyncLocalStorage instance, which provides a request-scoped context.
- Anywhere in the code, including deeply nested functions or service modules, you can access the environment variables using getRuntimeEnv().
Minimal example
src/app/runtimeContext.ts – Holds AsyncLocalStorage for request-scoped environment variables.
import { AsyncLocalStorage } from 'node:async_hooks';
// ENV type is defined in env.d.ts
export const asyncLocalStorage = new AsyncLocalStorage<ENV>();
export function getRuntimeEnv<K extends keyof ENV>(key: K): ENV[K] {
const env = asyncLocalStorage.getStore();
if (!env?.[key]) {
throw new Error(`Env "${String(key)}" is missing`);
}
return env[key];
};
src/middleware.ts – Middleware that sets up the runtime environment context per request.
import { defineMiddleware } from "astro:middleware";
import { asyncLocalStorage } from "./app/runtimeContext";
export const onRequest = defineMiddleware((context, next) => {
const runtimeEnv = context.locals.runtime.env;
if (!runtimeEnv) {
return next();
}
return asyncLocalStorage.run(runtimeEnv, () => {
return next();
});
});
src/pages/api/test.ts – Example API endpoint.
import { nestedFuncExample } from "src/app/nestedFuncExample";
import { getRuntimeEnv } from "src/app/runtimeContext";
export async function GET() {
const testVariable = getRuntimeEnv("TEST_VARIABLE");
nestedFuncExample();
return new Response(testVariable);
};
And we can access our variables at any level of nesting:
import { getRuntimeEnv } from "src/app/runtimeContext";
export const nestedFuncExample = () => {
const testVariable = getRuntimeEnv("TEST_VARIABLE");
console.log('testVariable: ', testVariable);
};
Approach 2: Custom Context Wrapper
Concept
- We create a custom wrapper for API handlers (withServerContext).
- Inside the wrapper, we build a context object from locals.runtime.env.
- The handler is then executed via serverStore.run(contextData, handler).
- Anywhere in the code, you can access this context using getServerContext().
Minimal example
src/app/serverContext.ts – Wraps API handlers and provides a request-scoped context using AsyncLocalStorage.
import type { APIContext } from "astro";
import { serverStore } from "src/app/serverStore";
export function withServerContext(handler: () => Promise<Response>) {
return async (context: APIContext) => {
// Extract environment variables for the current request
const { TEST_VARIABLE } = context.locals.runtime.env;
const contextData = {
testVariable: TEST_VARIABLE
};
// Run the handler inside AsyncLocalStorage context
return serverStore.run(contextData, handler);
};
};
src/app/serverStore.ts – Holds the AsyncLocalStorage instance and provides access to the current request context.
import { AsyncLocalStorage } from 'node:async_hooks';
type ContextData = {
testVariable: string;
};
// Get the current request context from AsyncLocalStorage
export function getServerContext(): ContextData {
const store = serverStore.getStore();
if (!store) {
throw new Error("Server context not initialized");
}
return store;
};
// AsyncLocalStorage instance to hold request context
export const serverStore = new AsyncLocalStorage<ContextData>();
src/pages/api/test.ts – Example API endpoint.
import { withServerContext } from "src/app/serverContext";
import { nestedFuncExample } from "src/app/nestedFuncExample";
import { getServerContext } from "src/app/serverStore";
export const GET = withServerContext(async () => {
const { testVariable } = getServerContext();
nestedFuncExample();
return new Response(testVariable);
});
And we can access our variables at any level of nesting:
export const nestedFuncExample = () => {
const { testVariable } = getServerContext();
console.log('testVariable: ', testVariable);
};
Deployment notes
Important: Both approaches require the disable_nodejs_process_v2 flag
in wrangler.json:
{
"compatibility_flags": [
"nodejs_compat",
"disable_nodejs_process_v2"
]
}
At the time of writing, this flag ensures that AsyncLocalStorage works correctly on Cloudflare Pages until full support is stabilized.
Summary
So, we explored two approaches to make Cloudflare runtime environment variables accessible deep in your Astro SSR code: using middleware and a custom context wrapper, both leveraging AsyncLocalStorage. I recommend using the first approach (middleware), as it’s simpler and integrates naturally with Astro. The second approach is provided mainly to demonstrate an alternative method and could be useful if you ever face limitations with middleware.
At first glance, creating a request-scoped context for every request might seem excessive. However, in a serverless environment like Cloudflare Pages, this is a safe and practical way to make runtime environment variables accessible anywhere in your code.
With some adjustments, this approach can also be adapted to other serverless providers that support AsyncLocalStorage.
Check out the full working examples on GitHub:
Top comments (0)