Glaze
Glaze builds desktop applications with a Racket backend and a Web frontend rendered inside a native OS window. Windows uses WebView2, macOS uses WKWebView, and Linux uses WebKitGTK.
Human-first. Agent-native. Local by design. New projects include an agent instruction contract and native-window verification script. The CLI exposes machine-readable project inspection and runtime diagnostics without changing Glaze’s human-readable workflow.
1 Quick Start
$ raco pkg install --auto glaze |
$ raco glaze init myapp |
$ cd myapp |
$ raco glaze inspect --json |
$ raco glaze doctor --json |
$ raco glaze verify |
$ racket main.rkt |
# or: raco glaze dev |
A Glaze application is native-GUI only. If its native WebView cannot start, application startup fails with platform-specific installation or repair guidance. Glaze never substitutes a system-browser tab for the desktop window.
2 Application Lifecycle
| (require glaze/app) | package: glaze |
When the native window closes, the local server is stopped and the procedure returns (values 'webview shutdown). If native WebView startup fails, Glaze first stops the local server and then propagates an actionable startup error. There is intentionally no browser-fallback option.
#:api-token may be a string or #t. With #t, Glaze generates a random capability token and uses a one-time bootstrap URL to set an HttpOnly cookie for the embedded frontend. #:on-error receives API handler failures. #:check-update wires an update manifest into the application lifecycle.
When #:capability is supplied, run-app automatically creates an API token if necessary. Every API route then requires a declared and granted permission; this mode is default-deny.
With #:window-state #t, #:app-id selects a platform config path where Glaze saves outer position, size, and maximized state at close and restores them on the next launch. An explicit path may be supplied instead. Stale geometry is clamped to the current virtual desktop so a disconnected monitor cannot strand the window.
parameter
(current-api-token token) → void? token : string?
3 Local Server
| (require glaze/server) | package: glaze |
| (require glaze/browser) | |
procedure
(start-server [ #:port port #:public-dir public-dir #:api api #:events events #:api-token api-token #:capability capability #:serve-api-client? serve-api-client?])
→
exact-nonnegative-integer? procedure? port : exact-nonnegative-integer? = 8080 public-dir : (or/c string? path?) = "public" api : (listof route?) = '() events : (or/c #f event-bus?) = #f api-token : (or/c #f string?) = #f capability : (or/c #f capability?) = #f serve-api-client? : boolean? = #t
The low-level server requires an explicit, non-empty #:api-token whenever #:capability is supplied, keeping runtime authority bound to the embedded WebView rather than an unauthenticated loopback caller.
4 JavaScript Bridge
| (require glaze/api) | package: glaze |
| (require glaze/api-macros) | |
The embedded frontend calls Racket through ordinary same-origin HTTP requests. This keeps the bridge easy to inspect and test with normal developer tools.
procedure
(GET path handler [ #:permission permission #:resource resource]) → route? path : string? handler : procedure? permission : (or/c #f symbol? string?) = #f resource : (or/c #f procedure?) = #f
procedure
(POST path handler [ #:permission permission #:resource resource]) → route? path : string? handler : procedure? permission : (or/c #f symbol? string?) = #f resource : (or/c #f procedure?) = #f
procedure
(PUT path handler [ #:permission permission #:resource resource]) → route? path : string? handler : procedure? permission : (or/c #f symbol? string?) = #f resource : (or/c #f procedure?) = #f
procedure
(DELETE path handler [ #:permission permission #:resource resource]) → route? path : string? handler : procedure? permission : (or/c #f symbol? string?) = #f resource : (or/c #f procedure?) = #f
The resource procedure receives the same request and captured path parameters as the route handler. Its result is checked against the active scoped permission before the handler can run. define-api-routes accepts the same #:permission and #:resource options after its route path.
5 Runtime Capabilities
| (require glaze/capability) | package: glaze |
procedure
id : (or/c symbol? string?) authorize : procedure?
procedure
(path-permission id #:allow allow-roots [ #:deny deny-roots]) → any/c id : (or/c symbol? string?) allow-roots : list? deny-roots : list? = '()
procedure
(command-permission id #:allow allow-programs [ #:deny deny-programs #:arguments arguments-ok?]) → any/c id : (or/c symbol? string?) allow-programs : list? deny-programs : list? = '() arguments-ok? : procedure? = (lambda (arguments) #t)
procedure
program : path-string? arguments : list?
procedure
(capability-authorized? capability permission [ resource]) → boolean? capability : capability? permission : (or/c symbol? string?) resource : any/c = #f
parameter
(current-capability-id id) → void? id : (or/c #f string?)
6 Scoped Filesystem
| (require glaze/filesystem) | package: glaze |
The default prefix generates client functions such as glaze.api.fsReadText, glaze.api.fsWriteFile, and glaze.api.fsMove. Binary payloads use base64 strings in JSON.
procedure
(fs-create-dir! path [ #:recursive? recursive?]) → void? path : path-string? recursive? : boolean? = #t
procedure
path : path-string? recursive? : boolean? = #f
procedure
(fs-copy! source destination [ #:replace? replace?]) → void? source : path-string? destination : path-string? replace? : boolean? = #f
procedure
(fs-move! source destination [ #:replace? replace?]) → void? source : path-string? destination : path-string? replace? : boolean? = #f
A route handler receives the web-server request followed by any captured :param path values. Returning a jsexpr produces a JSON 200 response; a full response value may also be returned, including a streaming response (streaming-response / event-stream-response below).
procedure
status : exact-nonnegative-integer? message : string?
procedure
(streaming-response writer [ #:mime mime #:headers headers]) → response? writer : (-> output-port? any) mime : bytes? = #"application/octet-stream" headers : (listof header?) = '()
A handler that raises before returning still maps to a 500 JSON (nothing is on the wire yet); an exception inside the writer closes the connection mid-stream, so wrap your own errors there if truncation is unacceptable. The typical use is proxying a streaming LLM endpoint — tokens reach the page as they arrive, API keys stay in Racket, and the page never makes a cross-origin call:
(GET "api/llm/chat" (lambda (req) (streaming-response #:mime #"application/x-ndjson" (lambda (out) (for ([delta (in-llm-deltas (request-json-body req))]) (displayln (jsexpr->string (hasheq 'delta delta)) out) (flush-output out))))))
procedure
(event-stream-response sender [ #:headers headers]) → response? sender : procedure? headers : (listof header?) = '()
event: name\ndata: <json> |
(GET "api/sse/demo" (lambda (req) (event-stream-response (lambda (send) (send 'delta (hasheq 'text "Hel")) (send 'delta (hasheq 'text "lo")) (send 'done (hasheq 'ok #t))))))
(define-api-routes api [(POST "api/counter/bump") (bump [delta exact-nonnegative-integer? 1]) (hasheq 'count (add1 delta))])
The generated /glaze/api.js exposes route-specific functions plus glaze.call(...) and glaze.on(...).
7 Event Push
| (require glaze/events) | package: glaze |
Glaze uses same-origin Server-Sent Events for backend-to-frontend push.
procedure
bus : event-bus? name : (or/c symbol? string?) data : jsexpr?
8 Native WebView
| (require glaze/webview/main) | package: glaze |
The native WebView is an application prerequisite, not an optional rendering mode. The backends are WebView2 on Windows, WKWebView on macOS, and WebKitGTK on Linux.
procedure
(open-window url [ #:title title #:width width #:height height #:devtools? devtools? #:window-state window-state #:on-close on-close]) → webview? url : string? title : string? = "Glaze" width : exact-positive-integer? = 1024 height : exact-positive-integer? = 768 devtools? : boolean? = #f window-state : (or/c #f path-string?) = #f on-close : (-> any) = (lambda () (void))
There is no #:fallback-browser? keyword.
procedure
(open-webview url [ #:title title #:width width #:height height #:devtools? devtools? #:window-state window-state #:on-close on-close]) → webview? url : string? title : string? = "Glaze" width : exact-positive-integer? = 1024 height : exact-positive-integer? = 768 devtools? : boolean? = #f window-state : (or/c #f path-string?) = #f on-close : (-> any) = (lambda () (void))
procedure
webview : webview? path : path-string?
procedure
wv : webview? dest : (or/c #f string? path?) = #f
procedure
wv : webview? width : exact-positive-integer? height : exact-positive-integer?
8.1 Startup Dependency Feedback
When native startup fails, Glaze reports the backend error and remediation. Typical guidance includes:
Windows: install or repair Microsoft Edge WebView2 Runtime (Evergreen), with a winget command and Microsoft’s official download page.
Debian/Ubuntu: sudo apt install libgtk-3-0 libwebkit2gtk-4.1-0.
Fedora: sudo dnf install gtk3 webkit2gtk4.1.
Arch: sudo pacman -S gtk3 webkit2gtk-4.1.
macOS: WKWebView is built in; use a logged-in graphical session and report the preserved backend error if initialization still fails.
Set environment variable GLAZE_NO_STARTUP_DIALOG to 1 to suppress the interactive error dialog while retaining the exception. Dialogs are also suppressed automatically under common CI environments.
9 System Integrations
| (require glaze/sys) | package: glaze |
procedure
title : string? body : string? = "" subtitle : string? = ""
These helpers are best-effort integrations. Their failure semantics are separate from the WebView startup contract: the WebView is required for the application itself, while an optional integration may report failure without changing the application’s rendering model.
10 System Tray
| (require glaze/tray) | package: glaze |
procedure
(make-tray #:icon icon #:tooltip tooltip #:menu menu [ #:on-event on-event]) → tray? icon : any/c tooltip : string? menu : list? on-event : procedure? = (lambda (e) (void))
The tray remains an optional capability. If its native backend is unavailable, Glaze may use an inert tray stub; this does not weaken the mandatory native WebView contract for the main window.
11 Security
The local server binds to loopback and validates Host headers against 127.0.0.1, localhost, and [::1] to reduce DNS rebinding risk.
With #:api-token, API routes and the SSE stream require a capability. run-app opens the native WebView at a one-time bootstrap URL; the server exchanges the token for an HttpOnly cookie and redirects to the clean path. Programmatic clients may use the X-Glaze-Token header.
With #:capability, the server additionally enforces named permissions and resource scopes. Routes without #:permission, routes whose permission is absent, and resources outside a granted scope return 403 before the handler runs. Grant 'glaze:events to authorize the SSE endpoint.
This is defense in depth against casual local callers, not isolation from other processes running as the same OS user.
12 Update Checks
| (require glaze/update) | package: glaze |
procedure
(check-update manifest-url [ #:current-version current-version]) → (or/c #f hash?) manifest-url : string? current-version : string? = "0.0.0"
procedure
path : (or/c string? path?) expected-hex : string?
For installed applications, the signed updater is the preferred path. An update-manifest binds the application id, SemVer version, release channel, minimum supported version, staged rollout percentage, and a set of platform/architecture update-artifact values into one Ed25519-signed payload. Artifact records include a bounded size, SHA-256 digest, installer kind and arguments, and an optional second Ed25519 signature.
procedure
(fetch-update-manifest manifest-url public-key [ #:key-id key-id #:maximum-bytes maximum-bytes]) → update-manifest? manifest-url : string? public-key : path-string? key-id : (or/c #f string?) = #f maximum-bytes : exact-positive-integer? = (* 1024 1024)
procedure
(select-update config manifest) → (or/c #f update-candidate?)
config : updater-config? manifest : update-manifest?
procedure
(download-update config candidate destination) → path? config : updater-config? candidate : update-candidate? destination : path-string?
procedure
(make-install-plan candidate downloaded-path [ #:backup-path backup-path] #:install install [ #:restart restart #:rollback rollback]) → install-plan? candidate : update-candidate? downloaded-path : path-string? backup-path : (or/c #f path-string?) = #f install : procedure? restart : procedure? = void rollback : procedure? = void
procedure
(make-replace-install-plan candidate downloaded-path target-path #:backup-path backup-path [ #:restart restart]) → install-plan? candidate : update-candidate? downloaded-path : path-string? target-path : path-string? backup-path : path-string? restart : procedure? = void
Release automation can validate and sign a payload with raco glaze manifest-sign, then verify the wrapper and pinned key id with raco glaze manifest-verify.
13 Licensing
| (require glaze/license) | package: glaze |
Glaze includes an offline RSA-2048/SHA-256 licensing helper backed by the system openssl command.
procedure
(issue-license #:private-key private-key #:product product #:subject subject [ #:expiry expiry #:machine-id machine-id #:out output]) → path? private-key : path-string? product : string? subject : string? expiry : (or/c #f string?) = #f machine-id : (or/c #f string?) = #f output : (or/c string? path?) = "app.license"
procedure
(validate-license license-file #:public-key public-key #:product product [ #:machine-id machine-id]) → hash? license-file : (or/c string? path?) public-key : path-string? product : string? machine-id : string? = (machine-id)
14 File Dialogs
| (require glaze/dialogs) | package: glaze |
procedure
(pick-file [ #:title title #:directory directory #:filters filters]) → (or/c #f path?) title : (or/c #f string?) = #f directory : (or/c #f path-string?) = #f filters : list? = '()
procedure
(pick-files [ #:title title #:directory directory #:filters filters]) → (listof path?) title : (or/c #f string?) = #f directory : (or/c #f path-string?) = #f filters : list? = '()
15 Deep Links and Launch at Login
| (require glaze/deeplink) | package: glaze |
| (require glaze/autolaunch) | |
procedure
(ensure-url-scheme! scheme [ #:app-name app-name]) → any/c scheme : string? app-name : string? = scheme
16 Packaging
| (require glaze/build) | package: glaze |
procedure
(build-app [ #:entry entry #:name name #:version version #:icon icon #:out-dir out-dir #:embed-dlls? embed-dlls? #:installer? installer? #:sign sign #:entitlements entitlements #:no-hardened-runtime? no-hardened-runtime? #:timestamp-url timestamp-url #:notarize-profile notarize-profile #:url-schemes url-schemes]) → path? entry : (or/c string? path?) = "main.rkt" name : (or/c #f string?) = #f version : (or/c #f string?) = #f icon : any/c = #f out-dir : (or/c string? path?) = "dist" embed-dlls? : boolean? = #f installer? : boolean? = #f sign : (or/c #f string?) = #f entitlements : any/c = #f no-hardened-runtime? : boolean? = #f timestamp-url : (or/c #f string?) = #f notarize-profile : (or/c #f string?) = #f url-schemes : list? = '()
Installer-toolchain absence may degrade an installer request to an archive with a loud warning. That packaging fallback is unrelated to runtime startup: the built application still requires its native WebView.
17 CLI Commands
raco glaze init <name> Create a native desktop project |
raco glaze dev Run the project's native main.rkt |
raco glaze build Build a distributable / installer |
raco glaze keygen [--out <dir>] Create an RSA keypair for licenses |
raco glaze license sign|verify Sign or verify license files |
raco glaze help Show help |
There is intentionally no browser-mode dev or serve command. Development and production use the same native WebView startup path.