Wisp

Type to search every docs page and heading.

Deploying

Run wisp build, copy the binary, run it. Pick the build for your host:

You haveUse
VPS or serverwisp build; wisp service install keeps it running (VPS)
Container host (Fly.io, Railway, Render, Cloud Run, Azure)wisp build --docker (Docker)
Static host (GitHub Pages, GitLab Pages, S3)wisp build --static (or --spa)
Edge or serverless (Cloudflare, Deno Deploy, Vercel, Netlify, Amplify, Firebase, Azure Static Web Apps)wisp build --target <host> (targets)
AWS Lambda / Bun / Node--target lambda / --target bun / --target node

An app that signs cookies needs WISP_SECRET (32+ random characters) on every host. Logs, metrics and traces: Observe. Step by step for each host: Hosting.

A plain wisp build inside a host's CI picks the target from its variables and says so: WORKERS_CI or CF_PAGES (cloudflare), VERCEL (output in .vercel/output), NETLIFY, DENO_DEPLOYMENT_ID (deno), AWS_APP_ID (node). --target native forces the plain binary.

wisp deploy init <host> writes a config:

HostWrites
cloudflare, deno, vercel, netlify, lambda, pages (GitHub Pages).github/workflows/deploy.yml: build and deploy on push to main; line 1 names the secrets; --force replaces it
fly, render, railwaythat host's config, and a Dockerfile if none

Binary

wisp build makes one release binary with static files and styles inside. It listens on $HOST:$PORT (0.0.0.0:3000 in release).

HTTP/2 without a proxy: the h2 feature serves h2c with prior knowledge on the same port (for a proxy that speaks h2c, or curl --http2-prior-knowledge). HTTP/1 is unchanged and pays nothing. There is no TLS in the server, so browsers still want the proxy.

# Cargo.toml
wisp = { version = "..", features = ["h2"] }

Static and SPA

wisp build --static [--out site] writes dist/: every parameterless page as about/index.html, plus static/ and the /_app files (.maps with --sourcemap). Forms need a server: the export warns for each page with actions, and for each exported page whose HTML holds a form that posts (method="post", action="?/name"), whichever page its action is on. A [params] route lists its pages:

<!-- src/routes/blog/[slug]/+page.wisp (or its +page.rs) -->
---
fn entries() -> Vec<&'static str> {
    vec!["hello", "second-post"]
}
---
  • entries returns a String or &str per param, or a tuple in path order. For [[optional]] and [...rest] an empty string leaves it out.
  • A route with actions or a +server.rs needs a server (the export warns).
  • --spa is --static plus an index.html fallback (Netlify: /* /index.html 200 in _redirects; Cloudflare Pages with no 404.html).
  • A const SSR: bool = false; page whose [params] have no entries is written once (params 0) to _app/spa/N.html. index.html lists them, and wisp.js draws the one whose route fits, with that address's params. It gets its data from +page.js.

Prerender

---
const PRERENDER: bool = true;
// with [params]
fn entries() -> Vec<&'static str> {
    vec!["hello", "second-post"]
}
let post = db::post(&slug).await?;
---
  • wisp build builds the binary, runs it once (init runs) to render those pages, builds again with the bytes inside, and serves them as they are (ETag, 304).
  • Before that (cargo run, other targets) each worker keeps the first render.
  • One render serves all, so cx in statements, markup or load is a build error. Its layouts render once, as for a request without cookies.
  • --static prerenders every page.
  • Under the hood the build calls wisp::prerender on the app; you never call it yourself.

Service

wisp build, then wisp service install (as root or administrator) runs the release binary from the app folder as an OS service that starts at boot. Then start, stop, status, uninstall.

OptionWhat it does
--user <name>run as that user
--port <n>listen port
--name <service>service name (default: the package name)
--dry-runprint what would be written and run, change nothing
  • Linux: /etc/systemd/system/<name>.service with Restart=on-failure, EnvironmentFile=-/etc/<name>.env (made 0600 if missing: put WISP_SECRET there), WorkingDirectory, LimitNOFILE=1048576, User= when given, and AmbientCapabilities=CAP_NET_BIND_SERVICE for --port below 1024. Then daemon-reload, enable, start. The --user must read the app folder.
  • macOS: /Library/LaunchDaemons/wisp.<name>.plist, loaded with launchctl.
  • Windows: a scheduled task at startup (schtasks, as SYSTEM). A true Windows service must answer the Service Control Manager, which needs unsafe FFI that Wisp does not have, so stop ends the process without draining.

SIGTERM (systemd stop, launchd) and Ctrl+C make the runtime stop accepting and drain for up to 10 seconds.

Docker

wisp build --docker              # --force replaces existing files
docker build -t my-app .
docker run -p 3000:3000 -e WISP_SECRET=... my-app
  • Writes a two-stage Dockerfile (rust:slim then debian:stable-slim, HOST=0.0.0.0, WISP_DATA=/data) and .dockerignore.
  • Saved tables live in /data: mount a volume (-v my-app-data:/data).
  • Docker's default seccomp refuses io_uring, so the server uses an epoll per worker. A profile allowing io_uring_setup, io_uring_enter, io_uring_register brings it back.

Is This Page Useful?

Edit This Page