Porting a Swift embedded database to Linux: what actually broke

Porting BlazeDB to Linux: the bugs weren't where I expected

A few months ago I open-sourced BlazeDB, an embedded Swift database with WAL durability and page-level AES-GCM encryption.

In the launch post, I mentioned that macOS support came together fairly quickly while Linux took much longer. I never really explained why.

This post is that follow-up.

The goal

The target was simple:

  • One storage engine
  • One Swift package
  • Runs on macOS, iOS, and Linux

That meant supporting both GUI applications and server-side/CLI workloads (blazedb doctor, blazedb dump) without Xcode.

The surprising part wasn't Linux itself.

It was discovering how many assumptions I'd accidentally made while developing primarily on Apple platforms.


1. Crypto isn't just import CryptoKit

Apple platforms use CryptoKit.

Linux uses swift-crypto (import Crypto).

The import is straightforward:

#if canImport(CryptoKit)
import CryptoKit
#else
import Crypto
#endif

But encryption isn't a place where portability can be an afterthought.

PBKDF2, page encryption, and WAL frame encryption all pass through the same abstraction layer. Once encryption is part of your storage format, your crypto backend becomes part of your portability surface.


2. Linux exposed a correctness bug, not a Linux bug

This ended up being the most interesting issue.

BlazeDB stores records in a custom binary format inside encrypted pages.

During development I'd assumed loading values directly from raw bytes was safe.

Linux CI immediately proved otherwise.

I added a regression test that intentionally decoded from a misaligned buffer:

Encode record
      ↓
Prepend one byte
      ↓
Decode from subdata(...)
      ↓
Crash

The problem wasn't Linux.

UnsafeRawPointer.load(as:) has the same alignment requirements everywhere.

The difference was the environment.

Unaligned scalar loads often appear to "work" on x86_64 macOS.

Linux CI (and strict ARM) traps immediately.

The fix wasn't platform-specific.

It was removing alignment assumptions entirely by switching to alignment-safe byte reads for decoding and page header parsing.

Linux simply found the bug first.


3. BLAZEDB_LINUX_CORE became its own operating mode

Linux support wasn't just removing SwiftUI imports.

Instead I introduced a dedicated compile-time configuration:

BLAZEDB_LINUX_CORE

The portable core currently includes:

  • WAL
  • Encryption
  • open
  • put
  • get
  • query
  • observe

Some platform-specific features remain outside that configuration, including parts of the query planner, spatial/vector APIs, and trigger persistence.

That means maintaining two build modes.

It's more work, but I'd rather explicitly define the portable surface than pretend every feature works everywhere.


4. Most portability work is boring

None of these issues were individually difficult.

Together they accounted for most of the port.

Examples:

  • arc4random_buf β†’ UInt8.random(in:)
  • Different default data directories
  • SwiftPM compiling every test target even when filtering one suite

Portability usually isn't one giant blocker.

It's dozens of tiny ones.


5. Cheap Linux CI caught the expensive bug

The alignment issue wasn't discovered by chaos testing.

It was caught because Linux Tier 0 runs on every pull request.

Current test layout:

Stage Runs
Pull Requests macOS Tier0 + Tier1, Linux Tier0
Nightly Linux Tier1 + Tier2, macOS Tier2 + TSan
Weekly Tier3 destructive / chaos testing

Linux isn't a promotion pipeline.

It's another source of signal.

Sometimes the cheapest tests find the most valuable bugs.


Current status

Verified today

  • BlazeDBCore builds on Linux
  • Tier0 passes on Ubuntu 22.04 (Swift 6.2+)
  • CLI targets build
  • Portable storage path exercised in CI

Still true

  • Linux has seen much less production use than macOS.
  • Most confidence comes from CI rather than deployment volume.
  • Feature parity isn't the goal yet. A portable, tested storage engine is.

Questions

For those maintaining Swift packages across Apple platforms and Linux:

  • Did you split into portable/core targets, or stay with one target and lots of #ifs?
  • How do you validate binary layouts without exploding your CI matrix?
  • Any good patterns for keeping CryptoKit and swift-crypto abstractions aligned over time?

I'd be interested to hear how others have approached long-term cross-platform maintenance for storage engines and binary formats.

1 Like