swift-paperless: A Native iOS Client for a Self-Hosted Paperless-ngx
Native iOS app for interacting with a Paperless-ngx installation to manage documents
At a glance
- What is it?
- Swift Paperless is a SwiftUI iOS app for managing documents in a self-hosted Paperless-ngx installation, and the repository is as much a release engineering project as an application: the Xcode project is generated, screenshots are captured and framed by a tool called swpngx, and betas are cut entirely on CI with the version tag created only after the upload succeeds.
- Who is it for?
- swift-paperless fits a person who runs Paperless-ngx and wants a native iOS client for it, especially someone who wants to build from source and follow a documented release process rather than reverse-engineer an app. It is a poor fit if you expect a hosted service, because the app requires a self-hosted instance to function.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly Swift, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
An iOS Client for a Server You Run Yourself
Swift Paperless is a native iOS app for the Paperless-ngx software, which is a self-hosted document management system that helps you organise your documents digitally.
The constraint is stated plainly in the README: this application requires a self-hosted instance to function. There is no demo account and no hosted tier, so the app is an interface onto infrastructure you already run, and the first thing a new user needs is a Paperless-ngx server and its API details before the app has anything to talk to.
The app itself is written in Swift with SwiftUI, and it is published on the App Store, with the development versions on TestFlight. The documentation is published separately through mkdocs, and the interface is localised through Crowdin, with a status badge showing the state of the translation work.
The repository layout mirrors a standard iOS project split by concern rather than by feature, with AppShared/, AppViews/, Common/, DataModel/, Networking/ and Persistence/ as the application layers, a ShareExtension/ directory for the share sheet integration, a swift-paperless/ directory for the app target and a swift-paperlessUITests/ directory for the UI tests, plus Config/ for the build settings, scripts/ for the automation, fastlane/ for the store metadata and screenshots, and a demo/ directory holding a docker-compose setup for trying the app against a local server.
The Xcode Project Is Generated, Not Committed
The build section is short and it changes how you work on the codebase.
The Xcode project is generated with XcodeGen from project.yml, and the build settings live in Config/ as .xcconfig files. The swift-paperless.xcodeproj is not committed, and you have to generate it before opening or building:
brew install xcodegen # once
just generate # or: xcodegen generate
open swift-paperless.xcodeprojThe just build and just test-xcode tasks regenerate the project automatically, so a normal build never touches a stale project file. The instruction is to edit project.yml for the targets, sources and dependencies, and Config/*.xcconfig for the build settings, then re-run just generate.
This is a deliberate choice rather than an accident of a modern project. Generating the project means the project file is never the thing two people conflict over, and it means a change to the target list is a diff in a YAML file rather than a merge of a pbxproj. The cost is one extra tool, XcodeGen, and the discipline of not opening the generated project in a way that suggests it should be edited.
The build is driven by a Justfile rather than by npm scripts or a Makefile, and there is a Gemfile for the fastlane side, so the toolchain is a mix of just, uv, Ruby and Xcode.
swpngx Drives Screenshots, Frames and Device Metadata
Most of the automation lives in the scripts/ package, called swpngx, and in the fastlane/ directory. You run it with uv against the scripts project:
uv run --project scripts swpngx devices checkThe device definitions are centralised. Device ids, simulator names, bezel PNGs and framing geometry are defined once in screenshot_devices.toml, screenshots.toml references those ids for the capture step, and frames.toml references the same file for the framing step. That single source of truth is what makes the alignment check possible, since the check command compares what the capture will produce against the framing configuration.
The capture step writes files with the device id as the prefix, for example fastlane/screenshots/en-US/iPhone_17_Pro_Max-01_documents.png, and the backend is torn down with a separate teardown command. The backend itself can be pinned to a specific tag, and there are options to add random tags, random correspondents and random document types, which is a neat way to stress the layout with unlikely content before an App Store review does it for you.
The framing step installs device bezels from Apple's Design Resources. Each device entry names a bezel pack from bezel_packs.toml and the PNG filename to install. The tool can use DMGs you have already downloaded, or download them from Apple into its own cache, and it can install a single pack such as the iPhone 17 from one DMG path.
Betas Are Cut on CI and the Tag Comes After the Upload
The release process is the part of this repository that is unusual enough to be worth reading twice.
A beta is cut entirely on CI, with no commit, no tag and no local build:
just beta # from origin/main
just beta --ref develop/v1.12 # from another branch
just beta --dry-run # CI builds and exports, uploads nothingThe dry run is the safe way to see what would happen, and the command previews the notes the build will publish before anything is uploaded. The workflow then asks App Store Connect for the build number, writes it into Config/Shared/Version.xcconfig for that build only, archives, signs, uploads, and, after the upload succeeds, creates the builds/<version>/<build> tag and its GitHub prerelease at the built commit.
That ordering is the important detail. The tag is the record of what shipped, not the trigger, so it can never name a build that failed to upload. A build number that never reached the store therefore never gets a tag, and the tags remain a truthful index of what is actually live.
The tags in the repository follow that scheme, with the recent ones named builds/1.10.4/226, builds/1.11.0/225 and builds/1.11.0/224, which is why the release list looks unusual next to a normal semver project.
The Changelog Rules That Handle the 4000 Character Cap
Release notes are generated from a single accumulating file, and the rules are more intricate than they first look.
The notes come from current_changelog.txt, which accumulates the user-facing bullets for the current marketing version, and nothing clears it at build time. A build's notes are the bullets added since the previous build tag, and they become the prerelease body, which is what the in-app What's New screen shows.
The TestFlight What to Test field is handled separately, as one 1.11.0 (209) style section per build of the current version, newest first, in the same shape as changelog.txt but scoped to the version in flight. The problem it solves is a size limit: App Store Connect caps that field at 4000 characters, and a version's notes routinely run past it, so whole builds are dropped off the oldest end until it fits, and a link to the releases takes their place. A build is either there in full or not at all, and only a single build whose own notes exceed the budget falls back to trimming by line.
When the version ships, the file is emptied in the same commit that bumps MARKETING_VERSION with just set-version, which removes the manual step of clearing the changelog by hand to fit. To fix notes after the fact, you edit the release body on GitHub for the copy users read, and for TestFlight you run just set-test-notes with the build. changelog.txt is an offline copy of the per-build notes, regenerated from the releases with just changelog-archive.
Framing the Screenshots and the Apple Licence You Must Accept
The framing step turns raw simulator screenshots into App Store images, and it has a hard platform requirement.
The bezels come from Apple's Design Resources, and the download requires macOS because the tool uses hdiutil to mount the DMG. Apple's DMG licence prompt is accepted automatically with Y, and the README is explicit that you must comply with the Apple Design Resources licence yourself. That is worth reading before you run the command, because the tool will click through the prompt for you.
The framing configuration is frames.toml, which references the same device file as the capture step, so the geometry that decides where the device frame sits is defined once and used by both. The whole sequence is capture, then install bezels, then frame, and each stage has its own command.
One more constraint shapes the output. iPhone screenshots use Pro Max only, because App Store Connect scales the images for smaller phones. That removes a whole matrix of capture work, at the cost of losing the per-device framing you would have wanted.
There is also a docker-compose file for the screenshot environment, docker-compose.screenshot.yml, and a demo directory with a docker-compose setup and environment file, so the whole pipeline can be reproduced in a container rather than only on a developer's machine.
Where the Code Lives and Who Works on It
The project is released on the App Store and on TestFlight, and the README points out that the development versions are on TestFlight for anyone who wants the latest build before it reaches the store.
The source tree is split into the layers a SwiftUI application usually needs, with AppViews/ for the interface, Networking/ for the API client, DataModel/ for the model types, Persistence/ for local storage, Common/ for shared utilities and AppShared/ for code used by both the app and the ShareExtension, which is what lets a document be sent to Paperless from another application.
The build configuration is in Config/ with a Shared subdirectory holding the version file that the CI writes the build number into, and the project definition is project.yml. There is a bump.py and a user-defaults.py at the root, a Justfile for the tasks, a .swiftformat configuration and an .xcode-version file, and a .pre-commit-config.yaml for the hooks, so the formatting rules are enforced the same way as in a server side project.
Issues go to GitHub, and the author offers a direct address for support questions. The licence is MIT, the repository is not archived, and the last push was on 2026-10-15, with the most recent release tag, builds/1.10.4/226, from 2026-10-14.
Editorial conclusion
swift-paperless fits a person who runs Paperless-ngx and wants a native iOS client for it, especially someone who wants to build from source and follow a documented release process rather than reverse-engineer an app. It is a poor fit if you expect a hosted service, because the app requires a self-hosted instance to function. Before contributing, note that the Xcode project is generated from project.yml and the .xcconfig files, so editing the .xcodeproj is the wrong move, and that the screenshot pipeline depends on Apple's design resources, whose licence you must comply with. The app is MIT licensed, the release numbers are per-build tags in the form builds/<version>/<build>, and the last push was on 2026-10-15.
Frequently asked questions
What does swift-paperless need to work?
It needs a self-hosted Paperless-ngx instance, since the README states that the application requires one to function. The app itself is a native SwiftUI client published on the App Store, with development builds on TestFlight and localisation through Crowdin.
How do I build the app from source?
The Xcode project is generated with XcodeGen from project.yml, and swift-paperless.xcodeproj is not committed. Install XcodeGen with `brew install xcodegen`, then run `just generate` or `xcodegen generate` and open swift-paperless.xcodeproj. Edit project.yml and Config/*.xcconfig rather than the generated project.
How are TestFlight betas released?
Entirely on CI, with no commit, no tag and no local build. The just beta command previews the notes and dispatches the beta workflow, which asks App Store Connect for the build number, writes it into the version configuration for that build only, archives, signs, uploads, and then creates the builds/<version>/<build> tag and the GitHub prerelease at the built commit, so the tag can never name a build that failed to upload.
What is swpngx used for?
It is the automation package that drives device checks, App Store screenshot capture and framing, using the device ids, simulator names and framing geometry defined once in screenshot_devices.toml, with screenshots.toml and frames.toml referencing it. It can also add random tags, correspondents and document types to stress the layout.
Are there any licensing requirements for the screenshot pipeline?
The device bezels are installed from Apple's Design Resources, the download requires macOS because the tool uses hdiutil, and Apple's DMG licence prompt is accepted automatically. The README states that you must comply with the Apple Design Resources licence yourself.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/paulgessinger-swift-paperless)Was this useful?