Skip to content

feat(v3): add server mode for headless HTTP deployment#4903

Merged
leaanthony merged 5 commits into
v3-alphafrom
feature/headless-server-mode
Jan 25, 2026
Merged

feat(v3): add server mode for headless HTTP deployment#4903
leaanthony merged 5 commits into
v3-alphafrom
feature/headless-server-mode

Conversation

@leaanthony

@leaanthony leaanthony commented Jan 24, 2026

Copy link
Copy Markdown
Member

Summary

  • Add server mode (-tags server) for running Wails apps as pure HTTP servers without GUI dependencies
  • WebSocket event broadcasting to connected browsers
  • Browser clients represented as BrowserWindow implementing Window interface
  • Health check endpoint at /health
  • Docker support with Dockerfile.server template and Taskfile tasks
  • Environment variable overrides: WAILS_SERVER_HOST, WAILS_SERVER_PORT

Features

  • Build and run: wails3 task build:server, wails3 task run:server
  • Docker: wails3 task build:docker, wails3 task run:docker
  • CGO-free: Server mode builds without CGO dependencies
  • Graceful shutdown: Configurable timeout, handles SIGINT/SIGTERM

Test plan

  • Server mode builds successfully
  • Docker image builds and runs
  • Health check endpoint responds
  • WebSocket events broadcast to browsers
  • Unit tests pass (go test -tags server)

Documentation

Added docs/guides/server-build.mdx with full usage guide.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added experimental server mode enabling Wails apps to run as HTTP servers without GUI windows
    • Docker support for server-mode deployments with multi-stage builds
    • New build and run tasks for server mode and Docker workflows
  • Documentation

    • Added comprehensive server mode guide with configuration, examples, and use cases
    • Removed distribution tutorial documentation
  • Chores

    • Updated Go toolchain to 1.25
    • Updated all example dependencies to match latest ecosystem releases

✏️ Tip: You can customize this high-level summary in your review settings.

leaanthony and others added 2 commits January 25, 2026 10:41
Server mode allows Wails applications to run as pure HTTP servers
without native GUI dependencies. Enable with `-tags server` build tag.

Features:
- HTTP server with configurable host/port via ServerOptions
- WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides
- WebSocket event broadcasting to connected browsers
- Browser clients represented as BrowserWindow (Window interface)
- Health check endpoint at /health
- Graceful shutdown with configurable timeout
- Docker support with Dockerfile.server template and tasks

Build and run:
  wails3 task build:server
  wails3 task run:server
  wails3 task build:docker
  wails3 task run:docker

Documentation at docs/guides/server-build.mdx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Server mode allows Wails applications to run as pure HTTP servers
without native GUI dependencies. Enable with `-tags server` build tag.

Features:
- HTTP server with configurable host/port via ServerOptions
- WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides
- WebSocket event broadcasting to connected browsers
- Browser clients represented as BrowserWindow (Window interface)
- Health check endpoint at /health
- Graceful shutdown with configurable timeout
- Docker support with Dockerfile.server template and tasks

Build and run:
  wails3 task build:server
  wails3 task run:server
  wails3 task build:docker
  wails3 task run:docker

Documentation at docs/guides/server-build.mdx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jan 24, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@leaanthony has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 9 minutes and 40 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

Walkthrough

This pull request introduces experimental server mode for Wails v3, enabling applications to run as pure HTTP servers without native GUI dependencies. It includes comprehensive documentation, build configurations, a complete example implementation, and platform-specific adjustments to gate desktop-only code paths. Key additions involve HTTP server setup, WebSocket event broadcasting, browser window abstractions, and supporting infrastructure across build tasks and runtime assets.

Changes

Cohort / File(s) Summary
Documentation
docs/astro.config.mjs, docs/src/content/docs/guides/server-build.mdx
Adds server-build documentation page to sidebar with comprehensive coverage of server mode including Quick Start, configuration via ServerOptions, health endpoints, WebSocket events, production workflows, and Docker deployment.
Removed Documentation
docs/src/content/docs/tutorials/04-distributing-your-app.mdx
Entire tutorial on distributing Wails applications deleted (532 lines).
Changelog & Core Version Info
v3/UNRELEASED_CHANGELOG.md, v3/go.mod
Documents experimental server mode feature and updates core module dependencies (glamour, nfpm, go-git, sqlite, and standard library packages).
Go Module Updates Across Examples
v3/examples/android/go.mod, v3/examples/dev/go.mod, v3/examples/file-association/go.mod, v3/examples/gin-example/go.mod, v3/examples/gin-routing/go.mod, v3/examples/gin-service/go.mod, v3/examples/ios/go.mod, v3/examples/notifications/go.mod, v3/examples/print/go.mod, v3/tests/window-visibility-test/go.mod
Unified go.mod updates: bump Go toolchain 1.24→1.25, update wails/v3 to alpha.62, and refresh indirect dependencies across mergo, go-crypto, circl, go-git, dbus, golangorg/x/*, and other ecosystem packages.
Build Task Configuration
v3/Taskfile.yaml, v3/internal/commands/build_assets/Taskfile.tmpl.yml, v3/internal/templates/_common/Taskfile.tmpl.yml
Add build:server, run:server, test:server, build:docker, and run:docker tasks to enable server-mode compilation, Docker image builds, and server-specific test execution.
Server Example Implementation
v3/examples/server/main.go, v3/examples/server/go.mod, v3/examples/server/Taskfile.yml, v3/examples/server/README.md
Complete server mode example with GreetService, asset embedding, WebSocket event broadcasting, background tick emissions, and comprehensive documentation covering Docker deployment and configuration.
Server Mode Docker Support
v3/examples/server/Dockerfile, v3/internal/commands/build_assets/docker/Dockerfile.server
Multi-stage Dockerfiles for server mode using golang:alpine builder and distroless runtime, with automatic frontend asset inclusion, TLS configuration, and health check endpoint.
Core Server Mode Implementation
v3/pkg/application/application_server.go, v3/pkg/application/application_server_test.go
Primary server mode platform implementation (554 lines) with HTTP/WebSocket server setup, port binding, TLS support, graceful shutdown, health endpoint, asset serving, and extensive no-op GUI implementations. Includes health check, asset serving, and shutdown tests.
Browser Window & WebSocket
v3/pkg/application/browser_window.go, v3/pkg/application/websocket_server.go, v3/pkg/application/websocket_stub.go
Introduces BrowserWindow type for browser clients, WebSocketBroadcaster for connection/event management, and non-server stub returning nil. Manages clientID-to-window mappings and bidirectional event dispatch.
Application Configuration & Options
v3/pkg/application/application_options.go
Adds ServerOptions (Host, Port, ReadTimeout, WriteTimeout, IdleTimeout, ShutdownTimeout, TLS) and TLSOptions (CertFile, KeyFile) types to configure server mode behavior.
Message Processing & Events
v3/pkg/application/messageprocessor.go, v3/pkg/application/messageprocessor_events.go, v3/pkg/application/events_common_server.go
Updates message routing to check browser windows by ClientID before window name/ID fallback; adds nil-check for event.Sender assignment; provides server-mode no-op setupCommonEvents hook.
Platform-Specific Build Constraints (Android/iOS/Darwin/Linux/Windows)
v3/pkg/application/application_android.go, v3/pkg/application/application_android_nocgo.go, v3/pkg/application/application_darwin.go, v3/pkg/application/application_ios.go, v3/pkg/application/application_linux.go, v3/pkg/application/application_windows.go, v3/pkg/application/clipboard_linux.go, v3/pkg/application/dialogs_linux.go, v3/pkg/application/events_common_linux.go, v3/pkg/application/keys_linux.go, v3/pkg/application/linux_cgo.go, v3/pkg/application/mainthread_linux.go, v3/pkg/application/menu_linux.go, v3/pkg/application/menuitem_linux.go, v3/pkg/application/screen_linux.go, v3/pkg/application/single_instance_linux.go, v3/pkg/application/systemtray_linux.go, v3/pkg/application/webview_window_linux.go, v3/pkg/application/webview_window_linux_dev.go, v3/pkg/application/webview_window_linux_production.go
Add !server build tag exclusion across all platform-specific files to prevent compilation of GUI-dependent code when server mode is active.
Public API Removals
v3/pkg/application/application.go
Removes RegisterListener and RegisterServiceHandler methods (previously commented out).
Runtime & Assets
v3/internal/assetserver/bundledassets/runtime.js, v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts
Updates runtime.js CancellablePromise export mapping; adds loadOptionalScript() function to load /wails/custom.js and clientId propagation to window._wails for server mode.
Event Interface Documentation
v3/pkg/application/events.go
Updates WailsEventListener interface comment to clarify usage by transport layers (IPC, WebSocket) for event broadcasting.

Sequence Diagram(s)

sequenceDiagram
    participant Browser as Browser Client
    participant Server as HTTP Server
    participant EventMgr as Event Manager
    participant BackendApp as Backend Application

    rect rgba(100, 200, 100, 0.5)
    Note over Browser,BackendApp: Server Mode Initialization
    Server->>Server: Start HTTP Server (port 8080)
    Server->>Server: Register WebSocket Broadcaster
    Server->>Server: Setup Asset Routes (/health, /wails/events)
    Server->>EventMgr: Add broadcaster as listener
    end

    rect rgba(100, 150, 200, 0.5)
    Note over Browser,BackendApp: Client Connection Flow
    Browser->>Server: HTTP GET /
    Server->>Browser: Serve frontend assets
    Browser->>Server: Establish WebSocket /wails/events
    Server->>Server: Upgrade to WebSocket
    Server->>Server: Create BrowserWindow(clientID)
    Server->>Browser: WebSocket connection established
    end

    rect rgba(200, 150, 100, 0.5)
    Note over Browser,BackendApp: Event Broadcasting
    BackendApp->>EventMgr: Emit event (e.g., "server-tick")
    EventMgr->>Server: Notify listeners
    Server->>Browser: Broadcast via WebSocket
    Browser->>Browser: Handle event in JavaScript
    end

    rect rgba(150, 100, 200, 0.5)
    Note over Browser,BackendApp: Graceful Shutdown
    BackendApp->>Server: Receive shutdown signal
    Server->>Browser: Close WebSocket
    Server->>EventMgr: Cleanup
    Server->>Server: Stop HTTP listener
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

This PR introduces a substantial new feature with 80+ affected files spanning multiple concerns: comprehensive server mode implementation (~554 lines in core file), platform build tag updates across 20+ files, go.mod dependency updates across 10+ examples, build configuration additions, documentation, and example code. While many changes are repetitive (build tag updates), the core implementation is logic-dense and introduces new public types (ServerOptions, TLSOptions, BrowserWindow, WebSocketBroadcaster) requiring careful validation. The heterogeneous nature of changes—spanning runtime, configuration, documentation, and platform-specific code—demands separate reasoning for different cohorts.

Possibly related PRs

Suggested labels

Documentation, runtime, v3-alpha, go, Implemented in v3, size:XXL

Suggested reviewers

  • atterpac

Poem

🐰 A server mode hops in today,
No GUI windows in the way!
WebSockets broadcast events with glee,
Docker deploys now, wild and free!
Browser windows mapped with care,
Headless apps beyond compare! 🚀

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description addresses the purpose and scope comprehensively, but lacks required template sections like issue reference, type of change checkboxes, test configuration details, and checklist completion. Include issue reference (Fixes #), select type of change checkbox, provide test configuration output, and complete all checklist items including changelog and documentation updates.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding server mode for headless HTTP deployment, which is the primary objective of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Jan 25, 2026

Copy link
Copy Markdown

Deploying wails with  Cloudflare Pages  Cloudflare Pages

Latest commit: 7c4d735
Status: ✅  Deploy successful!
Preview URL: https://d10940cc.wails.pages.dev
Branch Preview URL: https://feature-headless-server-mode.wails.pages.dev

View logs

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
v3/pkg/application/application_android.go (2)

516-522: Memory leak: C.CString allocations are never freed.

The C.CString function allocates memory that must be explicitly freed with C.free. These allocations leak memory on every JNI call.

🔧 Suggested fix
 //export Java_com_wails_app_WailsBridge_nativeHandleMessage
 func Java_com_wails_app_WailsBridge_nativeHandleMessage(env *C.JNIEnv, obj C.jobject, jmessage C.jstring) C.jstring {
 	// Convert Java string to Go string
 	cMessage := C.jstringToC(env, jmessage)
 	defer C.releaseJString(env, jmessage, cMessage)
 
 	goMessage := C.GoString(cMessage)
 
 	androidLogf("debug", "🤖 [JNI] nativeHandleMessage: %s", goMessage)
 
 	globalAppLock.RLock()
 	app := globalApp
 	globalAppLock.RUnlock()
 
 	if app == nil {
 		errorResponse := `{"error":"App not initialized"}`
-		return C.createJString(env, C.CString(errorResponse))
+		cErr := C.CString(errorResponse)
+		defer C.free(unsafe.Pointer(cErr))
+		return C.createJString(env, cErr)
 	}
 
 	// Parse and handle the message
 	response := handleMessageForAndroid(app, goMessage)
-	return C.createJString(env, C.CString(response))
+	cResp := C.CString(response)
+	defer C.free(unsafe.Pointer(cResp))
+	return C.createJString(env, cResp)
 }

526-533: Memory leak: C.CString allocation not freed in nativeGetAssetMimeType.

Same issue as above — the C.CString(mimeType) allocation leaks memory.

🔧 Suggested fix
 //export Java_com_wails_app_WailsBridge_nativeGetAssetMimeType
 func Java_com_wails_app_WailsBridge_nativeGetAssetMimeType(env *C.JNIEnv, obj C.jobject, jpath C.jstring) C.jstring {
 	// Convert Java string to Go string
 	cPath := C.jstringToC(env, jpath)
 	defer C.releaseJString(env, jpath, cPath)
 
 	goPath := C.GoString(cPath)
 	mimeType := getMimeTypeForPath(goPath)
-	return C.createJString(env, C.CString(mimeType))
+	cMime := C.CString(mimeType)
+	defer C.free(unsafe.Pointer(cMime))
+	return C.createJString(env, cMime)
 }
🤖 Fix all issues with AI agents
In `@docs/src/content/docs/guides/server-build.mdx`:
- Around line 280-286: The healthcheck block uses wget which isn't present in
distroless images; update the healthcheck "test" entry to use a tool available
in minimal images (e.g., curl with --fail) or document that a healthcheck binary
must be added to the Custom Dockerfile. Specifically, change the healthcheck
"test" command in the healthcheck block (the "test" array) to use curl (or an
HTTP-native healthcheck) or add a note next to the healthcheck example
explaining that distroless images require embedding a client binary for HTTP
probes.
- Around line 272-286: Remove the deprecated top-level "version: '3.8'" entry
from the compose snippet so the compose file uses the modern schema; in the
Docker Compose YAML shown (the service named "app" with
ports/environment/healthcheck) delete the version line and leave the services
block (app, ports, environment, healthcheck) intact to avoid deprecation
warnings.

In `@v3/examples/gin-routing/go.mod`:
- Line 1: The module declaration currently reads "module gin-example" which
doesn't match the directory; update the module statement in the go.mod (the
module declaration symbol) to reflect the actual module path for this package
(e.g., the repository path that includes the v3/examples/gin-routing segment or
a module name matching the directory) so the module name aligns with the folder
structure and import paths.

In `@v3/examples/gin-service/go.mod`:
- Line 74: The replace directive currently targets v3.0.0-alpha.9 while the
module requires v3.0.0-alpha.62, so update the replace directive in go.mod (the
line starting with "replace github.com/wailsapp/wails/v3") to reference
v3.0.0-alpha.62 so the replace will apply to the required version.

In `@v3/examples/ios/go.mod`:
- Line 1: The go.mod module name is a placeholder ("changeme"); update the
module declaration in the go.mod file so it reflects the real project (for
example "ios-example" or the intended module name) by replacing the current
module token on the first line; ensure the new module name follows Go module
naming conventions (a valid module path) so imports and builds resolve
correctly.

In `@v3/examples/server/Dockerfile`:
- Around line 42-43: The COPY instruction copying frontend assets from the
builder stage uses a hardcoded path (/app/examples/server/frontend/dist)
inconsistent with the conditional build contexts; update the Dockerfile so the
assets are placed/copied from a consistent location regardless of build
context—either add a conditional COPY that checks both possible builder paths or
change the build stage to always output frontend assets to a fixed directory
(e.g., /app/frontend/dist) and update the COPY --from=builder line to use that
fixed path; reference the builder stage name "builder" and the existing COPY
--from=builder /app/examples/server/frontend/dist /frontend/dist when making the
change.

In `@v3/examples/server/README.md`:
- Line 38: Replace the bare URL "http://localhost:8080" in the README sentence
("Then open http://localhost:8080 in your browser.") with an autolink or
Markdown link to satisfy MD034; for example use <http://localhost:8080> or a
Markdown link such as [http://localhost:8080](http://localhost:8080) or a
descriptive label like [Open the app](http://localhost:8080).

In `@v3/internal/commands/build_assets/Taskfile.tmpl.yml`:
- Around line 147-177: The run:docker task hardcodes the container's internal
port to 8080 in the `docker run` command, which will break if the app is
configured with a different port via WAILS_SERVER_PORT or ServerOptions.Port;
update the task to make the container internal port configurable (e.g. add an
INTERNAL_PORT/CONTAINER_PORT var and use it in the `docker run` command inside
the run:docker `cmds`), ensure the build:docker task and vars (TAG, PORT) remain
consistent, and add a brief note in the task description that the internal
container port must match WAILS_SERVER_PORT/ServerOptions.Port when overriding
the port.

In `@v3/pkg/application/application_server_test.go`:
- Around line 133-137: Remove the accidental terminal output embedded in the
Assets.AssetOptions block: delete the stray text beginning with "task:
[build:docker] docker build ..." so that the struct literal only contains valid
fields (e.g., keep the Handler: http.HandlerFunc(...) entry). Specifically edit
the AssetOptions entry (Assets) in the test so the corrupt token is removed and
the Handler field (http.HandlerFunc func) is the next valid field in the struct,
ensuring the test compiles.

In `@v3/pkg/application/application_server.go`:
- Around line 301-304: Update the comment above serverApp.getScreens to reflect
actual behavior: it returns nil and an error rather than an empty slice; change
the comment from "getScreens returns empty slice in server mode." to something
like "getScreens returns nil and an error in server mode (screen information not
available)." Ensure the comment mentions serverApp.getScreens and the nil,error
return semantics.

In `@v3/pkg/application/menu_linux.go`:
- Line 1: Update the build constraints on the platform-specific menu
implementations to match menu_linux.go by adding the "!server" exclusion: add
the same "//go:build <os> && !android && !server" style tag to both
menu_windows.go and menu_darwin.go so they are excluded in server builds; this
keeps them consistent with application_windows.go/application_darwin.go and the
server stubs (serverMenu, serverSystemTray, serverWebviewWindow).

In `@v3/tests/window-visibility-test/go.mod`:
- Line 3: The module declares "go 1.25" in go.mod but the CI only tests older
toolchains; either update the CI workflow's Go versions matrix (the "go-version"
entries used in the build-and-test-v3.yml workflow) to include "1.25" so the
tested toolchain matches the go directive, or change the go.mod line from "go
1.25" back to "go 1.24" so it matches the existing CI matrix; make the change
consistently (either CI matrix or go.mod) and run the CI matrix to verify tests
pass.
♻️ Duplicate comments (1)
v3/examples/ios/go.mod (1)

3-3: Same Go 1.25 version concern as other modules.

See comment on gin-example/go.mod regarding Go version verification.

🧹 Nitpick comments (14)
v3/UNRELEASED_CHANGELOG.md (1)

20-20: Optional: include PR reference in changelog entry.
Helps traceability in the release notes.

📝 Suggested tweak
-- Add experimental server mode for headless/web deployments (`-tags server`). Enables running Wails apps as HTTP servers without native GUI dependencies. Build with `wails3 task build:server`. See `examples/server` for details.
+- Add experimental server mode for headless/web deployments (`-tags server`). Enables running Wails apps as HTTP servers without native GUI dependencies. Build with `wails3 task build:server`. See `examples/server` for details. (`#4903`)
v3/internal/commands/build_assets/docker/Dockerfile.server (2)

18-22: Add go mod download before build for better Docker layer caching.

Running go mod tidy ensures module consistency, but adding an explicit go mod download step allows Docker to cache dependencies separately from source changes, improving rebuild times.

Suggested improvement
 # Remove local replace directive if present (for production builds)
 RUN sed -i '/^replace/d' go.mod || true

-# Download dependencies
+# Download dependencies (separate layer for caching)
 RUN go mod tidy
+RUN go mod download

 # Build the server binary
 RUN go build -tags server -ldflags="-s -w" -o server .

30-31: Hardcoded frontend path assumes specific project structure.

The path /app/frontend/dist assumes a standard Vite/frontend build output location. If a project uses a different frontend structure or output directory, this COPY will fail. Consider documenting this assumption or making it configurable via build args.

Optional: Add ARG for flexibility
+ARG FRONTEND_DIST=frontend/dist
+
 # Copy frontend assets
-COPY --from=builder /app/frontend/dist /frontend/dist
+COPY --from=builder /app/${FRONTEND_DIST} /frontend/dist
v3/internal/runtime/desktop/@wailsio/runtime/src/index.ts (1)

88-98: Promise resolves before script execution completes.

The function returns a Promise that resolves after the HEAD check and script tag injection, but before the script has actually loaded and executed. If callers need to wait for the script to be ready, they'll get a false positive.

For the current use case (line 101), this is acceptable since custom.js is fire-and-forget. However, since this is a public export, consider documenting this behavior or optionally waiting for script load.

Optional: Wait for script load
 export function loadOptionalScript(url: string): Promise<void> {
     return fetch(url, { method: 'HEAD' })
         .then(response => {
             if (response.ok) {
-                const script = document.createElement('script');
-                script.src = url;
-                document.head.appendChild(script);
+                return new Promise<void>((resolve) => {
+                    const script = document.createElement('script');
+                    script.src = url;
+                    script.onload = () => resolve();
+                    script.onerror = () => resolve(); // Still resolve - script is optional
+                    document.head.appendChild(script);
+                });
             }
         })
         .catch(() => {}); // Silently ignore - script is optional
 }
v3/pkg/application/application_options.go (1)

137-165: Consider adding TLS configuration options for production hardening.

The ServerOptions struct provides good defaults and documentation. For production server deployments, consider extending TLSOptions to support:

  • MinVersion (e.g., TLS 1.2 minimum)
  • Custom *tls.Config for advanced scenarios

This is optional since users can wrap/proxy the server, but could be valuable for standalone deployments.

💡 Optional enhancement for TLSOptions
// TLSOptions configures HTTPS for the headless server.
type TLSOptions struct {
	// CertFile is the path to the TLS certificate file.
	CertFile string

	// KeyFile is the path to the TLS private key file.
	KeyFile string

	// MinVersion is the minimum TLS version (e.g., tls.VersionTLS12).
	// Default: tls.VersionTLS12
	MinVersion uint16
}
v3/Taskfile.yaml (1)

395-401: Test filter may be overly restrictive.

The -run TestServerMode filter only runs tests matching that specific pattern. As the server mode codebase grows, you may want to run all server-tagged tests:

💡 Consider broader test coverage
   test:server:
     summary: Run server mode unit tests
     dir: 'pkg/application'
     cmds:
       - echo "Running server mode tests..."
-      - go test -tags server -v -run TestServerMode .
+      - go test -tags server -v .
       - echo "✅ Server mode tests passed"

Alternatively, if you want to keep tests isolated, consider using a naming convention like TestServer* for all server-related tests.

v3/examples/server/Taskfile.yml (1)

23-30: The sources directive serves no purpose here.

The sources directive is used to determine if a task should re-run based on file changes. However, with only a precondition and no commands, specifying sources has no effect. Consider removing it for clarity.

Suggested simplification
  build:frontend:
    summary: Ensures frontend assets exist (static HTML, no build needed)
    dir: frontend
-   sources:
-     - dist/index.html
    preconditions:
      - sh: test -f dist/index.html
        msg: "frontend/dist/index.html not found"
v3/pkg/application/browser_window.go (1)

66-68: Consider returning meaningful errors for unsupported operations.

Methods like GetScreen() return (nil, nil) and Print() returns nil. In server mode, these operations are genuinely unsupported. Returning a sentinel error (e.g., ErrNotSupported) would help callers distinguish "not supported" from "succeeded with no result."

Example pattern
var ErrNotSupportedInServerMode = errors.New("operation not supported in server mode")

func (b *BrowserWindow) GetScreen() (*Screen, error) { 
    return nil, ErrNotSupportedInServerMode 
}

func (b *BrowserWindow) Print() error { 
    return ErrNotSupportedInServerMode 
}
v3/examples/server/main.go (2)

57-60: Hardcoded URLs may be misleading when env vars override configuration.

The log messages display localhost:8080 but the actual host/port can be overridden via WAILS_SERVER_HOST and WAILS_SERVER_PORT environment variables. Consider reading the effective values or noting this caveat.


68-77: Goroutine lacks graceful shutdown.

The ticker goroutine runs indefinitely and doesn't stop when the application shuts down. While this is acceptable for an example, consider using a context or shutdown channel for clean termination.

♻️ Suggested improvement
+	ctx, cancel := context.WithCancel(context.Background())
+	app.OnShutdown(func() {
+		cancel()
+	})
+
 	// Emit periodic events to test WebSocket broadcasting
 	go func() {
 		time.Sleep(2 * time.Second) // Wait for server to start
 		ticker := time.NewTicker(5 * time.Second)
 		defer ticker.Stop()
-		for {
-			<-ticker.C
-			app.Event.Emit("server-tick", time.Now().Format(time.RFC3339))
-			log.Println("Emitted server-tick event")
+		for {
+			select {
+			case <-ctx.Done():
+				return
+			case <-ticker.C:
+				app.Event.Emit("server-tick", time.Now().Format(time.RFC3339))
+				log.Println("Emitted server-tick event")
+			}
 		}
 	}()

Note: You'll need to add "context" to the imports.

v3/pkg/application/application_server_test.go (1)

45-46: Consider using a retry loop instead of fixed sleep for server startup.

Using time.Sleep(200 * time.Millisecond) to wait for server startup can be flaky in CI environments under load. A retry loop with backoff would be more robust.

v3/pkg/application/websocket_server.go (3)

103-107: Minor race condition: len(b.clients) accessed after releasing lock.

The log statement accesses len(b.clients) after the mutex is released, which could show a stale count if another goroutine modifies the map concurrently. This is only a cosmetic issue for logging.

♻️ Suggested fix
 func (b *WebSocketBroadcaster) register(conn *websocket.Conn, window *BrowserWindow) {
 	client := &clientInfo{
 		conn:   conn,
 		window: window,
 	}
 	b.mu.Lock()
 	b.clients[conn] = client
+	clientCount := len(b.clients)
 	b.mu.Unlock()
-	b.app.info("WebSocket client connected", "id", window.Name(), "clients", len(b.clients))
+	b.app.info("WebSocket client connected", "id", window.Name(), "clients", clientCount)
 }

109-122: Same race condition in unregister for logging.

Similar to register, len(b.clients) is accessed after the mutex is released.

♻️ Suggested fix
 func (b *WebSocketBroadcaster) unregister(conn *websocket.Conn, runtimeClientID string) {
 	b.mu.Lock()
 	client := b.clients[conn]
 	delete(b.clients, conn)
 	if runtimeClientID != "" {
 		delete(b.windows, runtimeClientID)
 	}
+	clientCount := len(b.clients)
 	b.mu.Unlock()
 	conn.Close(websocket.StatusNormalClosure, "")
 	if client != nil {
-		b.app.info("WebSocket client disconnected", "id", client.window.Name(), "clients", len(b.clients))
+		b.app.info("WebSocket client disconnected", "id", client.window.Name(), "clients", clientCount)
 	}
 }

62-65: Security note: InsecureSkipVerify: true allows any origin.

This is appropriate for server mode where browser clients connect from various origins, but consider adding a configuration option in ServerOptions to restrict allowed origins for production deployments that need stricter CORS control.

Comment thread docs/src/content/docs/guides/server-build.mdx
Comment thread docs/src/content/docs/guides/server-build.mdx
Comment thread v3/examples/gin-routing/go.mod Outdated
Comment thread v3/examples/gin-service/go.mod Outdated
Comment thread v3/examples/ios/go.mod Outdated
Comment thread v3/internal/commands/build_assets/Taskfile.tmpl.yml
Comment thread v3/pkg/application/application_server_test.go
Comment thread v3/pkg/application/application_server.go Outdated
Comment thread v3/pkg/application/menu_linux.go
Comment thread v3/tests/window-visibility-test/go.mod Outdated
leaanthony and others added 3 commits January 25, 2026 11:28
- Fix corrupted test file with embedded terminal output
- Fix module name mismatch in gin-routing (was gin-example)
- Fix replace directive version mismatch in gin-service
- Fix placeholder module name in ios example (was changeme)
- Fix Dockerfile COPY path to work from both build contexts
- Fix bare URL in README (MD034 compliance)
- Fix comment accuracy in getScreens (returns error, not empty slice)
- Remove deprecated docker-compose version field
- Add port documentation in Taskfile template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add note about healthcheck wget not being available in distroless images
- Add !server build constraint to menu_windows.go and menu_darwin.go
- Downgrade window-visibility-test go.mod from 1.25 to 1.24 to match CI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
4 Security Hotspots
5.3% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@leaanthony leaanthony merged commit 9a363d7 into v3-alpha Jan 25, 2026
54 of 55 checks passed
@leaanthony leaanthony deleted the feature/headless-server-mode branch January 25, 2026 03:33
Grantmartin2002 pushed a commit to Grantmartin2002/wails that referenced this pull request Apr 29, 2026
* feat(v3): add server mode for headless HTTP deployment

Server mode allows Wails applications to run as pure HTTP servers
without native GUI dependencies. Enable with `-tags server` build tag.

Features:
- HTTP server with configurable host/port via ServerOptions
- WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides
- WebSocket event broadcasting to connected browsers
- Browser clients represented as BrowserWindow (Window interface)
- Health check endpoint at /health
- Graceful shutdown with configurable timeout
- Docker support with Dockerfile.server template and tasks

Build and run:
  wails3 task build:server
  wails3 task run:server
  wails3 task build:docker
  wails3 task run:docker

Documentation at docs/guides/server-build.mdx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(v3): add server mode for headless HTTP deployment

Server mode allows Wails applications to run as pure HTTP servers
without native GUI dependencies. Enable with `-tags server` build tag.

Features:
- HTTP server with configurable host/port via ServerOptions
- WAILS_SERVER_HOST and WAILS_SERVER_PORT env var overrides
- WebSocket event broadcasting to connected browsers
- Browser clients represented as BrowserWindow (Window interface)
- Health check endpoint at /health
- Graceful shutdown with configurable timeout
- Docker support with Dockerfile.server template and tasks

Build and run:
  wails3 task build:server
  wails3 task run:server
  wails3 task build:docker
  wails3 task run:docker

Documentation at docs/guides/server-build.mdx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Fix corrupted test file with embedded terminal output
- Fix module name mismatch in gin-routing (was gin-example)
- Fix replace directive version mismatch in gin-service
- Fix placeholder module name in ios example (was changeme)
- Fix Dockerfile COPY path to work from both build contexts
- Fix bare URL in README (MD034 compliance)
- Fix comment accuracy in getScreens (returns error, not empty slice)
- Remove deprecated docker-compose version field
- Add port documentation in Taskfile template

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address CodeRabbit review comments

- Add note about healthcheck wget not being available in distroless images
- Add !server build constraint to menu_windows.go and menu_darwin.go
- Downgrade window-visibility-test go.mod from 1.25 to 1.24 to match CI

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant