singleserve

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Jul 14, 2026 License: MIT Imports: 22 Imported by: 0

README

Singleserve

Singleserve is a lightweight Go toolkit for single-binary, browser-based local applications. It provides the local process boundary—loopback HTTP hosting, per-launch authentication, browser startup, browser-presence tracking, lifetime policy, shutdown guards, and graceful drain—while applications keep ownership of their router, business logic, frontend framework, and embedded assets.

[!IMPORTANT] v0.1.0 is the first supported release. See the v0.1 API specification, release governance, and roadmap.

Intended use

Singleserve targets local applications that:

  • ship as one Go binary;
  • serve a browser UI from a loopback-only HTTP listener;
  • need a fresh authentication secret for every process launch;
  • may live until explicitly stopped or until their browser tabs disconnect;
  • need application-specific vetoes before a user- or browser-driven shutdown; and
  • do not want a desktop webview, hosted account system, or prescribed frontend stack.

It is not a router, asset pipeline, desktop shell, remote-access server, application framework, or business-logic layer.

Install

Add the released module to an application:

go get github.com/rztaylor/singleserve@v0.1.0

Runnable example

Run the executable specification from the repository root:

go run ./examples/minimal

It starts a browser-bound loopback server and opens a live lifecycle dashboard. The page shows each heartbeat and its timestamp, the next-heartbeat countdown, consecutive failures, a manual health probe, backend-loss detection timing, and the normal/fallback tab-close shutdown windows. Shutting down or losing the backend moves the page into a terminal state and attempts to close the tab; if the browser blocks script closure, the page clearly asks the user to close it. If browser opening fails, the command prints the authenticated URL for manual opening. Ctrl+C remains an owner-forced shutdown path.

Usage

The public Go API lives in the root package:

import "github.com/rztaylor/singleserve"

The startup flow is deliberately explicit:

srv, err := singleserve.New(singleserve.Options{
    Handler:  appRouter,
    Lifetime: singleserve.BrowserBoundLifetime(),
    Guard: singleserve.ShutdownGuardFunc(func(ctx context.Context, request singleserve.ShutdownRequest) error {
        if workIsRunning() {
            return singleserve.DenyShutdown("work_running", "Wait for the current operation to finish")
        }
        return nil
    }),
})
if err != nil {
    return err
}

launch, err := srv.Start(ctx)
if err != nil {
    return err
}
if err := launch.OpenBrowser(ctx); err != nil {
    fmt.Fprintln(os.Stderr, "Open this URL manually:", launch.URL())
}
result, err := launch.Wait()

Start binds before returning. OpenBrowser is optional; if it fails, the listener remains available and launch.URL() can be shown for manual opening. Wait returns only after the server has drained or reports a serving/drain error.

The application can import the dependency-free browser client directly from the running server:

<script type="module">
  import { connect } from "/_singleserve/client.js";

  const session = await connect({
    onServerUnavailable: () => showRecoveryUI(),
  });

  const response = await session.fetch("/api/items");
</script>

The initial capability URL establishes a launch-unique strict session cookie for ordinary asset loads. The client retains the per-tab token in sessionStorage, removes it from the address bar, authenticates same-origin requests, sends heartbeats, and reports server loss without rendering UI.

Design boundaries

  • Applications pass an http.Handler; Singleserve wraps it but never constructs or mutates the application router.
  • The reserved /_singleserve/ endpoints are the only HTTP routes owned by the toolkit.
  • Authentication, browser state, and lifecycle state are process-local and never persisted.
  • The framework-neutral browser client is a plain ES module with no npm or UI-framework dependency.
  • The initial implementation is standard-library-only.
  • Non-loopback listeners, TLS, remote access, multi-user identity, WebSockets, and application persistence are outside v0.1.

Repository map

Licence

Singleserve uses the MIT License. The choice is intended to keep adoption simple for open-source and proprietary local applications while retaining the standard warranty disclaimer.

Documentation

Overview

Package singleserve provides the local HTTP, browser authentication, browser-presence, and graceful-shutdown boundary for single-binary browser applications while leaving routing, assets, and business logic to callers.

A Server wraps an application-owned net/http.Handler, binds only to loopback, and produces a per-launch capability URL. Applications choose an explicit or browser-bound lifetime and may supply a shutdown guard.

Example
package main

import (
	"context"
	"fmt"
	"net/http"

	"github.com/rztaylor/singleserve"
)

func main() {
	server, err := singleserve.New(singleserve.Options{
		Handler:  http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = fmt.Fprint(w, "hello") }),
		Lifetime: singleserve.ExplicitLifetime(),
		Opener:   singleserve.BrowserOpenerFunc(func(context.Context, string) error { return nil }),
	})
	if err != nil {
		panic(err)
	}

	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	_, err = server.Start(ctx)
	fmt.Println(err)
}
Output:
context canceled

Index

Examples

Constants

View Source
const (
	// TokenHeader carries the per-launch authentication token.
	TokenHeader = "X-Singleserve-Token"
	// TabHeader identifies one browser tab instance.
	TabHeader = "X-Singleserve-Tab"
	// ControlPath is reserved for Singleserve lifecycle endpoints.
	ControlPath = "/_singleserve/"
)

Variables

View Source
var ErrAlreadyStarted = errors.New("singleserve: server already started")

ErrAlreadyStarted reports an attempt to start a single-use Server twice.

View Source
var ErrBrowserAlreadyOpened = errors.New("singleserve: browser already opened")

ErrBrowserAlreadyOpened reports an attempt to open one Launch twice.

View Source
var (

	// ErrTooManyTabs reports that the bounded tab registry has no reclaimable entries.
	ErrTooManyTabs = errors.New("singleserve: too many connected tabs")
)

Functions

func DenyShutdown

func DenyShutdown(code, message string) error

DenyShutdown creates a typed shutdown veto. Invalid codes or empty messages are treated as internal guard failures when evaluated.

Types

type BrowserOpener

type BrowserOpener interface {
	Open(context.Context, string) error
}

BrowserOpener opens a URL without waiting for the browser to exit.

type BrowserOpenerFunc

type BrowserOpenerFunc func(context.Context, string) error

BrowserOpenerFunc adapts a function to BrowserOpener.

func (BrowserOpenerFunc) Open

func (f BrowserOpenerFunc) Open(ctx context.Context, rawURL string) error

Open calls f.

type Launch

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

Launch is the active view of a started Server.

func (*Launch) Address

func (l *Launch) Address() string

Address returns the assigned listener address.

func (*Launch) BaseURL

func (l *Launch) BaseURL() string

BaseURL returns the listener URL without authentication material.

func (*Launch) OpenBrowser

func (l *Launch) OpenBrowser(ctx context.Context) error

OpenBrowser opens URL with the configured platform opener.

func (*Launch) Shutdown

func (l *Launch) Shutdown(ctx context.Context) error

Shutdown forces owner-controlled graceful shutdown and waits for completion or for ctx to be canceled. It bypasses the application guard.

func (*Launch) URL

func (l *Launch) URL() string

URL returns the authenticated browser bootstrap URL. Treat it as a secret.

func (*Launch) Wait

func (l *Launch) Wait() (Result, error)

Wait blocks until the HTTP server has fully stopped.

type LifetimeMode

type LifetimeMode uint8

LifetimeMode controls whether browser absence can stop the server.

const (
	// LifetimeExplicit stops only on parent cancellation, owner shutdown, or an
	// authenticated browser shutdown request.
	LifetimeExplicit LifetimeMode = iota
	// LifetimeBrowserBound also stops when first contact never arrives or every
	// contacted browser tab disconnects.
	LifetimeBrowserBound
)

type LifetimePolicy

type LifetimePolicy struct {
	Mode                LifetimeMode
	FirstContactTimeout time.Duration
	HeartbeatTimeout    time.Duration
	DisconnectGrace     time.Duration
	CheckInterval       time.Duration
}

LifetimePolicy configures browser-presence shutdown behavior.

func BrowserBoundLifetime

func BrowserBoundLifetime() LifetimePolicy

BrowserBoundLifetime returns the recommended browser-owned policy.

func ExplicitLifetime

func ExplicitLifetime() LifetimePolicy

ExplicitLifetime returns the safe zero-value lifetime policy.

type Options

type Options struct {
	Handler  http.Handler
	Address  string
	Lifetime LifetimePolicy
	Guard    ShutdownGuard
	Opener   BrowserOpener
}

Options configures one single-use local server.

type Result

type Result struct {
	Reason    ShutdownReason
	StartedAt time.Time
	StoppedAt time.Time
}

Result describes the completed server lifetime.

type Server

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

Server owns one authenticated loopback listener and its lifecycle state. A Server is single-use.

func New

func New(options Options) (*Server, error)

New validates options and prepares a single-use Server.

func (*Server) Start

func (s *Server) Start(ctx context.Context) (*Launch, error)

Start binds the configured listener and starts serving before returning.

func (*Server) Tabs

func (s *Server) Tabs() []TabSnapshot

Tabs returns a time-ordered copy of the current tab registry.

type ShutdownDeniedError

type ShutdownDeniedError struct {
	Code    string
	Message string
}

ShutdownDeniedError is a safe, expected application veto.

func (*ShutdownDeniedError) Error

func (e *ShutdownDeniedError) Error() string

type ShutdownGuard

type ShutdownGuard interface {
	CheckShutdown(context.Context, ShutdownRequest) error
}

ShutdownGuard can veto browser-initiated or browser-bound shutdown.

type ShutdownGuardFunc

type ShutdownGuardFunc func(context.Context, ShutdownRequest) error

ShutdownGuardFunc adapts a function to ShutdownGuard.

func (ShutdownGuardFunc) CheckShutdown

func (f ShutdownGuardFunc) CheckShutdown(ctx context.Context, request ShutdownRequest) error

CheckShutdown calls f.

type ShutdownReason

type ShutdownReason string

ShutdownReason records why graceful shutdown began.

const (
	ShutdownBrowserRequest      ShutdownReason = "browser_request"
	ShutdownBrowserDisconnected ShutdownReason = "browser_disconnected"
	ShutdownFirstContactTimeout ShutdownReason = "first_contact_timeout"
	ShutdownProgrammatic        ShutdownReason = "programmatic"
	ShutdownContextCanceled     ShutdownReason = "context_canceled"
	ShutdownServerError         ShutdownReason = "server_error"
)

type ShutdownRequest

type ShutdownRequest struct {
	Reason ShutdownReason
	Tabs   []TabSnapshot
}

ShutdownRequest is the server-built input to an application shutdown guard.

type TabSnapshot

type TabSnapshot struct {
	ID             string
	State          TabState
	ConnectedAt    time.Time
	LastSeenAt     time.Time
	DisconnectedAt time.Time
}

TabSnapshot is an immutable copy of tracked browser presence.

type TabState

type TabState string

TabState is the current server-side state of one browser tab instance.

const (
	TabConnected    TabState = "connected"
	TabDisconnected TabState = "disconnected"
	TabExpired      TabState = "expired"
)

Directories

Path Synopsis
examples
minimal command
Command minimal is an executable specification of Singleserve's public API.
Command minimal is an executable specification of Singleserve's public API.

Jump to

Keyboard shortcuts

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