Skip to content

Repository files navigation

Backplane

Swift 6.2+ Platforms

Backplane is a small server-side Swift framework that ties together the SSWG ecosystem (swift-service-lifecycle, swift-argument-parser, swift-service-context, swift-log, swift-metrics, swift-distributed-tracing, swift-configuration) into a single ergonomic harness for building CLI-driven services. It abstracts away the boilerplate of the bootstrap dance — installing the global logging/metrics/tracing systems in the right order, after CLI parsing, before any service is built — without taking power away from the underlying libraries.

Backplane is structured for both long-running services (HTTP servers, queue workers) and one-shot tasks (migrations, backfills) sharing the same service graph and configuration story.


Features

  • Self-describing services. A type conforming to BackplaneService carries its own construction (static make(context:)), dependencies, subgroup, and replacement strategy — naming its key in a command's requiredServices is the entire registration. services() exists only for overrides: protocol-key bindings, third-party types via closures, test doubles. Backplane boots only the transitive closure of a command's required services, with per-subgroup failure policies, blue-green or cold replacement, hot reload, and recovery of failed entries.
  • Keypath-driven references. \.database style references everywhere — autocomplete-friendly and the resolved type is inferred from the keypath. A key's resolved type can also be a protocol, backed by a concrete registration (passive: / factory:as: on EntryDescriptor).
  • Per-entry configuration scopes. A factory's context.config is pre-scoped to its entry id — the entry "postgres" reads postgres.host as host, and the same service type registered under two keys reads two scopes (two databases, one type).
  • Two command shapes: PersistentCommand (services run until signalled) and TaskCommand (services come up, the command does its work, the group shuts down gracefully).
  • Bootstrap pipeline. BackplaneCommand.bootstrap(...) returns a BootstrapPlan that the framework applies through BootstrapCoordinator — globals install in tracing → metrics → logging order, after CLI flags are parsed, before the first Logger is built.
  • Vendor-neutral structured logging via StructuredLogHandler driven by an extensible StructuredLogProfile.
  • CLI option groups for common observability flags (LoggingOptions, TracingOptions, MetricsOptions).
  • Trait-gated optional integrations: every cloud-vendor or ecosystem dependency is opt-in via a Swift Package trait, so apps pay nothing for what they don't use.

Requirements

  • Swift 6.2+
  • macOS 15+

Installation

Add Backplane to Package.swift:

.package(url: "https://github.com/<org>/swift-backplane.git", from: "2.0.0"),

By default, only the core Backplane library is resolved. Optional integrations are enabled per-consumer via package traits — add only the ones you need:

.package(
    url: "https://github.com/<org>/swift-backplane.git",
    from: "2.0.0",
    traits: ["Postgres", "OTel", "GCP"]
),
Trait Library product Adds
(none) Backplane Core framework, always available.
(none) BackplaneVault ConfigStore (writable, secret-aware config; safe for concurrent stores on one file via locked read-merge-writes) + ConfigEncryption protocol + PassthroughEncryption + FileLock (cross-process advisory lock). Imported on its own, no extra deps.
Postgres BackplanePostgres PostgresNIO-backed service key, configuration, migrations.
OTel BackplaneOTel swift-otel integration: BootstrapPlan factory + CLI flags.
GCP BackplaneGCP Cloud Trace tracer/exporter + Cloud Logging LogHandler.
KeyfileEncryption (extends BackplaneVault) LocalKeyfileEncryption — AES-256-GCM reference impl backed by a machine-local keyfile. Pulls swift-crypto.
Macros (extends Backplane) The #service declaration macro — shorthand for declaring a key on Services. Pulls swift-syntax at build time; off by default.

Then depend on each library product in the targets that need it:

.executableTarget(
    name: "MyService",
    dependencies: [
        .product(name: "Backplane", package: "swift-backplane"),
        .product(name: "BackplanePostgres", package: "swift-backplane"),
        .product(name: "BackplaneOTel", package: "swift-backplane"),
    ]
),

At a glance

No services() registry — \.postgres is required by the commands, and BackplanePostgresService conforms to BackplaneService, so its entry (and its config scope) materialises from the key alone:

import Backplane
import BackplanePostgres

@main
struct MyApp: BackplaneApplication {
    typealias RootCommand = AppCommand
    static let identifier = "my-app"
}

struct AppCommand: AsyncParsableCommand {
    static var configuration = CommandConfiguration(
        subcommands: [Serve.self, Migrate.self],
        defaultSubcommand: Serve.self
    )
}

struct Serve: PersistentCommand {
    typealias App = MyApp
    static let configuration = CommandConfiguration(abstract: "Run the server")

    @OptionGroup var logging: LoggingOptions
    @OptionGroup var tracing: TracingOptions

    var requiredServices: ServiceList { [\.postgres] }

    func bootstrap(config: ConfigReader, environment: Environment) async throws -> BootstrapPlan {
        var plan = BootstrapPlan()
        plan.logLevel = logging.resolvedLogLevel
        plan.logHandlerFactory = logging.format.factory(default: BackplaneLogging.cloudRunOrStream.asFactory)
        return plan
    }
}

struct Migrate: TaskCommand {
    typealias App = MyApp
    static let configuration = CommandConfiguration(abstract: "Run database migrations")
    var requiredServices: ServiceList { [\.postgres] }

    func execute(with context: BackplaneContext) async throws {
        let pg = try await context.requireService(\.postgres)
        try await PostgresMigrator.migrate(
            myMigrations,
            on: pg.client,
            logger: context.logger
        )
    }
}

Writing a service

A service that conforms to BackplaneService is complete at the point it's written — construction, dependencies, subgroup, and replacement strategy all live on the type:

final class Notifier: BackplaneService {
    let webhookURL: String
    init(webhookURL: String) { self.webhookURL = webhookURL }

    static func make(context: BackplaneContext) async throws -> Notifier {
        // context.config is pre-scoped to the entry id ("notifier").
        Notifier(webhookURL: try context.requireConfig().requiredString(forKey: "webhookURL"))
    }
    static var dependencies: ServiceList { [\.postgres] }
    static var subgroup: SubgroupTag { .integrations }

    func start() async throws { /* … */ }
    func shutdown() async { /* … */ }
}

extension Services {
    public var notifier: ServiceKey<Notifier> { "notifier" }
}

Declare [\.notifier] in a command's requiredServices and Backplane materialises the entry, its postgres dependency, and both config scopes — nothing else to write.

For everything a conformance can't express, services() on the app is the override surface: bind a protocol key to a concrete implementation (passive: / factory:as:), build a third-party type with a closure, swap in a test double, or override a conforming type's defaults (EntryDescriptor(\.postgres, subgroup: .integrations) — explicit descriptors always win).

Declaring service keys

A service key is a computed var on the Services namespace, returning a typed ServiceKey<T>. Consumers extend Services once per key:

extension Services {
    public var database: ServiceKey<PostgresClient> {
        ServiceKey(id: "database")
    }
}

That's the entire boilerplate — name the property for the value, not …Key. ServiceKey is ExpressibleByStringLiteral, so the body can also be just "database". With the Macros trait enabled, #service(PostgresClient.self, name: "database") expands to the same declaration. The keypath \.database is then usable wherever Backplane accepts a service key — requireService, dependency lists on EntryDescriptor, requiredServices on a command. The return type of requireService(\.database) is inferred from the keypath, so no cast is needed at the call site. The key's id doubles as the entry's configuration scope.

Satellite targets ship their keys the same way: BackplanePostgres exposes Services.postgres, which is why the snippet above writes \.postgres without declaring it itself.

Documentation

Full DocC documentation lives alongside each module:

  • Backplane — getting started, key concepts, local + cloud deployment guides.
  • BackplanePostgres — Postgres walkthrough.
  • BackplaneOTel — OTel walkthrough.
  • BackplaneGCP — Cloud Trace + Cloud Logging walkthrough.
  • BackplaneVault — writable, encryption-aware configuration: core model, reading & writing, encryption backends, file locking.

Build the docs locally with the Swift DocC Plugin:

swift package --traits Postgres,OTel,GCP \
    generate-documentation --target Backplane

Testing

swift test                                                        # core only
swift test --traits Postgres,OTel,GCP,KeyfileEncryption,Macros    # everything

The test suite covers dependency resolution, lifecycle modes, bootstrap ordering, structured logging, CLI option parsing, and the optional integrations.

License

See LICENSE.

About

Service-management scaffold for Swift server applications

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages