MCPcopy Index your code
hub / github.com/bikeshaving/shovel

github.com/bikeshaving/shovel @v0.2.18

Chat with this repo
repository ↗ · DeepWiki ↗ · release v0.2.18 ↗ · + Follow
1,536 symbols 3,929 edges 200 files 327 documented · 21%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Shovel.js

Run Service Workers anywhere.

Shovel is a meta-framework for building server applications using the ServiceWorker API. Write once, deploy to Node.js, Bun, or Cloudflare Workers.

// server.ts
import {Router} from "@b9g/router";

const router = new Router();

router.route("/kv/:key")
  .get(async (req, ctx) => {
    const cache = await self.caches.open("kv");
    const cached = await cache.match(ctx.params.key);
    return cached ?? new Response(null, {status: 404});
  })
  .put(async (req, ctx) => {
    const cache = await self.caches.open("kv");
    await cache.put(ctx.params.key, new Response(await req.text()));
    return new Response(null, {status: 201});
  })
  .delete(async (req, ctx) => {
    const cache = await self.caches.open("kv");
    await cache.delete(ctx.params.key);
    return new Response(null, {status: 204});
  });

self.addEventListener("fetch", (ev) => {
  ev.respondWith(router.handle(ev.request));
});
$ shovel develop server.ts
listening on http://localhost:7777

$ curl -X PUT :7777/kv/hello -d "world"

$ curl :7777/kv/hello
world

Quick Start

# Create a new project
npm create shovel my-app

# Development with hot reload
npx @b9g/shovel develop src/server.ts

# Build for production
npx @b9g/shovel build src/server.ts --platform=node
npx @b9g/shovel build src/server.ts --platform=bun
npx @b9g/shovel build src/server.ts --platform=cloudflare

Documentation

Visit shovel.js.org for guides and API reference.

Web Standards

Shovel is obsessively standards-first. All Shovel APIs use web standards, and Shovel implements/shims useful standards when they're missing.

API Standard Purpose
fetch() Fetch Networking
install, activate, fetch events Service Workers Server lifecycle
AsyncContext.Variable TC39 Stage 2 Request-scoped state
self.caches Cache API Response caching
self.directories FileSystem API Storage (local, S3, R2)
self.cookieStore CookieStore API Cookie management
URLPattern URLPattern Route matching

Your code uses standards. Shovel makes them work everywhere.

Meta-Framework

Shovel is a meta-framework: it generates bundles and compiles your code with ESBuild. You write code, and it runs in development and production with the exact same APIs. Shovel takes care of single file bundle requirements, and transpiling JSX/TypeScript.

True Portability

Same code, any runtime, any rendering strategy:

  • Server runtimes: Node.js, Bun, Cloudflare Workers
  • Browser ServiceWorkers: The same app can run as a PWA
  • Universal rendering: Dynamic, static, or client-side

The core abstraction is the ServiceWorker-style storage pattern. Globals provide a consistent API for common web concerns:

const cache  = await self.caches.open("sessions");     // Cache API
const dir    = await self.directories.open("uploads"); // FileSystem API
const db     = self.databases.get("main");             // Zen DB (opened on activate)
const logger = self.loggers.get(["app", "requests"]);  // LogTape

Each storage type is: - Lazy - connections created on first open(), cached thereafter - Configured uniformly - all are configured by shovel.json - Platform-aware - sensible defaults per platform, override what you need

This pattern means your app logic stays clean. Swap in Redis for caches, S3 for local filesystem, Postgres for SQLite - change the config, not the code.

Platform APIs

// Cache API - Request/Response-based caching
const cache = await self.caches.open("my-cache");
await cache.put(request, response.clone());
const cached = await cache.match(request);

// File System Access - storage directories (local, S3, R2)
const directory = await self.directories.open("uploads");
const file = await directory.getFileHandle("image.png");
const contents = await (await file.getFile()).arrayBuffer();

// Cookie Store - cookie management
const session = await self.cookieStore.get("session");
await self.cookieStore.set("theme", "dark");

// AsyncContext - request-scoped state without prop drilling
const requestId = new AsyncContext.Variable();
requestId.run(crypto.randomUUID(), async () => {
  console.log(requestId.get()); // Works anywhere in the call stack
});

Asset Pipeline

Import any file and get its production URL with content hashing:

import styles from "./styles.css" with {assetBase: "/assets"};
import logo from "./logo.png" with {assetBase: "/assets"};

// styles = "/assets/styles-a1b2c3d4.css"
// logo = "/assets/logo-e5f6g7h8.png"

At build time, Shovel: - Copies assets to the output directory with content hashes - Generates a manifest mapping original paths to hashed URLs - Transforms imports to return the final URLs

Assets are served via the platform's best option: - Node/Bun: Static file middleware or directory storage - Cloudflare: Workers Assets (edge-cached, zero config)

Configuration

Configure Shovel using shovel.json in your project root.

Philosophy

Shovel's configuration follows these principles:

  1. Platform Defaults, User Overrides - Each platform provides sensible defaults. You only configure what you want to change.

  2. Uniform Interface - Caches, directories, databases, and loggers all use the same { module, export, ...options } pattern. No magic strings or builtin aliases.

  3. Layered Resolution - For any cache or directory name:

  4. If config specifies module/export → use that
  5. Otherwise → use platform default

  6. Platform Re-exports - Each platform exports DefaultCache representing what makes sense for that environment:

  7. Cloudflare: Native Cache API
  8. Bun/Node: MemoryCache

  9. Transparency - Config is what you see. Every backend is an explicit module path, making it easy to debug and trace.

Basic Config

{
  "port": "$PORT || 7777",
  "host": "$HOST || localhost",
  "workers": "$WORKERS ?? 1",
  "caches": {
    "sessions": {
      "module": "@b9g/cache-redis",
      "url": "$REDIS_URL"
    }
  },
  "directories": {
    "uploads": {
      "module": "@b9g/filesystem-s3",
      "bucket": "$S3_BUCKET"
    }
  },
  "databases": {
    "main": {
      "module": "@b9g/zen/bun",
      "url": "$DATABASE_URL"
    }
  },
  "logging": {
    "loggers": [
      {"category": ["app"], "level": "info", "sinks": ["console"]}
    ]
  }
}

Caches

Configure cache backends using module (uses default export, or specify export for named exports):

{
  "caches": {
    "api-responses": {
      "module": "@b9g/cache/memory"
    },
    "sessions": {
      "module": "@b9g/cache-redis",
      "url": "$REDIS_URL"
    }
  }
}
  • Default: Platform's DefaultCache when no config specified (MemoryCache on Bun/Node, native on Cloudflare)
  • Pattern matching: Use wildcards like "api-*" to match multiple cache names
  • Empty config: "my-cache": {} uses platform default explicitly

Directories

Configure directory backends. Platforms provide defaults for well-known directories (server, public, tmp):

{
  "directories": {
    "uploads": {
      "module": "@b9g/filesystem-s3",
      "bucket": "MY_BUCKET",
      "region": "us-east-1"
    },
    "data": {
      "module": "@b9g/filesystem/node-fs",
      "path": "./data"
    }
  }
}
  • Well-known defaults: server (dist/server), public (dist/public), tmp (OS temp)
  • Custom directories: Must be explicitly configured

Logging

Shovel uses LogTape for logging:

const logger = self.loggers.get(["shovel", "myapp"]);
logger.info`Request received: ${request.url}`;

Zero-config logging: Use the ["shovel", ...] category hierarchy to inherit Shovel's default logging (info level to console). No configuration needed.

For custom configuration, use shovel.json:

{
  "logging": {
    "sinks": {
      "file": {
        "module": "@logtape/logtape",
        "export": "getFileSink",
        "path": "./logs/app.log"
      }
    },
    "loggers": [
      {"category": ["myapp"], "level": "info", "sinks": ["console"]},
      {"category": ["myapp", "db"], "level": "debug", "sinks": ["file"]}
    ]
  }
}
  • Console sink is implicit - always available as "console"
  • Category hierarchy - ["myapp", "db"] inherits from ["myapp"]
  • parentSinks - use "override" to replace parent sinks instead of inheriting

Databases

Configure database drivers using the same module/export pattern:

{
  "databases": {
    "main": {
      "module": "@b9g/zen/bun",
      "url": "$DATABASE_URL"
    }
  }
}

Open databases in activate (for migrations), then use get() in requests:

self.addEventListener("activate", (event) => {
  event.waitUntil(self.databases.open("main", 1, (e) => {
    e.waitUntil(runMigrations(e));
  }));
});

self.addEventListener("fetch", (event) => {
  const db = self.databases.get("main");
});

Expression Syntax

Configuration values support a domain-specific expression language that generates JavaScript code evaluated at runtime.

Environment Variables

$VAR                    → process.env.VAR
$VAR || fallback        → process.env.VAR || "fallback"
$VAR ?? fallback        → process.env.VAR ?? "fallback"

Bracket Placeholders

Placeholder Description Resolution
[outdir] Build output directory Build time
[tmpdir] OS temp directory Runtime
[git] Git commit SHA Build time

The bracket syntax mirrors esbuild/webpack output filename templating ([name], [hash]).

Operators

Operator Example Description
\|\| $VAR \|\| default Logical OR (falsy fallback)
?? $VAR ?? default Nullish coalescing
&& $A && $B Logical AND
? : $ENV === prod ? a : b Ternary conditional
===, !== $ENV === production Strict equality
! !$DISABLED Logical NOT

Path Expressions

Path expressions support path segments and relative resolution:

$DATADIR/uploads        → joins env var with path segment
[outdir]/server         → joins build output with path segment
./data                  → resolved to absolute path at build time

Example

{
  "port": "$PORT || 7777",
  "host": "$HOST || 0.0.0.0",
  "directories": {
    "server": { "path": "[outdir]/server" },
    "public": { "path": "[outdir]/public" },
    "tmp": { "path": "[tmpdir]" },
    "data": { "path": "./data" },
    "cache": { "path": "($CACHE_DIR || [tmpdir])/myapp" }
  },
  "cache": {
    "provider": "$NODE_ENV === production ? redis : memory"
  }
}

Dynamic values (containing $VAR or [tmpdir]) use getters to ensure evaluation at access time, not module load time.

Access in Code

import {config} from "shovel:config";
console.log(config.port); // Resolved value

Packages

Package Description
@b9g/shovel CLI for development and deployment
@b9g/router URLPattern-based routing with middleware
@b9g/cache Cache API implementation
@b9g/filesystem File System Access implementation
@b9g/async-context AsyncContext.Variable implementation
@b9g/http-errors Standard HTTP error classes
@b9g/assets Static asset handling
@b9g/platform Core runtime and platform APIs
@b9g/platform-node Node.js adapter
@b9g/platform-bun Bun adapter
@b9g/platform-cloudflare Cloudflare Workers adapter
@b9g/match-pattern URLPattern with extensions (100% WPT)

License

MIT

Extension points exported contracts — how you extend this code

LoggerStorage (Interface)
* Logger storage API for accessing named loggers. * @example const logger = self.loggers.get(["app"]); * @example co [6 …
packages/platform/src/globals.d.ts
FileSystemBackend (Interface)
(no doc) [6 implementers]
packages/filesystem/src/index.ts
Platform (Interface)
* Platform interface (mirrors @b9g/platform) [3 implementers]
packages/shovel-wpt/src/runners/platform.ts
CFAssetsBinding (Interface)
(no doc) [3 implementers]
packages/platform-cloudflare/src/directories.ts
CacheEntry (Interface)
* Cache entry stored in memory
packages/cache/src/memory.ts
ParamDefinition (Interface)
* Parse search pattern
packages/match-pattern/src/index.ts
InternalMatchResult (Interface)
* Result of internal route matching (for handle)
packages/router/src/index.ts
AssetsConfig (Interface)
USWDS asset URLs for the admin UI
packages/admin/src/admin.tsx

Core symbols most depended-on inside this repo

get
called by 543
packages/platform/src/runtime.ts
route
called by 155
packages/router/src/index.ts
text
called by 153
packages/platform/src/runtime.ts
set
called by 127
packages/platform/src/runtime.ts
writeFile
called by 115
packages/filesystem/src/index.ts
use
called by 108
packages/router/src/index.ts
parse
called by 108
src/utils/config.ts
handle
called by 107
packages/router/src/index.ts

Shape

Method 642
Function 480
Class 241
Interface 172
Enum 1

Languages

TypeScript100%

Modules by API surface

packages/platform/src/runtime.ts199 symbols
packages/match-pattern/src/index.ts72 symbols
src/utils/config.ts65 symbols
packages/platform-cloudflare/src/directories.ts64 symbols
packages/filesystem/src/index.ts55 symbols
packages/router/src/index.ts49 symbols
packages/http-errors/src/index.ts48 symbols
packages/platform/src/index.ts44 symbols
packages/router/test/router.test.ts40 symbols
packages/filesystem-s3/src/index.ts32 symbols
packages/platform-node/src/index.ts29 symbols
packages/platform-bun/src/index.ts28 symbols

Datastores touched

dbDatabase · 1 repos
myappDatabase · 1 repos
mydbDatabase · 1 repos

For agents

$ claude mcp add shovel \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact