substrate

package module
v1.2.5 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 12, 2026 License: MIT Imports: 25 Imported by: 0

README

Substrate

A Caddy module that adds a custom transport method for reverse_proxy, enabling dynamic JavaScript execution via Deno.

Overview

Substrate behaves like FastCGI but over HTTP - it runs JavaScript files as separate Deno processes and proxies HTTP traffic to them via Unix domain sockets. Each file gets its own process with automatic lifecycle management.

Substrate automatically downloads and manages its own Deno runtime, so no external dependencies are required.

Installation

Build Caddy with the Substrate module:

xcaddy build --with github.com/fserb/substrate

Quick Start

  1. Create a Caddyfile:
root /path/to/your/files

@js_files {
    path *.js
    file {path}
}

reverse_proxy @js_files {
    transport substrate {
        idle_timeout 5m
        startup_timeout 30s
    }
}
  1. Create a JavaScript file (e.g., hello.js):
const [socketPath] = Deno.args;

Deno.serve({
  path: socketPath
}, (req) => {
  return new Response('Hello from Substrate!');
});
  1. Start Caddy:
caddy run
  1. Request triggers process execution:
curl http://localhost/hello.js
# → "Hello from Substrate!"

How It Works

  1. File Matching: Caddy's file matcher identifies JavaScript files
  2. Deno Runtime: Substrate downloads and caches Deno automatically on first use
  3. Process Creation: Substrate runs the file via deno run --allow-all script.js socketPath
  4. Socket Management: Each file gets a unique Unix domain socket automatically assigned
  5. Request Proxying: HTTP requests are proxied to the running process via Unix socket
  6. Lifecycle Management: Processes are reused, restarted, and cleaned up automatically

Process Contract

Your JavaScript file receives one argument:

  • Deno.args[0]: Unix socket path to listen on (e.g., /tmp/substrate-abc123.sock)

Scripts do not need shebang lines or executable permission - Substrate handles execution via its embedded Deno runtime.

Example:

const [socketPath] = Deno.args;

Deno.serve({ path: socketPath }, (req) => {
  return new Response('Hello!');
});

// Optional: Graceful shutdown
Deno.addSignalListener("SIGTERM", () => {
  Deno.exit(0);
});

Configuration

Transport Options
reverse_proxy @matcher {
    transport substrate {
        idle_timeout 5m      # How long to keep unused processes (0=never cleanup, -1=close after request)
        startup_timeout 30s  # How long to wait for process startup
    }
}
Idle Timeout Modes
  • Positive values (e.g., 5m): Normal operation - cleanup after idle period
  • Zero (0): Processes run indefinitely until manually stopped
  • Negative one (-1): One-shot mode - process terminates after each request

Features

  • Zero Configuration: Scripts just need to listen on the provided Unix socket
  • Automatic Deno Management: Deno runtime downloaded and cached automatically
  • Automatic Socket Management: Each process gets a unique Unix domain socket
  • Process Reuse: Same file requests share the same process
  • Hot Reloading: File changes restart the associated process
  • Concurrent Safe: Multiple requests handled properly
  • Resource Cleanup: Idle processes and socket files automatically cleaned up
  • Security: Unix socket isolation and privilege dropping when running as root
  • Advanced Routing: URL rewriting, subpath matching, and pattern-based routing

Development

./task build    # Build the module
./task test     # Run all tests (unit + integration + e2e)
./task run      # Run example configuration

Advanced Usage

URL Rewriting

Route clean URLs to JavaScript scripts:

@simple_rewrite {
    not path *.js
    file {path}.js
}

reverse_proxy @simple_rewrite {
    transport substrate
}
Subpath Routing

Extract subpaths and forward as headers:

@subpath_match {
    path_regexp m ^(.*)(/[^/]+)$
}

handle @subpath_match {
    @file_exists file {re.m.1}.lemon.js
    handle @file_exists {
        reverse_proxy {
            header_up X-Subpath {re.m.2}
            transport substrate
        }
    }
}

Examples

Check the e2e tests in e2e/ directory for comprehensive usage patterns and working examples.

Documentation

Overview

Deno runtime management.

DenoManager downloads and caches the Deno binary for the current platform. Substrate uses a specific Deno version to ensure consistent behavior. The binary is cached in {cache_dir}/deno/{version}-{platform}/. Default cache_dir is ~/.cache/substrate/.

This avoids requiring Deno to be pre-installed on the system.

Index

Constants

View Source
const DenoVersion = "v2.6.4"

Variables

This section is empty.

Functions

This section is empty.

Types

type DenoManager added in v1.2.0

type DenoManager struct {
	// contains filtered or unexported fields
}

DenoManager handles downloading and caching of the Deno runtime

func NewDenoManager added in v1.2.0

func NewDenoManager(cacheDir string, logger *zap.Logger) *DenoManager

NewDenoManager creates a new DenoManager with the default version If cacheDir is empty, uses ~/.cache/substrate/ Deno binary is stored in {cacheDir}/deno/{version}-{platform}/

func (*DenoManager) Get added in v1.2.0

func (dm *DenoManager) Get() (string, error)

Get returns the path to the Deno binary, downloading it if necessary

type Process added in v1.0.0

type Process struct {
	ScriptPath string
	SocketPath string
	DenoPath   string // Path to the deno binary
	DenoOpts   string // Extra deno options (e.g., "--config=/path/to/deno.json")
	Cmd        *exec.Cmd
	LastUsed   time.Time
	// contains filtered or unexported fields
}

func (*Process) Stop added in v1.0.0

func (p *Process) Stop() error

type ProcessManager added in v1.0.0

type ProcessManager struct {
	// contains filtered or unexported fields
}

func NewProcessManager added in v1.0.0

func NewProcessManager(idleTimeout, startupTimeout caddy.Duration, env map[string]string, denoOpts string, deno *DenoManager, logger *zap.Logger) (*ProcessManager, error)

func (*ProcessManager) Destruct added in v1.0.0

func (pm *ProcessManager) Destruct() error

func (*ProcessManager) Stop added in v1.0.0

func (pm *ProcessManager) Stop() error

type ProcessStartupError added in v1.0.4

type ProcessStartupError struct {
	Err        error
	ExitCode   int
	Stdout     string
	Stderr     string
	ScriptPath string
}

ProcessStartupError contains detailed information about process startup failures

func (*ProcessStartupError) Error added in v1.0.4

func (e *ProcessStartupError) Error() string

type SubstrateTransport added in v1.0.0

type SubstrateTransport struct {
	IdleTimeout    caddy.Duration    `json:"idle_timeout,omitempty"`
	StartupTimeout caddy.Duration    `json:"startup_timeout,omitempty"`
	Env            map[string]string `json:"env,omitempty"`
	DenoOpts       string            `json:"deno_opts,omitempty"`
	CacheDir       string            `json:"cache_dir,omitempty"`
	// contains filtered or unexported fields
}

func (SubstrateTransport) CaddyModule added in v1.0.0

func (SubstrateTransport) CaddyModule() caddy.ModuleInfo

func (*SubstrateTransport) Cleanup added in v1.0.0

func (t *SubstrateTransport) Cleanup() error

func (*SubstrateTransport) Provision added in v1.0.0

func (t *SubstrateTransport) Provision(ctx caddy.Context) error

func (*SubstrateTransport) RoundTrip added in v1.0.0

func (t *SubstrateTransport) RoundTrip(req *http.Request) (*http.Response, error)

func (*SubstrateTransport) UnmarshalCaddyfile added in v1.0.0

func (t *SubstrateTransport) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

func (*SubstrateTransport) Validate added in v1.0.0

func (t *SubstrateTransport) Validate() error

Directories

Path Synopsis

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL