Hi everyone,
I'd like to share a project I've been building: SwiftWeb, an HTML-first web framework for writing full-stack web apps entirely in Swift — server rendering, typed routing, server actions, WebAssembly client islands, and distributed actors — plus a Cloudflare adapter that runs those actors inside Durable Objects.
- swift-web — the framework and the
swebCLI - swift-html — a framework-neutral typed HTML DSL and rendering engine
- swift-web-cloudflare — Cloudflare Workers + Durable Objects host adapter
It's a developer preview (swift-web 0.2.1). Requirements are honest but quirky right now — details below.
Live demo: Japan Event Calendar — a SwiftWeb app running on Cloudflare Workers. It lists sightseeing events across Japan on a calendar, server-rendered and localized into 14 languages, so you can browse it in yours.
The model
Apps follow SwiftUI's shape: App / Scene / @Page.
public struct MyApp: App {
public var body: some Scene {
HomePage()
}
}
@Page("/")
struct HomePage {
func body() -> some HTML {
div {
h1 { "Hello SwiftWeb" }
p { "Rendered by SwiftHTML and served through SwiftWeb." }
}
}
}
Pages render to HTML on the server by default — no JavaScript is shipped. Interactive parts are declared as ClientComponent and run in the browser as WASM (islands architecture), with SwiftUI-style @State:
struct ClientCounter: ClientComponent, Sendable {
@State private var count = 0
var body: some HTML {
Button("Count \(count)") {
count += 1 // runs in the browser
}
}
}
Each island declares when it loads (.eager / .visible / .interaction / .idle / .manual) and which logical WASM bundle it belongs to.
Distributed actors as the client-server boundary
This is the part I'd most like feedback on. A service contract is a @Resolvable protocol, the server implements it as an ordinary distributed actor, and a client component receives the resolved actor through @Actor — then calls it directly from the browser:
@Resolvable
protocol CounterServiceProtocol: DistributedActor
where ActorSystem == WebActorSystem {
distributed func increment() async throws -> Int
}
struct CounterClient: ClientComponent {
@Actor private var counter: any CounterServiceProtocol
func increment() async throws -> Int {
try await counter.increment() // browser WASM → server actor
}
}
There is no fetch, no endpoint definition, and no serialization code in user code — WebActorSystem handles the transport over HTTP (/_swiftweb/actors/invoke) and WebSocket (/_swiftweb/actors/ws), including server → client push.
The Cloudflare adapter routes each actor identity to its own Durable Object via idFromName, so actor state persists across calls. Durable Objects' one-instance-per-id model maps surprisingly cleanly onto distributed actor identity. This is verified end-to-end in workerd: envelope POST → Worker routing → per-identity DO → activation → state across calls, plus WebSocket sessions with server-side pushes.
Server actions
For mutations that should stay on the server, there's a macro-based server action layer:
actor CounterActions: PageOwnedServerActions {
@ServerAction(.post, "increment")
func increment(_ input: NoActionInput, context: ActionInvocationContext) async throws -> ActionResult {
_ = try await counterService.increment()
return .invalidate(.page)
}
}
.invalidate(.page) re-renders the page while preserving client island @State — only server-owned DOM is merged.
Ecosystem notes and requirements
- The host HTTP stack is built on swift-http-server (
NIOHTTPServer) and swift-http-api-proposal (HTTPAPIs). Both declare tools-version 6.4 and enableLifetimeDependence, so host builds currently require a Swift 6.4 toolchain (e.g. Xcode-beta). This is the main reason SwiftWeb ships as a developer preview — it rides the still-evolving next-generation server packages. Once 6.4 goes GA this friction disappears. - Browser WASM builds use the Swift 6.3.1 release toolchain with the
swift-6.3.1-RELEASE_wasmSDK, and JavaScriptKit for the DOM bridge. - Embedded Swift WASM is not supported — the browser graph needs
Distributed,Codable, and Foundation. Production WASM builds strip debug sections, runwasm-opt, and emit.gz/.brsidecars instead. - There's also a Vapor host adapter (swift-web-vapor), branch-pinned until Vapor 5 ships a stable release.
Trying it
mint install 1amageek/swift-web@0.2.1 sweb
sweb new MyApp --output MyApp
cd MyApp && sweb dev
A longer walkthrough is in my blog post: Building and Running Web Apps with Just Swift.
Feedback I'm looking for
- The
@Actor/.actor()injection API on top of Apple's@Resolvabledistributed actor model — does the shape feel right? - The WebSocket actor transport design, especially server → client push semantics
- Whether the per-identity Durable Object mapping holds up for real workloads
- General API shape of
@Page,PageGroup, and the server action macros
Design docs live in the repo (Platform Host Architecture, Actor Injection Design, WebSocket Actor Transport Design).
Feedback and PRs are very welcome. Thanks!