How Workerify's Vite Plugin and Core Library Work Together: A Deep Dive

Building modern web applications with Service Workers can be complex. Workerify’s goal isn’t to simplify every aspect of Service Worker development, but rather to make it straightforward to create API-like endpoints directly in the browser. By providing a Fastify-like router that runs entirely inside a Service Worker, Workerify makes this specific use case both easy and powerful. In this post, we’ll explore Workerify’s architecture and how its Vite plugin and core library work hand in hand.

Architecture Overview

Workerify is made of two main packages:

  • @workerify/lib: the core routing library, handling request matching and processing

  • @workerify/vite-plugin: a Vite plugin that manages Service Worker registration and build-time optimizations

These two communicate through the BroadcastChannel API, letting your app define routes while the Service Worker intercepts and handles HTTP traffic.

The Journey of a Request

Let’s follow how a request flows through the system.

1. Initial Setup: The Vite Plugin

When you add the Workerify plugin to your Vite config:

// vite.config.ts
import workerify from '@workerify/vite-plugin';

export default defineConfig({
  plugins: [workerify()]
});

The plugin takes care of several things:

  • Generates a Service Worker file at /workerify-sw.js

  • Provides a virtual module (virtual:workerify-register) for easy registration

  • Handles both dev and build modes, served from memory in dev, emitted as a build asset in prod

2. Application Initialization

In your app code, you register the Service Worker and define routes:

import { registerWorkerifySW } from 'virtual:workerify-register';
import Workerify from '@workerify/lib';

await registerWorkerifySW();

const app = new Workerify({ logger: true });

app.get('/todos', async () => JSON.stringify(todos));

app.post('/todos', async (request, reply) => {
  todos.push(request.body?.todo);
  reply.headers = { 'HX-Trigger': 'todos:refresh' };
});

await app.listen();

3. The Registration Dance

When app.listen() runs:

  1. A unique consumer ID is created

  2. The instance registers with the Service Worker at /__workerify/register

  3. Routes are broadcast via BroadcastChannel

  4. The Service Worker acknowledges the registration

4. The Service Worker: Traffic Controller

The Service Worker, generated by the plugin, maps clients to consumers and routes. When a fetch event occurs, it looks for a matching route and, if found, hands off processing to the right consumer through the channel.

5. Request Pipeline

  1. Interception — Service Worker catches the request

  2. Route Matching — Finds the right handler

  3. Broadcast — Sends request details to the consumer

  4. Processing — Consumer runs the handler

  5. Response — Returned via BroadcastChannel

  6. Delivery — Service Worker replies to the browser

Multi-Tab Support

Service Workers are shared across tabs. Workerify ensures isolation by giving each tab its own consumer ID and routes. Closing a tab automatically cleans up its routes, avoiding conflicts between tabs.

The Communication Protocol

Workerify relies on a simple, well-defined protocol over BroadcastChannel:

  • Route registration:

{
  type: 'workerify:routes:update',
  consumerId, routes
}
  • Request handling:

{
  type: 'workerify:handle',
  id,
  consumerId,
  request
}
  • Response delivery:

{
  type: 'workerify:response',
  id,
  status,
  headers,
  body
}
  • Debugging:

{ type: 'workerify:routes:list' },
{ type: 'workerify:clients:list' }

Build-Time Optimizations

The Vite plugin improves both dev and production workflows:

  1. Pre-compiled templates for the Service Worker

  2. Virtual module generation for registration code

  3. Automatic base path handling

  4. Memory serving in dev for faster updates

Why This Architecture?

Unlike traditional Service Worker setups, Workerify removes boilerplate and gives developers:

  • A familiar API (Fastify-like)

  • Zero network latency, requests handled in-browser

  • SPA-friendly design, perfect with HTMX

  • Smooth dev experience, hot reload and route updates without reinstalling the SW

  • Full TypeScript support

Performance Considerations

  • Minimal overhead with BroadcastChannel

  • Lazy route matching only when needed

  • Automatic cleanup of closed tabs

  • Memory efficiency by storing routes once in the Service Worker

Practical Example: A Todo App

import { registerWorkerifySW } from 'virtual:workerify-register';
import Workerify from '@workerify/lib';
import htmx from 'htmx.org';

await registerWorkerifySW();

const app = new Workerify({ logger: true });
const todos: string[] = [];

app.get('/todos', async () =>
  `<ul>${todos.map(t => `<li>${t}</li>`).join('')}</ul>`
);

app.post('/todos', async (request, reply) => {
  todos.push(request.body?.todo);
  reply.headers = { 'HX-Trigger': 'todos:refresh' };
  return { success: true };
});

await app.listen();
htmx.trigger('#app', 'workerify-ready');

Start now

Getting started with Workerify is straightforward. You can scaffold a new project in seconds with:

npx @workerify/create-htmx-app

This will generate a ready-to-use setup with Vite, HTMX, and Workerify so you can start experimenting right away.

Conclusion

Workerify makes Service Worker routing simple and powerful by:

  • Separating route definition (lib) from SW management (plugin)

  • Using BroadcastChannel for fast communication

  • Handling multi-tab isolation automatically

  • Offering a familiar, developer-friendly API

This design opens the door to offline-first apps, zero-latency SPAs, and new client-side architectures. Workerify is still evolving, but it’s a useful starting point for experimenting with Service Workers in new ways — and it can even serve as a stepping stone toward simpler application models, especially those built with HTMX.

👉 Try it out, experiment with your own projects, and feel free to share your feedback and experiences with me, I’d love to hear how you use Workerify! A fresh Discord is available if you’d like to join and share with me 😁

comments powered by Disqus