This document explains how go-webglue works under the hood.
┌─────────────────────────────────────────────────────────┐
│ Browser │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Page Modules (.page.js) │ │
│ │ ├─ home.page.js │ │
│ │ ├─ users.page.js │ │
│ │ └─ ... │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ webglue.js (Core Client Library) │ │
│ │ ├─ API Proxy (api.module.method) │ │
│ │ ├─ Event Handlers (SSE) │ │
│ │ ├─ SPA Router │ │
│ │ └─ Tag Factories (DIV, BUTTON, etc.) │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
HTTP/SSE
│
┌─────────────────────────────────────────────────────────┐
│ Go Server │
│ ┌───────────────────────────────────────────────────┐ │
│ │ http.ServeMux │ │
│ │ ├─ / → StaticHandler │ │
│ │ ├─ /api/* → ApiHandler │ │
│ │ └─ /events → EventHandler (SSE) │ │
│ └───────────────────────────────────────────────────┘ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Modules │ │
│ │ ├─ webglue (core) │ │
│ │ ├─ your-module-1 │ │
│ │ │ ├─ API Struct │ │
│ │ │ ├─ Events │ │
│ │ │ └─ Resources (embed.FS) │ │
│ │ └─ your-module-2 │ │
│ └───────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
Location: mux_handler.go
A Module is the fundamental building block:
type Module struct {
Name string // Used in URLs and JS API paths
Resources *embed.FS // Client-side files (JS, CSS, HTML)
Events []*Event // Server-to-client event streams
Api any // Struct with exported methods
}Key Points:
- Each module has its own namespace (e.g.,
api.mymodule.*) - Resources are embedded at compile time using
//go:embed - The core "webglue" module is always included automatically
Location: mux_handler.go:20-48
When you call webglue.NewHandler(), three HTTP handlers are created:
Location: static_handler.go
Responsibilities:
- Serves the main
index.html(with auto-generated import maps) - Serves embedded CSS/JS/images
- Minifies resources in production mode
- Supports file-system fallback in dev mode
Process:
- Check if path matches dev mode file → serve from filesystem
- Check if path matches cached resource → serve from memory
- Otherwise → serve
index.html(SPA fallback)
Import Map Generation:
// Auto-generated in HTML:
<script type="importmap">
{
"imports": {
"webglue": "./webglue.js",
"mymodule": "./mymodule.js"
}
}
</script>Location: api_handler.go
Responsibilities:
- Routes HTTP calls to Go methods
- Uses reflection to invoke methods dynamically
- Handles parameter marshaling/unmarshaling
- Returns results as JSON
Process:
- Parse URL:
/api/{module}/{function} - Find module by name
- Capitalize function name (JS
camelCase→ GoPascalCase) - Use reflection to find method
- Build parameter list:
- Inject typed params (context, custom types via CallChecker)
- Unmarshal remaining params from JSON body
- Invoke method with
reflect.Value.Call() - Process return values:
- If any return is error type and non-nil → return error
- Single non-error return →
{"result": value} - Multiple returns →
{"result": [val1, val2, ...]}
Reflection Magic:
// Example method
func (api *MyApi) GetUser(ctx context.Context, token string, id int) (User, error)
// Parameter resolution:
// - ctx: Injected from request.Context()
// - token: Injected via CallChecker (if implemented)
// - id: Unmarshaled from JSON body [42]Location: event_handler.go
Responsibilities:
- Establishes Server-Sent Events (SSE) connection
- Streams events to connected clients
- Manages automatic reconnection
Process:
- Client connects to
/events?stream=webglue - Server keeps connection open
- When
event.Emit()is called, data is broadcast to all connections - Client receives event and triggers jQuery custom events
Location: pkg/client/webglue.js
start() → startAsync()
↓
1. Discover APIs (fetch /api/webglue/discover)
2. Build api.* proxy objects
3. Register event handlers
4. Set up SPA routing
5. Connect to SSE stream
6. Render initial pageOn startup:
GET /api/webglue/discover
→ {
"mymodule": {
"functions": ["getUser", "createUser"],
"events": ["userCreated"]
}
}Creates:
api.mymodule.getUser = (...params) =>
fetch('/api/mymodule/getUser', {
method: 'POST',
body: JSON.stringify(params)
})URL Format: /{pageName}?param=value
Process:
- User navigates to
/users?id=42 - Router extracts
pageName = "users",params = {id: "42"} - Import
users.page.jsdynamically - Call
page.render(url, params) - Replace body content with returned elements
History API:
goto(url)→pushState(add to history)goto(url, true)→replaceState(replace current)- Browser back/forward triggers
onpopstate→ re-render
Helper functions to create DOM elements with jQuery:
DIV("class-name", [ // String = CSS class
{ id: "myDiv" }, // Object = jQuery .prop()
el => console.log(el), // Function = callback with element
"Text content" // Primitives = appended
])Client Server
│ │
│ POST /api/users/getUser │
│ Body: [42] │
├──────────────────────────────>│
│ │ ApiHandler.ServeHTTP()
│ │ ├─ Parse: module="users", func="GetUser"
│ │ ├─ Find Module & Method (reflection)
│ │ ├─ Build params: [ctx, 42]
│ │ ├─ Call: GetUser(ctx, 42)
│ │ └─ Marshal result
│ {"result": {...}} │
│<──────────────────────────────│
│ │
Server Client
│ │
│ event.Emit(data) │
├──────────────────────────────>│ EventSource.onmessage
│ │ ├─ Parse JSON
│ │ ├─ Build event name: "webglue-users-updated"
│ │ └─ Trigger: $("*").trigger(eventName, data)
│ │
│ │ jQuery event bubbles
│ │ └─ Element.onUsersUpdated() called
User Action webglue.js
│ │
│ Click <a href="/users"> │
├──────────────────────────────>│ Event listener
│ │ ├─ preventDefault()
│ │ ├─ goto("/users")
│ │ ├─ history.pushState()
│ │ ├─ import("./users.page.js")
│ │ ├─ page.render()
│ │ └─ $("body").append(elements)
│ │
│ Page updated │
│<──────────────────────────────│
Instead of manually registering routes, go-webglue uses reflection to discover methods:
apiType := reflect.TypeOf(module.Api)
for i := 0; i < apiType.NumMethod(); i++ {
method := apiType.Method(i)
// method.Name becomes available as API endpoint
}Pros:
- Zero boilerplate
- Automatic API discovery
- Type-safe parameters
Cons:
- Slightly slower than direct calls (negligible for most apps)
- All exported methods are exposed (use CallChecker for access control)
Resources are compiled into the binary:
//go:embed client/*
var clientResources embed.FSBenefits:
- Single binary deployment
- No asset server needed
- Cache resources in memory with minification
Development Override:
Set MODULENAME_DEV=/path environment variable to serve from filesystem instead.
go-webglue uses Server-Sent Events (SSE) for server-to-client communication:
Why SSE?
- Simpler than WebSockets (unidirectional)
- Automatic reconnection built-in
- Works over HTTP (no upgrade needed)
- Better for pub/sub patterns
When to use WebSockets instead?
- Need bidirectional streaming
- Binary data transfer
- Lower latency requirements
The framework includes jQuery for DOM manipulation:
Why jQuery?
- Familiar API for many developers
- No build step required
- Lightweight for small apps
- Good enough for admin UIs and dashboards
Alternatives: You can use React/Vue/Svelte by:
- Not using the default
index.html - Providing custom HTML with your framework
- Using
/api/*endpoints directly
- Embedded resources are loaded once at startup
- Minified resources cached in memory
- Each SSE connection holds one goroutine
Production Mode:
- All resources minified and cached at startup
- No disk I/O during requests
- Serve index.html for unknown paths
Development Mode:
- Files read from disk on each request
- No minification
- Instant updates without rebuild
Single Instance:
- Handles thousands of concurrent SSE connections
- API calls are stateless (except your API struct state)
Multiple Instances:
- Need external pub/sub for SSE (Redis, etc.)
- Stateless API works fine behind load balancer
All exported methods are exposed by default:
- Use
CallCheckerinterface for authentication - Validate inputs in your methods
- Return errors for unauthorized access
- Client-side HTML is user-controlled (your JS)
- Be careful with user input in jQuery
.html() - Use
.text()for untrusted content
No CORS headers by default:
- Add CORS middleware if needed
- Or use a reverse proxy
Replace the default template:
webglue.Options{
IndexHtml: myCustomHTML, // Must include {WEBGLUE} placeholder
}Inject custom parameters into API calls:
func (api *MyApi) CheckCall(req *http.Request, funcName string) ([]any, error) {
// Authentication, rate limiting, logging, etc.
return []any{customParam}, nil
}Organize large apps:
webglue.Options{
Modules: []*webglue.Module{
userModule,
productModule,
adminModule,
},
}Each module has its own namespace, resources, and events.
- API Reference - Detailed API documentation
- Frontend Guide - Client-side development
- Examples - Real-world patterns