J.BLOG

What to look at before adopting a Swift package registry

Jaemyeong Jin···7 min read
한국어

Swift dependencies are usually declared with a Git URL. The same dependency can also be declared with a registry identifier.

This is a declaration with a Git URL.

.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0")

This is a declaration with a registry ID.

.package(id: "apple.swift-log", from: "1.5.0")

The difference is one line, but the path the package travels changes, and CI, reproducibility, internal distribution, security, and Xcode configuration are affected along with it. This post lists what an iOS team should look at when deciding whether to choose this approach. The post does not record applying it to a specific project, and it has no measured resolve times.

The cost of fetching through Git

With a Git URL, the first build clones the repository and picks a version from the tags. Even when only one release is needed, the Git history comes along. With a large repository or a complex dependency graph, resolving can get slow.

Reproducibility is at risk too. What a tag points to can change. If the repository is deleted or moved, having Package.resolved does not guarantee a build.

Some environments have a restricted network. When github.com is blocked, clones and CI can fail. In such places the security team usually requires going through an approved Artifactory, a Git mirror, or an artifact store. With a registry, a setup that supplies packages without reaching GitHub becomes possible.

The order in which a registry serves a package

A registry identifier has the form scope.package-name. For mona.LinkedList, the scope is mona and the name is LinkedList.

.package(id: "mona.LinkedList", .upToNextMajor(from: "1.0.0"))

According to the service specification, the main requests come in three steps. The list of releases comes from /{scope}/{name}, then the Package.swift of that version, then the archive from /{scope}/{name}/{version}.zip. Checking the checksum and the signature also fetches the metadata of the release (usage documentation). Because the archive is served over HTTP, releases can be operated as immutable, and a CDN or an internal store can be used. Source control and distribution are separated. The background is in SE-0292.

The development repository does not have to move. Development can continue on GitHub, GitLab, GitHub Enterprise Server, or an internal Git server, and the registry only delivers releases.

Where the configuration lives

The default registry is set with the CLI.

swift package-registry set https://packages.example.com

The setting is stored at project scope and at user scope. This is the file location at project scope.

.swiftpm/configuration/registries.json

This is the file location at user scope.

~/.swiftpm/configuration/registries.json

The file holds the default URL and the format version.

{
  "registries": {
    "[default]": {
      "url": "https://packages.example.com"
    }
  },
  "version": 1
}

In the help text of Swift 6.4, set takes one URL and has the --global and --scope options. I confirmed this only with --help on October 4, 2026. I did not connect to a registry server or change any configuration.

For a package project, declaring by ID in Package.swift is enough. An iOS app is different. An Xcode app project usually has no Package.swift, and dependencies are managed through project.pbxproj, the workspace, and Package.resolved. Do not assume that a setting made with the CLI applies to Xcode as it is. Check where the setting is actually stored.

Within a team, the registry URL setting is shared, and access tokens are not committed. Another option is a CLI or GUI tool that finds the configuration location in the workspace or .xcodeproj and writes mirrors.json. That is my suggestion, and this post has no example of having built one.

When Git and a registry are mixed

The case that needs the most care is one package arriving by two paths. The direct dependency is declared by registry ID, and another package pulls in the same thing by Git URL.

This is the direct dependency.

.package(id: "apple.swift-log", from: "1.5.0")

This is the declaration that comes in as a transitive dependency.

.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0")

With different origins, the two can get separate identities. Then modules or products can be duplicated, and symbols can clash.

What connects the two is /identifiers?url=. It looks up the registry ID that corresponds to a Git URL. The mapping between URL and ID has to be exact. It is better to keep one origin per package, and I suggest carrying out the move to ID declarations and the upkeep of Git or mirrors together, step by step.

A mirror is something else

A mirror has a different purpose from a registry. The mirror in SE-0219 keeps the .package(url:) declaration and only changes the address fetched from to another Git server.

This is an example that maps the original address to an internal Git address. git.example.com stands for the address of the internal Git server.

{
  "object": [
    {
      "original": "https://github.com/apple/swift-log.git",
      "mirror": "https://git.example.com/mirrors/apple/swift-log.git"
    }
  ],
  "version": 1
}

A registry is the path of IDs and archives, and a mirror is the path that swaps one Git for another. In a restricted network, I suggest applying an internal mirror first and moving to a registry after that.

Authentication and signing

A private registry needs authentication, and the login command can store the credentials (SE-0378).

swift package-registry login https://packages.example.com --token "$TOKEN"

Authentication is about access rights. Whether a release is genuine is handled by signing. A signature is evidence that the publisher holds the key and that the release has not been altered. The two signature headers in the archive response (X-Swift-Package-Signature-Format and X-Swift-Package-Signature) are checked, and the signature and the certificate chain are validated. A verified signature does not mean that the publisher can be trusted. Which CAs to trust, and which certificate may publish which scope, are decided separately by the organization.

Signing needs an X.509 certificate for code signing and a private key. On macOS an identity in the Keychain can be used, and in CI passing a key file and certificate chain files is also possible (SE-0391). The publish endpoint that the command below uploads the archive to is defined in SE-0321.

swift package-registry publish mycompany.design-system 1.2.0 \
  --url https://packages.example.com \
  --private-key-path ./private-key.pkcs8.der \
  --cert-chain-paths ./leaf-cert.der ./intermediate.der ./root.der

I confirmed only that --token of login and --url, --private-key-path, and --cert-chain-paths of publish are in the help text of Swift 6.4. The commands were not run against a real server. The environment variable SWIFTPM_REGISTRY_TOKEN, which the earlier version mentioned for CI, did not appear in the help text. The usage documentation on the main branch lists this variable as a way to authenticate (checked on October 4, 2026). I could not confirm whether Swift 6.4 supports it, so check with the toolchain in use before relying on it.

At the organization level, the scopes and packages each certificate may publish are managed, and when an internal CA is operated, the SwiftPM trust root configuration has to be distributed to the team.

Deciding whether to adopt it

With few dependencies and free access to GitHub, the gain may be small. The following are reasons to consider it. Resolving is slow. There are many internal packages. Outside access is restricted. A store such as JFrog, Nexus, or Artifactory is already in operation. A private SDK is delivered to customers. There are requirements for checksums, signing, or audit logs.

I suggest checking in the following order.

  1. List the dependencies.
  2. Look at the Git URLs in Package.resolved and in Xcode.
  3. Measure how long resolving takes in CI.
  4. Check whether GitHub, GitHubusercontent, and external binary URLs are allowed.
  5. Separate what to receive through a registry from what to receive through a mirror.
  6. Check that no dependency graph brings the same package in by both a Git URL and a registry ID.
  7. Confirm where the configuration lives in the workspace and .xcodeproj.
  8. Decide how tokens and CI variables are handled.
  9. Decide whether signing is needed and which CA to trust.
  10. See how it connects to the internal store.

Package approval, the choice between a registry and a mirror, sharing the Xcode configuration, and the external hosts allowed in CI are matters for the team or a platform group to set as policy.

The request order, the configuration files, authentication, and signing come from the Swift Evolution proposals and the SwiftPM documentation. The adoption order and the checklist are my own design choices built on that, not results confirmed by applying them. Where the configuration lives in each Xcode version, and how compatible it is, was not checked. Results confirming how a mixed graph actually behaves, or the speed before and after moving to a registry, are not in this post either. The documentation links point to the main branch, so their content can change.

광고Coupang Partners

이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.