Contents

1amageek/swift-web

SwiftWeb is a Swift framework for server-rendered web applications with an

What You Build

flowchart LR
  App["SwiftWeb.App"] --> Scene["Scene and routes"]
  Scene --> Page["@Page"]
  Page --> Document["HTMLDocument"]
  Document --> HTML["server-rendered HTML"]
  Document --> Island["ClientComponent"]
  Island --> WASM["Swift WASM runtime"]
  WASM --> Browser["hydration, state, and events"]

| Layer | Responsibility | |---|---| | SwiftHTML | HTML elements, reusable Component values, documents, and rendering | | SwiftWeb | Application scenes, pages, routing, request context, actions, and actors | | SwiftWebUI | Layout, controls, themes, modifiers, and client components | | sweb | Project generation, generated packages, development server, independent services, Storyboard, and production builds |

Requirements

SwiftWeb pins the host toolchain and WASM SDK to the same snapshot.

| Item | Required value | |---|---| | Swift tools version | 6.4 | | Swiftly selector | 6.4.x-snapshot-2026-08-14 | | Swift toolchain | swift-6.4.x-DEVELOPMENT-SNAPSHOT-2026-08-14-a | | Browser SDK | swift-6.4.x-DEVELOPMENT-SNAPSHOT-2026-08-14-a_wasm | | Package platform | macOS 26.2 or newer |

Select the pinned Swift toolchain and install the matching WASM SDK. SwiftWeb automatically detects the toolchain in its standard macOS installation location. See Toolchain for discovery rules and optional overrides.

Quick Start

Release 0.14.0

Install the sweb executable from the 0.14.0 release with Mint:

export PATH="$SWIFT_WEB_TOOLCHAIN_BIN:$PATH"
mint install 1amageek/swift-web@0.14.0 sweb
sweb --help

Create and run an application:

sweb new MyApp --output .
cd MyApp
sweb dev

Open http://127.0.0.1:3000. If port 3000 is occupied, sweb dev selects the next available port and prints it.

The generated package depends on released versions of SwiftWeb and SwiftHTML:

// swift-tools-version: 6.4

import PackageDescription

let package = Package(
    name: "MyApp",
    platforms: [.macOS("26.2")],
    products: [
        .library(name: "MyApp", targets: ["MyApp"]),
    ],
    dependencies: [
        .package(url: "https://github.com/1amageek/swift-web.git", from: "0.14.0"),
        .package(url: "https://github.com/1amageek/swift-html.git", from: "0.16.1"),
    ],
    targets: [
        .target(
            name: "MyApp",
            dependencies: [
                .product(name: "SwiftHTML", package: "swift-html"),
                .product(name: "SwiftWeb", package: "swift-web"),
            ],
            swiftSettings: [
                .enableUpcomingFeature("ApproachableConcurrency"),
            ]
        ),
    ],
    swiftLanguageModes: [.v6]
)

Try the current checkout

To try changes from a checkout, build the CLI and run the example from that same checkout instead of mixing a released CLI with source from main:

git clone https://github.com/1amageek/swift-web.git
cd swift-web
swift build --product sweb --jobs 2
export PATH="$PWD/.build/debug:$PATH"
cd Examples/CounterApp
sweb dev

The bundled examples resolve SwiftWeb from the repository root. See CounterApp for its local hosting model.

Authoring Model

Application and routes

An application declares its route topology through App.body:

import SwiftWeb

public struct MyApp: SwiftWeb.App {
    public init() {}

    public var body: some Scene {
        HomePage()
        AboutPage()
    }
}

App owns only the declarative application definition. The host owns its server configuration and lifecycle, then renders the app through SwiftWeb's common rendering boundary:

import SwiftWebHTTPServerHost

let host = HTTPServerHost(hostname: "127.0.0.1", port: 8080)
try await host.run(MyApp())

App.run() is the command-line convenience over this same path. Host adapter authors should follow the Host Rendering Contract.

Actor connections

SwiftWeb uses Swift Distributed Actors for identity-scoped remote state. The concrete actor declaration remains the interface whether the actor is local or hosted by another Service application.

import Distributed
import SwiftWeb

distributed actor CounterService {
    typealias ActorSystem = WebActorSystem

    private var value = 0

    distributed func increment() async throws -> Int {
        value += 1
        return value
    }
}

Bind an externally hosted actor at the page or scene that consumes it:

CounterPage()
    .actor(CounterService.self, identity: "primary")

Inside a bound page, Server Action, or client component, resolve and call the same concrete type:

@RemoteActor private var counter: CounterService

func increment() async throws -> Int {
    try await counter.increment()
}

ActorGroup registers construction and hosting policy in the application that owns the actor; .actor(Type.self, identity:) selects a reference in the caller. Swift code owns the type and logical identity. sweb.json selects the Service build/deploy unit, and the deployment adapter supplies transport and endpoint templates. URLs, credentials, adapter names, and artifact names do not enter the actor call site. An application-owned actor can also coordinate an external API as a resource. The connection policy and destination examples describe Native hosts, Durable Objects, containers, connected peers, functions, queues, and external APIs, with their different ownership and failure contracts.

When the deployment supplies only a hostRoute, browser calls go through the primary application's same-origin Actor endpoint and authorization before the Service hop. An explicit clientRoute selects direct browser routing. See browser Service routing for the binding, authorization, and ownership requirements.

See the Actor authoring guide and the adapter contract.

HTTPS and WSS

The native host can terminate TLS directly through swift-tls-nio. Supply a server-side TLSConfiguration through the host-owned transport configuration:

import SwiftWebHTTPServerHost
import TLS

let identity = TLSIdentity(
    privateKey: privateKeyBytes,
    keyType: .ecdsaP256,
    certificateChain: [Certificate(der: leafCertificateDER)]
)
let transport = try HTTPServerTransportConfiguration.tls(
    .server(identity: identity, alpn: ["http/1.1"])
)
let host = HTTPServerHost(
    hostname: "0.0.0.0",
    port: 8443,
    transport: transport
)

try await host.run(MyApp())

The same listener serves HTTPS routes and WSS upgrades. TLS is installed before the HTTP/1.1 and WebSocket handlers, so route and WebSocket APIs continue to receive plaintext while transport bytes remain encrypted. An empty ALPN list is normalized to http/1.1; unsupported protocols fail during transport configuration instead of selecting a codec the host cannot serve.

Browser clients should derive WebSocket URLs from the page origin. An HTTPS page therefore resolves its socket endpoint to wss://; HTTP continues to use ws:// for explicitly plaintext development listeners.

Binary callbacks receive WebSocketBinaryBuffer, an immutable owner plus a readable range. Slicing and forwarding that value retain adapter-native storage without materializing [UInt8]; withUnsafeBytes provides a scoped borrow and copyBytes() is the explicit conversion for APIs that require an array.

Page documents

A static page returns a complete HTMLDocument:

import SwiftHTML
import SwiftWeb

@Page("/")
struct HomePage {
    var document: some HTMLDocument {
        PageDocument(
            title: "Home",
            description: "A SwiftWeb application."
        ) {
            main {
                h1 { "Hello SwiftWeb" }
                p { "Rendered on the server with SwiftHTML." }
            }
        }
    }
}

Use load() and document(_:) when rendering needs asynchronous data:

import SwiftHTML
import SwiftWeb

@Page("/profile")
struct ProfilePage {
    struct Model: Sendable {
        let displayName: String
    }

    func load() async throws -> Model {
        Model(displayName: "Taylor")
    }

    func document(_ model: Model) -> some HTMLDocument {
        PageDocument(title: model.displayName) {
            main {
                h1 { model.displayName }
            }
        }
    }
}

Component is the reusable, nestable authoring unit. HTMLDocument owns a complete page and cannot be nested inside a component. The full contract is in HTML Authoring Model.

SwiftWebUI

Add the SwiftWebUI product when you want higher-level layout and controls:

.product(name: "SwiftWebUI", package: "swift-web")

SwiftWebUI components lower into the same SwiftHTML graph, so raw HTML elements and SwiftWebUI values can be composed at one page boundary.

import Foundation
import SwiftHTML
import SwiftWeb
import SwiftWebUI

@Page("/")
struct HomePage {
    var document: some HTMLDocument {
        PageDocument(title: "Home") {
            main {
                VStack(spacing: .large) {
                    Text("Hello SwiftWeb").as(.h1)
                    Link("About", destination: URL(string: "/about")!)
                }
                .frame(maxWidth: 720)
            }
        }
    }
}

Browser components

ClientComponent runs in the generated standard Swift WASM runtime. Its @State values and event handlers remain in the browser:

import SwiftHTML
import SwiftWebUI

public struct Counter: ClientComponent {
    @State private var count = 0

    public init() {}

    public var content: some Component {
        VStack(spacing: .small) {
            Text("Count: \(count)")
            Button("Increment") {
                count += 1
            }
        }
    }
}

The default contract places small client components in the eager main bundle. Large or deferred islands can declare a loading and bundle policy:

public static let loadPolicy: LoadPolicy = .visible
public static let bundle: BundlePolicy = .named("analytics")

Available load policies are .eager, .visible, .interaction, .idle, and .manual. See Client Bundle Loading for bundle resolution, ownership, and production behavior.

Development Workflow

sweb dev maintains desired source state and the currently serving worker. It materializes generated packages, rebuilds changed browser/server paths, swaps a ready worker, and recovers from build failures without discarding the last good application.

flowchart LR
  Edit["edit Sources"] --> Dev["sweb dev"]
  Dev --> Prepare["materialize .swiftweb/generated"]
  Prepare --> Build["build WASM and server worker"]
  Build --> Serve["serve latest successful generation"]
  Serve --> HMR["browser HMR or page patch"]
  HMR --> Edit

Generated content is build output. Keep application changes in Package.swift and Sources; do not edit .swiftweb/generated directly.

| Command | Purpose | |---|---| | sweb new <Name> [--output <directory>] | Create a minimal application | | sweb new <Name> --ai | Create a chat-oriented SwiftWebUI application | | sweb new <Name> --adapter <owner/repository> | Add an adapter package and configured production environment | | sweb prepare [--environment <name>] [--runtime standard|embedded] | Resolve adapters and materialize configured environments | | sweb xcode | Refresh and open .swiftweb/generated/dev | | sweb dev [--environment <name>] [--host <host>] [--port <port>] | Build and run the selected environment locally | | sweb storyboard | Runs the declared application through the Storyboard selection contract; the catalog is an independent repository | | sweb build [--environment <name>] [--runtime standard|embedded] | Build and verify the selected environment | | sweb deploy [--environment <name>] [--runtime standard|embedded] | Build, verify, and deploy the selected environment | | sweb clean [--storyboard] [--swiftpm] [--all] | Remove selected generated output |

All package commands accept --package-path <directory>. Lifecycle commands select their default environment from the source-controlled sweb.json.

Run sweb xcode to use the generated <AppName>-dev scheme in Xcode:

cd MyApp
sweb xcode

Production Builds and Deployment

Build the complete selected environment. Service adapters build independent service applications, the Host adapter owns primary application compilation, and the Deployment adapter owns platform validation:

cd MyApp
sweb build --environment production

Deploy only after the same environment has passed prepare and build:

sweb deploy --environment production

sweb deploy reruns prepare and build before the deployment operation. Remote state changes remain isolated to selected Service and Deployment adapter deploy tasks.

The browser runtime profile is selected at the build boundary. Application source keeps the same actor call surface in both profiles:

sweb build --environment production --runtime embedded

Embedded builds use the pinned matching Embedded WASM SDK. The development server remains a Standard WASM workflow; use prepare, build, or deploy for Embedded artifacts.

Project Layout

MyApp/
├─ Package.swift
├─ sweb.json                  source-controlled environments
├─ Sources/MyApp/
│  ├─ App.swift
│  ├─ Routes/
│  └─ Components/
└─ .swiftweb/                 generated; do not edit
   └─ generated/
      ├─ environments/<name>/workspace/
      │  └─ services/<service-name>/
      ├─ dev/
      ├─ server/
      └─ wasm/

SwiftWeb itself is split into runtime, browser, UI, development, and host targets. The documentation index maps each current contract to its owning area.

Host, Deployment, and Service Adapters

Deployment integrations live outside the core package. sweb new adds the selected adapter as a SwiftPM dependency and writes its environment selection to sweb.json:

sweb new Chat --ai --adapter owner/repository --output .
sweb new App --adapter owner/repository --output .

The adapter repository contract is documented in Host, Deployment, and Service Adapter Contract. Service applications remain build/deploy units rather than Swift-facing interfaces. Actor connections retain the concrete Swift Distributed Actor surface described by the Actor integration design.

Examples

| Example | Demonstrates | |---|---| | HelloWorld | Minimal app, static @Page, SwiftHTML, and SwiftWebUI rendering | | CounterApp | .actor(Type.self, identity:), local Actor hosting, browser and server @RemoteActor calls, hydration, and Server Actions |

cd Examples/HelloWorld
sweb dev

Documentation

Read the changelog for release-level changes, then use the documentation index for current public contracts, architecture decisions, and verification runbooks.

Contributing

Use the pinned toolchain for every validation command. Run the non-Metal Native tests with SwiftPM. Bound compilation separately so a cold build does not consume the test execution budget:

scripts/swift-test-timeout.sh 1200 -- swift build --build-tests --jobs 2
scripts/swift-test-timeout.sh 120 -- swift test --skip-build

Use --filter <SuiteOrTestName> for focused runs. Browser tests are opt-in; the Service Actor HTTP boundary gate checks forwarding between two native hosts through Chromium. It is separate from the Swift-WASM hydration and development-loop gate, which requires the full Chromium suite plus WebKit hydration, navigation-free Actor mutation, and reload persistence. WebKit launch is checked before Swift builds; both counter npm commands select this same required gate:

cd Tests/BrowserE2E
npm run install-webkit
npm run counter-wasm

See Development Reconciler Verification for the required environment and acceptance conditions.

License

SwiftWeb is available under the MIT License.

Package Metadata

Repository: 1amageek/swift-web

Default branch: main

README: README.md