Universal WebSocket support for Astro SSR Apps with pre-patched adapters - no manual patching required!
ZAstroWebsockets is a monorepo containing WebSocket-enabled Astro adapters for Node.js and Cloudflare Workers. Each adapter is distributed as a separate package with WebSocket support built-in, eliminating the need for manual patching.
- ✅ Pre-patched adapters: Ships with WebSocket-enabled versions of Astro adapters
- ✅ Multi-runtime support: Node.js and Cloudflare Workers
- ✅ Works with current Astro versions (v4 & v5)
- ✅ Unified API: Same WebSocket API across all runtimes
- ✅ TypeScript support: Full type safety and IntelliSense
- ✅ Drop-in replacement: Simply replace your adapter import
- ✅ Monorepo structure: Separate packages for each runtime
- ✅ Auto-versioning: Synced with upstream Astro submodule
Install the specific adapter package you need:
npm install zastro-websockets-nodenpm install zastro-websockets-cloudflareReplace your existing @astrojs/node import with the WebSocket-enabled version:
// astro.config.mjs
import { defineConfig } from "astro/config"
import node from "zastro-websockets-node"
export default defineConfig({
output: "server",
adapter: node({ mode: "standalone" }),
});Replace your existing @astrojs/cloudflare import with the WebSocket-enabled version:
// astro.config.mjs
import { defineConfig } from "astro/config"
import cloudflare from "zastro-websockets-cloudflare"
export default defineConfig({
output: "server",
adapter: cloudflare(),
});The WebSocket API is consistent across all runtimes:
// src/pages/api/ws.ts
import type { APIRoute } from "astro"
export const GET: APIRoute = (ctx) => {
// Check if this is a WebSocket upgrade request
if (ctx.locals.isUpgradeRequest) {
// Upgrade the connection to a WebSocket
const { response, socket } = ctx.locals.upgradeWebSocket()
// Set up your WebSocket handlers
socket.onopen = () => {
console.log("WebSocket connection opened")
}
socket.onmessage = (event) => {
console.log("Received:", event.data)
if (event.data === "ping") {
socket.send("pong")
}
}
socket.onclose = () => {
console.log("WebSocket connection closed")
}
socket.onerror = (error) => {
console.error("WebSocket error:", error)
}
// Return the upgrade response
return response
}
// Return error for non-WebSocket requests
return new Response("Upgrade required", { status: 426 })
}<!-- src/pages/index.astro -->
<script>
const ws = new WebSocket("ws://localhost:4321/api/ws")
ws.onopen = () => {
console.log("Connected to WebSocket")
ws.send("Hello, Server!")
}
ws.onmessage = (event) => {
console.log("Received:", event.data)
}
ws.onclose = () => {
console.log("Disconnected from WebSocket")
}
</script>This is a monorepo containing:
zastro-websockets/
├── packages/
│ ├── node/ # zastro-websockets-node package
│ └── cloudflare/ # zastro-websockets-cloudflare package
├── tests/projects/ # Test projects for both adapters
├── patches/ # Patch files for upstream Astro adapters
└── scripts/ # Build and sync scripts
zastro-websockets-node- WebSocket-enabled Node.js adapterzastro-websockets-cloudflare- WebSocket-enabled Cloudflare Workers adapter
Both packages provide full TypeScript support with proper type definitions:
// Types are automatically available in your Astro.locals
declare global {
namespace App {
interface Locals {
isUpgradeRequest: boolean
upgradeWebSocket(): { socket: WebSocket, response: Response }
runtime?: any // Runtime-specific context (Cloudflare only)
}
}
}Instead of maintaining fragile patch files, ZAstroWebsockets uses a modern dynamic build system that applies WebSocket modifications as code transformations directly to the upstream Astro source code.
The Node.js adapter build system performs live code modifications to integrate WebSocket support:
-
Copy Upstream Source
- Copies fresh source from
astro-upstream/packages/integrations/node/ - Maintains sync with latest Astro releases
- Copies fresh source from
-
Apply Code Transformations
- Modifies
serve-app.ts: Adds WebSocket upgrade handling - Modifies
standalone.ts: Integrates WebSocket server with HTTP server - Modifies
types.ts: Exports WebSocket types - Modifies
index.ts: Updates package name references - Creates WebSocket Files: Complete WebSocket implementation
- Modifies
-
WebSocket Server Integration
// Dynamically injected into serve-app.ts const wsServer = new ws.WebSocketServer({ noServer: true }) // Handles HTTP upgrade requests httpServer.on('upgrade', (req, socket, head) => { wsServer.handleUpgrade(req, socket, head, (ws) => { attach(websocket, ws, metadata) }) })
-
Generated WebSocket Files
websocket/index.ts: Main WebSocket wrapper with browser-compatible APIwebsocket/stats.ts: Connection tracking and statisticswebsocket/connection-manager.ts: Advanced connection lifecycle managementwebsocket/attach.ts: WebSocket attachment utilitieswebsocket/dev-middleware.ts: Development server integrationwebsocket/serve-websocket.ts: Production server integration
-
Package Configuration
- Adds Dependencies:
ws@^8.18.0and@types/ws@^8.5.12 - Adds Exports:
./websocketand./statsexports - Resolves Workspace Deps: Converts
@astrojs/internal-helpers@workspace:*to actual versions
- Adds Dependencies:
The Cloudflare adapter uses the same dynamic transformation approach:
-
Copy Upstream Source
- Copies fresh source from
astro-upstream/packages/integrations/cloudflare/ - Maintains compatibility with Cloudflare Workers runtime
- Copies fresh source from
-
Apply Code Transformations
- Modifies
index.ts: Updates package name references - Creates WebSocket Files: Cloudflare Workers-specific implementation
- Modifies
-
WebSocket Implementation
// Dynamically generated in websocket/index.ts export function upgradeWebSocket(): WebSocketUpgrade { // Uses Cloudflare's WebSocketPair API const { 0: client, 1: server } = new WebSocketPair() // Create upgrade response const response = createWebSocketResponse(client) // Return wrapped server socket return { socket: new CloudflareWebSocket(server), response } }
-
Generated WebSocket Files
websocket/index.ts: Main WebSocket implementation for Cloudflare Workerswebsocket/response.ts: HTTP upgrade response handlingwebsocket/websocket.ts: WebSocket wrapper classeswebsocket/dev-middleware.ts: Development server simulation
-
WebSocket Wrapper Classes
CloudflareWebSocket: Production wrapper for native WebSocketDevWebSocket: Development simulation with same APIUpgradeResponse: Proper HTTP 101 upgrade response
The build system uses 3 main scripts:
// scripts/dynamic-build.ts - Main orchestrator
export function buildAll() {
// 1. Clean and prepare upstream
// 2. Install upstream dependencies
// 3. Build Node.js adapter
// 4. Build Cloudflare adapter
// 5. Copy README.md to packages
}
// scripts/dynamic-build-node.ts - Node.js specific
export function buildNodeAdapter() {
// 1. Copy upstream source
// 2. Apply all WebSocket modifications
// 3. Update package.json and resolve dependencies
// 4. Build TypeScript locally
}
// scripts/dynamic-build-cloudflare.ts - Cloudflare specific
export function buildCloudflareAdapter() {
// 1. Copy upstream source
// 2. Apply WebSocket modifications
// 3. Update package.json
// 4. Build TypeScript locally
}The build system uses adapter-specific versioning:
// GitHub Actions workflow
const nodeAdapterVersion = require('./packages/integrations/node/package.json').version
const cloudflareAdapterVersion = require('./packages/integrations/cloudflare/package.json').version
const ourCommit = process.env.GITHUB_SHA.substring(0, 7)
// Version format: 8.3.4-abc1234 (adapter version + our commit)
const finalVersion = `${nodeAdapterVersion}-${ourCommit}`| Aspect | Old Patch System | New Dynamic System |
|---|---|---|
| Reliability | Patches break on updates | Code transformations adapt |
| Maintenance | Manual patch updates | Automated code generation |
| Debugging | Patch application errors | Clear TypeScript errors |
| Dependency Management | Workspace conflicts | Isolated local dependencies |
| Build Speed | Slow patch application | Fast code transformations |
| Flexibility | Limited to patch changes | Full code control |
- import node from "@astrojs/node"
+ import node from "zastro-websockets-node"- import cloudflare from "@astrojs/cloudflare"
+ import cloudflare from "zastro-websockets-cloudflare"That's it! No other changes needed.
| Feature | Manual Patching | zastro-websockets |
|---|---|---|
| Setup complexity | High (manual patch application) | Low (just change import) |
| Reliability | Depends on patch compatibility | High (pre-tested combinations) |
| Updates | Manual re-patching required | Automatic with package updates |
| Maintenance | User responsibility | Package maintainer responsibility |
| Risk | Patches might break on updates | Version-locked compatibility |
| Connection Management | Manual implementation | Built-in tracking and cleanup |
- ✅ Node.js (
zastro-websockets-node) - Production ready - ✅ Cloudflare Workers (
zastro-websockets-cloudflare) - Production ready
Note: The Deno adapter is no longer supported as it has been moved to the Deno organization. Please use the official Deno adapter for Deno deployments.
- Ensure you're using the correct adapter package (
zastro-websockets-nodeorzastro-websockets-cloudflare) - Check that your hosting environment supports WebSockets
- Verify the WebSocket URL matches your deployment
- Make sure to import the adapter from the correct package name
- Update your
tsconfig.jsonto include the package types - Restart your TypeScript server after installation
- The adapters work in both development and production
- Additional logging is available in development mode
- Make sure to set
output: "server"in your Astro config
- If you're upgrading from the old single package, uninstall
zastro-websocketsfirst - Install the specific adapter package you need (
zastro-websockets-nodeorzastro-websockets-cloudflare) - Update your import statements to use the new package names
When using the Cloudflare adapter, you get access to the runtime context:
export const GET: APIRoute = (ctx) => {
if (ctx.locals.isUpgradeRequest) {
const { response, socket } = ctx.locals.upgradeWebSocket()
// Access Cloudflare runtime features
const env = ctx.locals.runtime?.env
const cf = ctx.locals.runtime?.cf
// Use Cloudflare-specific features
const kv = env.MY_KV_NAMESPACE
const country = cf.country
return response
}
return new Response("Upgrade required", { status: 426 })
}The Node.js adapter provides standard WebSocket functionality with full Node.js compatibility.
The Node.js adapter includes a powerful ConnectionManager for production-grade WebSocket applications:
import { ConnectionManagerAPI } from 'zastro-websockets-node/connection-manager';
// Get connection statistics
const stats = ConnectionManagerAPI.getStats();
console.log(`Active connections: ${stats.totalManagedConnections}`);
// Perform health checks
const healthResults = await ConnectionManagerAPI.healthCheck();
// Close connections by criteria
const closedCount = ConnectionManagerAPI.closeConnections({
idleMoreThan: 300000, // Close connections idle for more than 5 minutes
tags: ['temporary'] // Close connections tagged as temporary
});import { getConnectionManager } from 'zastro-websockets-node/connection-manager';
const manager = getConnectionManager({
maxConnections: 1000, // Global connection limit
maxConnectionsPerIP: 10, // Per-IP connection limit
idleTimeout: 300000, // 5 minutes idle timeout
rateLimitWindow: 60000, // 1 minute rate limit window
rateLimitMaxConnections: 5, // Max 5 connections per IP per minute
enableHealthMonitoring: true, // Enable automatic health checks
customCleanupPolicy: (connection) => {
// Custom logic for connection cleanup
return connection.tags.has('temporary') && connection.age > 300000;
}
});
// Event listeners for monitoring
manager.on('connection:added', (connection) => {
console.log(`New connection: ${connection.id}`);
});
manager.on('pool:full', (rejected) => {
console.warn(`Connection pool full, rejected: ${rejected.ip}`);
});
manager.on('ratelimit:exceeded', (ip, attempts) => {
console.warn(`Rate limit exceeded for ${ip}`);
});// In your WebSocket route
socket.addEventListener('open', () => {
const connectionId = getConnectionId(socket);
if (connectionId) {
// Add tags for grouping
manager.addConnectionTag(connectionId, 'user-session');
manager.addConnectionTag(connectionId, 'real-time-updates');
// Store custom metadata
manager.setConnectionData(connectionId, 'userId', user.id);
manager.setConnectionData(connectionId, 'sessionStart', Date.now());
}
});
// Later, find connections by criteria
const userSessions = manager.getConnectionsByTag('user-session');
const temporaryConnections = manager.getConnectionsByTag('temporary');// Manual health check
const result = await manager.performHealthCheck(connectionId);
if (!result.healthy) {
console.warn(`Connection ${connectionId} is unhealthy: ${result.error}`);
}
// Automatic background health monitoring
manager.on('connection:health', (connectionId, result) => {
if (!result.healthy) {
console.warn(`Health check failed for ${connectionId}: ${result.error}`);
}
});// Graceful shutdown with timeout
await ConnectionManagerAPI.shutdown({
timeout: 10000, // 10 second timeout
closeCode: 1001, // WebSocket close code
closeReason: 'Server shutting down'
});Check out the test projects for complete working examples:
/tests/projects/node- Node.js WebSocket implementation/tests/projects/cloudflare- Cloudflare Workers WebSocket implementation/example-usage.md- Detailed usage guide with examples/example-websocket.ts- Basic WebSocket server example/example-cloudflare-websocket.ts- Cloudflare-specific example/example-connection-management.ts- Basic connection tracking and cleanup/example-advanced-connection-management.ts- Advanced connection management with pooling, rate limiting, and health monitoring
Contributions are welcome! Please:
- Fork the repository
- Create your feature branch
- Add tests for your changes
- Update documentation
- Submit a pull request
git clone https://github.com/zachhandley/zastro-websockets.git
cd zastro-websockets
git submodule update --init --recursive
pnpm install
pnpm run buildThis project uses a dynamic build system that automatically applies WebSocket patches to upstream Astro adapters. Here's how it works:
zastro-websockets/
├── astro-upstream/ # Git submodule of official Astro repo
├── packages/
│ ├── node/ # Built Node.js adapter (generated)
│ │ ├── dist/ # Built TypeScript files
│ │ ├── src/ # Copied & modified upstream source
│ │ └── package.json # Modified package.json
│ └── cloudflare/ # Built Cloudflare adapter (generated)
│ ├── dist/ # Built TypeScript files
│ ├── src/ # Copied & modified upstream source
│ └── package.json # Modified package.json
├── patches/ # Legacy patch files (reference only)
├── scripts/ # Build automation scripts
│ ├── dynamic-build.ts # Main build orchestrator
│ ├── dynamic-build-node.ts # Node.js adapter build script
│ ├── dynamic-build-cloudflare.ts # Cloudflare adapter build script
│ └── reset-submodule.ts # Submodule management
└── tests/projects/ # Test projects for both adapters
The build system uses a 6-step process for each adapter:
- Copy Upstream Source → Copy source files from
astro-upstream/packages/integrations/[adapter]topackages/[adapter]/ - Apply Code Transformations → Apply all WebSocket modifications as live code transformations
- Update Package.json → Change package name, add WebSocket dependencies, resolve workspace dependencies
- Install Dependencies → Install dependencies in upstream workspace (with
--no-frozen-lockfile) - Build Upstream → Compile TypeScript in upstream workspace
- Copy Built Files → Copy compiled
dist/and modifiedpackage.jsonto final packages + README.md
Main Build Script: scripts/dynamic-build.ts
- Orchestrates the entire build process
- Cleans upstream and installs dependencies
- Runs both Node.js and Cloudflare builds sequentially
Node.js Build: scripts/dynamic-build-node.ts
- Copies upstream Node.js adapter source
- Applies all WebSocket modifications as code transformations
- Creates WebSocket files:
websocket/,middleware/ - Adds
wsdependency and WebSocket exports (./websocket,./stats) - Resolves workspace dependencies to actual versions
- Builds in upstream workspace, then copies to final package
Cloudflare Build: scripts/dynamic-build-cloudflare.ts
- Copies upstream Cloudflare adapter source
- Applies WebSocket modifications for Cloudflare Workers
- Creates WebSocket files and entrypoint modifications
- Adds WebSocket export to package.json
- Resolves workspace dependencies to actual versions
- Builds in upstream workspace, then copies to final package
# Full build process (recommended)
pnpm run build
# Test the Node.js adapter
pnpm run test:node
# Test the Cloudflare adapter
pnpm run test:cloudflare
# Test both adapters
pnpm run test
# Reset the astro-upstream submodule to latest Astro tag
pnpm run reset
# Publish packages to npm (CI only)
pnpm run publish:packagesThe project uses GitHub Actions for automated building and publishing:
Workflow: .github/workflows/auto-publish.yml
- Triggers: Push to main, daily at 2 AM UTC, or manual dispatch
- Process:
- Checks out latest Astro tag from submodule
- Installs dependencies with workspace filtering
- Builds both adapters with dynamic build system
- Tests both adapters
- Updates package versions to
{adapter-version}-{commit-hash} - Publishes to npm with automation tokens
- Commits built packages to repo
- Creates GitHub release with version tag
Version Strategy:
- Uses adapter-specific versions (not Astro core version)
- Format:
8.3.4-abc1234(Node adapter v8.3.4 + commit abc1234) - Allows patch releases for fixes without waiting for Astro updates
Workspace Dependency Resolution:
- Automatically converts
@astrojs/internal-helpers@workspace:*to actual versions - Maps packages to correct workspace locations
- Handles complex monorepo dependency resolution
README.md Distribution:
- Automatically copies root README.md to each package
- Ensures npm packages have proper documentation
Authentication & Publishing:
- Uses npm automation tokens for 2FA bypass
- Creates
.npmrcfiles in package directories - Publishes to npm with proper access controls
-
Make Changes to Build Scripts
- Edit
scripts/dynamic-build-node.tsfor Node.js changes - Edit
scripts/dynamic-build-cloudflare.tsfor Cloudflare changes
- Edit
-
Test Your Changes
pnpm run build pnpm run test -
Debug Build Issues
- Check
packages/node/andpackages/cloudflare/for generated files - Review TypeScript errors in build output
- Verify WebSocket files are created correctly
- Check workspace dependency resolution
- Check
-
Update Tests
- Test projects are in
tests/projects/ - Update test projects to match API changes
- Test projects are in
| Aspect | Old Patch System | New Dynamic System |
|---|---|---|
| Modification Method | Git patches | Code transformations |
| Build Location | Upstream directory | Upstream → Local packages |
| Dependency Management | Workspace conflicts | Automated workspace resolution |
| TypeScript Compilation | Upstream tsconfig | Upstream build → Copy dist |
| Maintenance | Manual patch updates | Automated code generation |
| Debugging | Patch application errors | Clear TypeScript errors |
| CI/CD | Manual publishing | Automated GitHub Actions |
To add new WebSocket features:
- Update the build scripts to include your new files/modifications
- Add the files to the appropriate
createWebSocketFiles()function - Update package.json exports if needed in
updateUpstreamPackageJson() - Test the build process with
pnpm run build - Update the test projects to demonstrate the new features
Build Fails with Workspace Dependency Errors:
- Check that workspace dependency mapping is correct
- Verify that upstream packages exist at expected paths
- Ensure
--no-frozen-lockfileis used in CI
Missing WebSocket Files:
- Check that
createWebSocketFiles()creates all necessary files - Verify file paths and exports are correct
- Ensure TypeScript compilation includes all source files
Publishing Fails with ENEEDAUTH:
- Verify npm automation token is stored as repository secret
- Check that
.npmrcfiles are created in package directories - Ensure registry-url is set correctly in GitHub Actions
Version Mismatch Issues:
- Verify that adapter versions are read from correct package.json paths
- Check that version update logic handles different adapter versions
- Ensure commit hash is properly extracted
This dynamic build system eliminates the need for manual patch management while providing automated CI/CD and reliable publishing to npm.
MIT - see LICENSE file for details.
This package is inspired by:
- gratelets - For the pre-patched adapter approach
- astro-node-websocket - Original WebSocket integration concept
- The Astro team for the excellent framework and adapter architecture
See CHANGELOG.md for version history and updates.