This Website Is Served by an OS Written in Swift

This Website Is Served by an OS Written in Swift

TL;DR: I wrote an operating system in Embedded Swift for 64-bit ARM: kernel, drivers, TCP/IP stack, and userland. It runs on a Hetzner Cloud ARM VM and serves its own website, swiftos.tech, over HTTPS — nginx on my own kernel, my own syscall ABI, my own libc port. No Linux inside the guest. It also runs Node.js (jitless), hosts SQLite on its own filesystem, and schedules across four cores. The git history is 1,235 commits in two months, much of it written in pair with a coding agent under strict per-milestone gates — more on that below. Along the way, a text editor found a race in my scheduler and autoconf burned a six-hour CI budget executing a binary that had no OS to run on. Disclaimer. SwiftOS is a hobby research project, not a production OS. I write about Swift and on-device AI, so I'm not neutral. The repository is public, and make test runs the host unit tests and the in-QEMU boot assertions in one command; claims about the cloud deployment you can check by visiting the site. Go look first Open swiftos.tech. The response comes from nginx running on SwiftOS. Let me be precise about the stack, because Hacker News will be. The machine is a Hetzner Cloud ARM VM, so there is a hypervisor below me — KVM, with virtual firmware. "Bare metal" would be a stretch and I'm not claiming it. The accurate claim: SwiftOS is the only operating system on that machine's boot disk, and inside the guest there is no Linux and no compatibility layer. nginx is statically linked against my newlib port, talks to my syscall ABI, and its packets go through a TCP/IP stack written in Swift that lives in my kernel. Getting there meant teaching the kernel ACPI tables, GICv3, and PCIe ECAM. QEMU's polite device-tree world does not prepare you for real firmware. About TLS, since someone will check the fingerprint: public HTTPS is terminated by nginx linked against the ported OpenSSL. My own Swift TLS 1.3 stack exists, but its job is the ACME client that fetches the Let's Encrypt certificate. The homegrown TLS is not on the public serving path, and won't be until it has seen a lot more hostile input. What's actually in the box A tour, roughly in the order it was built: Boot: a minimal AArch64 PE32+ EFI loader calls ExitBootServices and hands off to the Swift kernel at EL1. Hardware is discovered from the device tree (or ACPI on the cloud VM), not hardcoded. Isolation: MMU-based, one address space per process, preemptive scheduling, fork/exec/waitpid. SMP works, tested at -smp 4, including cross-CPU TLB shootdown. Security: there is no uid == 0. A process carries a typed security context — principal, session, and a capability mask that the VFS checks at open time. (Capability purists will note this is a permission mask over a typed context, not object capabilities with unforgeable handles. Correct. The handle-based model is the current phase of work.) Filesystem: three tiers. An immutable, signed, packed read-only base image; a RAM tmpfs where data intentionally dies at reboot; and a persistent /data tier whose durability contract is deliberately narrow: there is no journal, and the guarantee is that fsync actually reaches the block device. Crash-consistency of anything beyond that is the application's job. SQLite runs on /data with its own rollback journal, which is exactly the arrangement that contract was written for. Networking: an in-kernel TCP/IP/UDP/ARP/ICMP/DNS stack whose protocol core is sans-IO pure Swift, so the same state machines compile into the kernel and into host unit tests on macOS. Userland: native Swift coreutils (ls -l, ps, top, cat, a calculator REPL that exists mainly to prove ARC works in userland) over a small bridge library, plus httpd, a poll()-driven concurrent HTTP server. Also in Swift: TLS 1.3, X.509 verification, and the ACME client. Real software: a ports tree cross-builds Lua, zlib, OpenSSL, pcre2, SQLite, nginx and others against the SwiftOS ABI. Node.js 24.16 runs on V8 in --v8-lite-mode — jitless, because SwiftOS refuses RWX pages. node -e "console.log(6 * 7)" prints 42 and npm --version prints 11.13.0. I'll be straight with you: printing a version string is a long way from npm install working, and I haven't got that far. A toy LLM: llmd serves TinyStories inference over HTTP from a GGUF model bundle, on a llama2-style engine written in Swift. EL0 userland native Swift coreutils + tools; nginx, Node.js (ported) ---------- syscall boundary: SwiftOS POSIX-like ABI, not Linux ABI EL1 kernel Embedded Swift: mm, sched, vfs, net, drivers, security arch/aarch64 boot + exception vectors + context switch (C + asm) The project's own code is about 91,000 lines of Swift (roughly 10,000 of that is tests), 5,000 lines of C, and 500 lines of assembly. The C lives where it has to: the UEFI loader, volatile MMIO accessors, and the syscall/runtime shims — plus third-party code like busybox and newlib, which stay in their own languages. The rule in the repo is Swift by default; every C file needs a written justification. Why Swift, briefly Two reasons. First, I wanted to know where the floor is. Swift has been drifting downward for years — server-side, then microcontrollers, then Embedded Swift in the official toolchain — and everyone repeats "Swift can go low-level now" without checking how low. A kernel is a thorough way to check. Second, I wanted a small modern core for hosting applications and AI models: capability-based security instead of root, immutable signed images instead of a mutable root filesystem, fast deterministic boot. You don't get that by trimming Linux. The design rule here is minimalism by removing legacy rather than emulating it: no Linux ABI, no dynamic linker, no containers. What Embedded Swift actually gives you: no Foundation, no full standard library, no reflection. You keep the type system, optionals, generics, enums with payloads, ~Copyable structs with deinit for resource ownership — and ARC, once you've built a heap for it. In the kernel the style is value types and Unsafe* pointers at the bottom, classes sparingly and only after the allocator is up. You write your own runtime hooks and exception vectors. It's a different sport from iOS development. Where the design ideas come from Not much of it comes from Unix, and some of it comes from machine rooms most of my generation never saw. The security model borrows from KeyKOS and EROS — with the honest footnote that SwiftOS's current permission-mask model is far weaker than their object capabilities, and closing that gap is the active work. Cells, the isolation domains, are closer to mainframe LPARs and Solaris Zones than to Docker: the partition is a service of the platform, not of a daemon running on top of it. Immutable A/B images are firmware discipline; ChromeOS and CoreOS brought that to Linux years ago, and spacecraft had it earlier still. The mainframe thread is the one I went digging in deliberately. That world treated recovery as a designed path rather than an emergency: explicit boot profiles (normal, previous-good, safe, diagnostics), watchdogs tied to specific recovery policies, an operator console that is not the same thing as a user shell, and a clean split between commands that change state and telemetry that reports it. I want all of that in SwiftOS. Today, only part of it is running code — A/B image validation paths exist and are tested, while supervised restart and safe mode are still targets in the architecture doc. What I refused to import from that world: JCL-shaped configuration languages and the batch-only operating model. If the project reaches the end of that list, I think it has a real niche — single-purpose appliance VMs and small fleets that host applications and models without carrying a Linux distribution's surface area. That's a long way off, and none of it is a promise. Where Swift earned its keep The syscall dispatcher is an exhaustive switch over an enum: add a syscall, forget a handler, and the kernel doesn't compile. TCP states are an enum with payloads, so a whole class of "impossible state" bugs died at compile time instead of at 2am. And authorization checks are ordinary typed code the compiler can see. From vfsOpen, verbatim: // M13: capability enforcement. Reading the filesystem needs capFsRead; // writing/creating (only the tmpfs is writable) needs capTmpWrite. if wantRead && (caps & capFsRead) == 0 { return Errno.access.code } if wantWrite && (caps & capTmpWrite) == 0 { return Errno.access.code } Nothing magic — but the security context that caps comes from is a typed value threaded through the process structure, not an integer convention scattered across the codebase. What paid off most was making the network protocol core sans-IO: pure functions and state machines, no MMIO, no I/O of any kind. The same TCP code runs in host unit tests on macOS and in the kernel. Most protocol bugs got caught in milliseconds on the host instead of through a hung QEMU serial console. Time bugs still made it through, of course. My favorite: a fast local client's SYN, ACK, data, and FIN can all arrive within a single poll of the NIC, so the connection races past established into closeWait before accept ever looks at it — and accept, which sampled the live state, never returned. The fix is a one-shot "handshake happened" latch. Types are great at impossible states and useless against timing. Where it hurt Two stories, both expensive. A text editor found a scheduler race my tests didn't. Porting busybox vi crashed the kernel with intermittent EL1 data aborts — wild stack pointer, wild PC, right after vi drew its screen. The trigger was poll(): vi polls stdin with a timeout to disambiguate ESC sequences, and my vfsPoll blocked by cooperatively yielding to the scheduler in a loop with IRQs enabled. The yield itself was the real bug. The context switch and its bookkeeping ran non-atomically, so a timer tick landing mid-switch re-entered the scheduler and overwrote the very CPU context being saved. Fork and exec yield once and almost never hit the window; poll yields in a tight loop for its whole timeout and hits it almost every time. The fix was to mask IRQs across the switch — one line of understanding, days of finding. Every syscall in the system had been exercising that yield path safely by sheer statistical luck. (This was in the single-core era; the later SMP series brought its own locking discipline, including a machine-checked audit of mutable kernel state, precisely because "we got lucky on one core" does not survive four.) Other people's software has opinions. Getting nginx, OpenSSL, and Node to build against a homemade ABI is mostly a war with autoconf. The best bug of the project: on a same-architecture ARM Linux CI runner, configure decided to execute a freshly linked freestanding test binary to see whether the compiler works. The binary targets an OS that isn't running — but it's valid aarch64, so Linux loads it, and it never returns. The first ports CI run sat inside that probe until it hit the six-hour ceiling. On my Mac the same probe fails instantly — wrong binary format — so configure carried on with quietly wrong answers instead. Same bug, two completely different faces, neither of them visible where I was developing. The rule since then: configure never runs a target binary, and I don't hand-answer its capability probes either, because a wrong cached yes fabricates a libc feature that doesn't exist and the failure surfaces three ports later. ARC deserves a mention too. Swift strings, arrays, and dictionaries in userland need a real allocator with free, so before the calculator REPL could exist I had to write a K&R-style free-list allocator over sbrk. None of this is Swift being hostile to bare metal. It's that every piece of "it just works" is now something you provide. The Rust question Rust would have been the safe choice: no_std is mature, Redox exists, the OS-dev community is large. If your goal is shipping a memory-safe kernel, take Rust and stop reading. My question was whether Swift can do this at all, because that answer matters to people who already think in Swift. The kernel mostly avoids ARC by design — value types at the bottom, classes rarely — so I sidestepped the ARC-overhead objection rather than answering it. The price I did pay was building runtime infrastructure that Rust's embedded ecosystem hands you for free. SwiftCube: an orchestrator with no containers SwiftOS's flagship profile is application and AI hosting, and the concrete form of that ambition is SwiftCube, a small Swift-everywhere cluster orchestrator in the same repo. The cast mirrors Kubernetes with shorter names: sctl is the CLI, sctld is the control plane — API server, scheduler, and reconcilers merged into one binary, with an embedded MVCC store called cubestore replicated over Raft — and slet is the node agent. Declarative manifests, watch streams, reconcile loops: the Kubernetes model, reused on purpose, because it's a good model. What makes it not-another-Kubernetes: there are no containers. A deployed instance is a SwiftOS Cell, the kernel's own isolation domain, which already bundles what an orchestrated workload needs — a content-addressed read-only base image, a private tmpfs scratch, explicit kernel capabilities instead of root-in-a-namespace, resource limits, lifecycle state. That deletes the container runtime, the image-layer machinery, and the union filesystem; slet remains, but it's a thin agent asking the kernel to create Cells, not a second isolation model living beside it. The manifest's capabilities: [ net.listen:8080, fs.read:/etc/app ] maps directly onto kernel grants. Why bother? The operator I have in mind is a coding agent: it deploys, tests, reads the result, and iterates many times an hour, with no human in the loop on each cycle. That operator needs environments it can compare by image hash rather than by inspection, cheap enough to create one per test and throw away, confined enough to run code it wrote thirty seconds ago, and machine-legible — a readiness probe with a definite verdict instead of a sleep and a prayer. Cells give those properties directly. Fast deploys should follow — starting a Cell is kernel work, with no image assembly in the path — but that's the part I'd defend the least, because it's unmeasured. The honest caveat. SwiftCube's control plane is real: cubestore, Raft elections with leader forwarding, the scheduler, node join over mTLS — all of it passes make swiftcube-test host-side. But the last mile is not wired: the adapter that would actually create a Cell on a node still reports .unavailable, so no instance has ever been deployed through the orchestrator. The repo's positioning note commits to a benchmark — sctl apply to first served request, against docker run and a Kubernetes rolling update on the same hardware — and to an uncomfortable rule: if the numbers aren't substantially better, the positioning gets revised. The elephant in the repo Browse the repository and you'll find CLAUDE.md, AGENTS.md, and the full project prompt checked in at the root. So let me describe the workflow plainly. A large share of this code was written by a coding agent (Claude Code). I set the architecture, the non-goals, and each milestone's acceptance criteria; the agent implemented one milestone at a time, including the tests that had to satisfy those criteria; every milestone had to build, boot in QEMU, and pass its executable check before the next one started. I reviewed what landed and drove the debugging when things went sideways — the war stories above are what that looked like in practice. The standing instruction at any consequential fork: ask, don't guess. 1,235 commits in two months is not human typing speed, and I won't pretend otherwise. I'll also not pretend the gates prove more than they do: they caught failures early and they keep every step reproducible, but "trustworthy kernel" would take independent review and far more adversarial testing than one person and one agent have done. What this workflow can actually carry deserves its own article, with numbers in it, rather than a defensive paragraph here. What this proves, and what it doesn't It shows Embedded Swift can express an operating system — MMU, SMP scheduler, drivers, network stack — and that the result is functional enough to serve a public website and start Node.js. It shows exhaustive switches move one class of bugs to compile time, and a sans-IO core moves another off a hung serial console and into a host test suite. It doesn't show SwiftOS is useful to you today. Everything outside /data dies at reboot by design. The homegrown TLS isn't on a public path yet. Drivers are still migrating toward restartable userland services. The orchestrator can't place a workload. It's a two-month-old operating system. Three things I'd tell you over a beer: The language was never the hard part. Toolchains, autoconf, and timing bugs were the hard part. Sans-IO everything you can afford to. Code that doesn't touch hardware gets tested where debugging is pleasant. Other people's software is the best test suite your ABI will ever face. vi found my scheduler race; nginx and OpenSSL found holes I'd never have thought to write tests for. Try it git clone https://github.com/asaptf/swift-os && cd swift-os make newlib && make busybox # one-time: cross-build libc + bring-up shell make build && make run # boot the kernel under QEMU virt make test # host tests + QEMU boot assertions Or just open swiftos.tech and view the response headers. Issues welcome in the repo.

Original Source

Read the full article at Hackernoon →

KhanList aggregates and links to publicly available news content. We do not host full articles from third-party sources. Always verify important information with original sources.