A Fastify-like router for Service Workers that enables handling HTTP requests directly in service workers with a familiar, ergonomic API.
⚠️ Early Stage Project: This project is in very early development (started September 20, 2025). APIs may change, and some features might not be fully stable yet. Use with caution in production environments.
I'm a fan of this framework’s simplicity, and I really wanted an easy way to build CSR SPAs (Client-Side Rendered Single-Page Applications).
Give it a try — I'm sure you'll love it. Workerify works seamlessly with htmx.
There are certainly plenty of other use cases (I have a few in mind 😉), but I'd love to hear your ideas. Come share them with us on Discord.
- 🚀 Fastify-like API - Familiar routing patterns with
GET,POST,PUT,DELETE, etc. - 🔧 Vite Integration - First-class Vite plugin for seamless development
- 📡 BroadcastChannel Communication - Efficient message passing between main thread (or web worker) and service worker
- 🎯 Type-Safe - Full TypeScript support with comprehensive type definitions
- ⚡ Zero Dependencies - Lightweight core library with no runtime dependencies
- 🔄 Hot Route Replacement - Development mode with automatic route updates
- 🌐 Multi-Tab Support - Intelligent client isolation prevents interference between browser tabs
The example made with Workerify and htmx is available here
npx @workerify/create-htmx-app# Using pnpm
pnpm add @workerify/lib @workerify/vite-plugin
# Using npm
npm install @workerify/lib @workerify/vite-plugin
# Using yarn
yarn add @workerify/lib @workerify/vite-pluginAdd the Workerify plugin to your vite.config.ts:
import{defineConfig}from'vite';importworkerifyfrom'@workerify/vite-plugin';exportdefaultdefineConfig({plugins: [workerify()]});Or
import{defineConfig}from'vite';importworkerifyfrom'@workerify/vite-plugin';exportdefaultdefineConfig({plugins: [workerify({scope: '/',// Service worker scopeswFileName: 'sw.js'// Service worker file name})]});import{Workerify}from'@workerify/lib';constapp=newWorkerify({logger: true});// Simple GET routeapp.get('/api/hello',async(request,reply)=>{return{message: 'Hello from Service Worker!'};});// Route with parametersapp.get('/api/users/:id',async(request,reply)=>{constuserId=request.params.id;return{ userId,name: `User ${userId}`};});// POST route with body handlingapp.post('/api/users',async(request,reply)=>{constdata=request.body;reply.status=201;return{id: Date.now(), ...data};});// Prefix matching for catch-all routes (using wildcard syntax)app.get('/api/*',async(request,reply)=>{return{message: 'Catch-all API route',path: request.url};});// Start listening for requests (now async)awaitapp.listen();In your main application:
import{registerWorkerifySW}from'virtual:workerify-register';import{Workerify}from'@workerify/lib';// Register the service workerawaitregisterWorkerifySW();// Create and use your Workerify instanceconstapp=newWorkerify({logger: true});// ... add your routesawaitapp.listen();TypeScript Support: The virtual module types are automatically included when you install
@workerify/vite-plugin. If TypeScript doesn't recognize them, add this to yourvite-env.d.ts:/// <reference types="@workerify/vite-plugin/client" />
In your browser's console:
constbc=newBroadcastChannel("workerify");bc.postMessage({type:'workerify:routes:list'});Workerify automatically handles multiple browser tabs of the same application running simultaneously. Each tab operates in complete isolation:
- Consumer ID System: Each Workerify instance generates a unique consumer ID
- Client Mapping: Service worker maintains a map of client IDs to consumer IDs
- Request Routing: HTTP requests are routed to the correct tab based on the originating client
- Automatic Cleanup: Closed tabs are automatically cleaned up to prevent memory leaks
Check which tabs are registered and their consumer mappings:
constbc=newBroadcastChannel("workerify");bc.postMessage({type:'workerify:clients:list'});// Check console for detailed client informationThis ensures that opening multiple tabs of your application works seamlessly without interference between tabs.
constapp=newWorkerify(options?: WorkerifyOptions);Options:
logger?: boolean- Enable console logging (default:false)scope?: string- Service worker scope
// HTTP method routes (supports wildcard with /* suffix)app.get(path: string,handler: RouteHandler);app.post(path: string,handler: RouteHandler);app.put(path: string,handler: RouteHandler);app.delete(path: string,handler: RouteHandler);app.patch(path: string,handler: RouteHandler);app.head(path: string,handler: RouteHandler);app.option(path: string,handler: RouteHandler);// Note: singular 'option'// Route for all HTTP methods (supports wildcard with /* suffix)app.all(path: string,handler: RouteHandler);// Examples with wildcard:app.get('/api/*',handler);// Matches /api/users, /api/posts, etc.app.post('/admin/*',handler);// Matches all POST requests under /admin/// Generic route registration with explicit matching controlapp.route({method?: HttpMethod,// Optional: specific HTTP methodpath: string,// Route pathhandler: RouteHandler,// Request handler functionmatch?: 'exact'|'prefix'// Matching strategy (default: 'exact')});// Plugin registrationapp.register(plugin: WorkerifyPlugin,options?: any): Promise<Workerify>;interfaceWorkerifyRequest{url: string;method: HttpMethod;headers: Record<string,string>;body?: ArrayBuffer|null;params: Record<string,string>;// Route parameters}interfaceWorkerifyReply{status?: number;statusText?: string;headers?: Record<string,string>;body?: any;bodyType?: 'json'|'text'|'arrayBuffer';}typeRouteHandler=(request: WorkerifyRequest,reply: WorkerifyReply)=>Promise<any>|any;// Simple wildcard syntax - automatically enables prefix matchingapp.get('/static/*',async(request,reply)=>{consturl=newURL(request.url);constpath=url.pathname.replace('/static/','');constresponse=awaitfetch(`/assets/${path}`);reply.status=response.status;reply.headers=Object.fromEntries(response.headers);reply.bodyType='arrayBuffer';returnawaitresponse.arrayBuffer();});// Proxy all requests under /api/ to an external serviceapp.all('/api/*',async(request,reply)=>{consturl=newURL(request.url);consttargetUrl=url.pathname.replace('/api/','https://api.example.com/');constresponse=awaitfetch(targetUrl,{method: request.method,headers: request.headers,body: request.body});reply.status=response.status;reply.bodyType='json';returnawaitresponse.json();});// Alternative: Use route() for explicit controlapp.route({path: '/proxy/',match: 'prefix',// Explicitly set prefix matchinghandler: async(request,reply)=>{// Handle proxy logic}});constauthenticate=async(request: WorkerifyRequest)=>{consttoken=request.headers['authorization'];if(!token)thrownewError('Unauthorized');// Validate token...return{userId: '123'};};app.get('/api/protected',async(request,reply)=>{constuser=awaitauthenticate(request);return{message: `Hello ${user.userId}`};});This is a monorepo managed with pnpm workspaces. To contribute:
# Clone the repository
git clone https://github.com/yourusername/workerify.git
cd workerify
# Install dependencies
pnpm install
# Build all packages
pnpm build
# Run development mode
pnpm dev
# Run linting
pnpm lint
# Run type checking
pnpm typecheckworkerify/
├── packages/
│ ├── lib/ # Core Workerify library
│ ├── vite-plugin/ # Vite plugin
│ └── examples/ # Example applications
│ └── htmx/ # HTMX example app
├── package.json # Root package.json
├── pnpm-workspace.yaml
└── turbo.json # Turbo configuration
Workerify requires browsers that support:
- Service Workers
- BroadcastChannel API
- ES Modules
This includes all modern browsers (Chrome 66+, Firefox 57+, Safari 11.1+, Edge 79+).
MIT
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Inspired by Fastify's excellent API design and developer experience.