A native iOS client for paperless-ngx — built in SwiftUI, on top of The Composable Architecture.
This is a complete rewrite of Less Paper, and it is the app on the App Store today: https://apps.apple.com/en/app/less-paper/id6464425056. It needs paperless-ngx 2.15.3 or newer — that is the release whose REST API first reached version 8, which is the oldest the app negotiates down to, and an older server is turned away with a message saying so.
Every push to main also ships a TestFlight build, and the beta is open to anyone who wants the
next release early: https://testflight.apple.com/join/3CM21m1n
- Multiple servers — connect to as many paperless-ngx instances as you like and switch between them
- Search and filter — the full filter set from the web UI, including custom-field filters, and a search field on every management list
- Saved views — create, edit and reorder them from the app
- Offline favorites — keep chosen documents on the device, PDF and all, and read them with no server in reach
- Document editing — title, correspondent, document type, storage path, tags, notes and custom fields
- Bulk edit — apply changes to a whole selection at once, including bulk delete and merge
- Import — share sheet extension for getting documents in from anywhere on the device
- Password-protected PDFs — unlock on import, with passwords remembered in the keychain
- Trash — restore a document you deleted, or empty the trash for good
- Permission-aware — controls your account has no permission to use stay hidden, and a restricted account can still add a server
- iPad layout — the document opens in a second column beside the list, rather than pushing it aside
- Single sign-on — sign in with an OIDC provider your server offers, through a system browser sheet
- Advanced authentication — client certificates, custom HTTP headers, and reverse-proxy setups such as Authelia
- Deep links — open a document straight from a
lesspaper://link - Diagnostics — a local error log, redacted of anything that could be a credential, that you can read and share when something goes wrong
- Tip jar — an optional way to support the app, in Settings
- iOS 18.0 or newer
- A running paperless-ngx instance to talk to
If your server offers OpenID Connect sign-in, the app shows the same providers on its sign-in screen and runs the login through a system browser sheet. For that flow to complete, the OAuth client your paperless instance uses needs two things on the identity provider:
lesspaper://oidc-callbackregistered as a redirect URI — this is how the browser hands the login back to the app- a public (PKCE) code exchange allowed — like any native client, the app exchanges its authorization code with PKCE and no client secret; a strictly confidential client will reject that exchange
If the browser sheet ends on an error from the provider about the redirect URI, the callback URL above is the thing that's missing.
Building requires Xcode 26.5; everything else is pinned with mise, which installs Tuist, SwiftLint, SwiftFormat and everything else at the exact versions CI uses.
mise install # tools + `brew bundle` via the postinstall hook
tuist install # resolve Swift package dependencies
tuist generate # generate LessPaper.xcworkspaceNo Tuist account is needed. The project is connected to the Tuist server for build insights and a shared binary cache, and optionalAuthentication in Tuist.swift means an unauthenticated clone simply goes without them rather than failing. Run tuist auth login to get them locally.
mise run docker:start # bring up paperless-ngx
mise run docker:seed # fill it with realistic fixture data
mise run docker:stopmise run ci:test:unit # run the unit tests
mise run ci:test:ui # run the XCUITest journeys (slow)
mise run ci:lint # SwiftLint + SwiftFormat
mise run format # apply formatting
mise run snapshots:diff # visual diff of changed snapshots
mise run screenshots:frame # re-render the App Store screenshotsThe project is generated by Tuist from Project.swift, and split into modules declared in Tuist/ProjectDescriptionHelpers/Module.swift:
- Interface / implementation —
ApiInterfaceholds the models and use-case protocols;ApiImplementationholds the concrete networking. Features depend only on the interface, which is what keeps them testable. - Feature modules — one per domain (
DocumentsFeature,TagsFeature,CustomFieldsFeature, …), each a TCA reducer plus its SwiftUI views. - App modules —
ShareAppalone, which stands in for the share extension so XCUITest can drive it without going through another app's share sheet. The per-feature harness apps this list once described are gone: UI tests drive the real app now — see Testing. - Support modules —
Componentsfor shared UI andDesignTokensfor the colour palette it draws from,Loggingfor the shareable error log,TestSupport/ApiTestSupport/UITestSupport/SnapshotSupportfor the test helpers, andMarketingKit, which renders the App Store screenshots from committed captures.
Dependency injection runs through swift-dependencies, persistence through swift-sharing, and networking through Get.
Build and test runs are reported to the Tuist server, and the project's dashboard is public: https://tuist.dev/plu/less-paper. It is the quickest way to see how long CI is taking and which targets dominate a run, without digging through workflow logs.
Tests are written with Swift Testing and cover unit tests and snapshot tests, plus XCUITest journeys that drive the real app.
The API layer is tested against a live paperless-ngx container rather than mocks, so the tests catch real API drift.
UI tests live in AppUITests and drive the assembled app end to end — onboarding, server management, settings, the create/edit/delete lifecycle of each entity, custom fields, and document browsing and editing — rather than a harness app per feature. Each test creates its own paperless user, so what it makes is invisible to every other test and the lists it opens start empty. The journeys run serially and take several minutes; they are the slow part of the suite, so CI reserves them for main and for pull requests labelled UITests or TestFlight.
CI runs only the modules whose fingerprint changed, so most pull requests test a slice of the suite rather than all of it. A pull request labelled FullTests turns that off and runs everything the job covers — it does not pull in the UI tests, so pair it with UITests to run genuinely everything.
Every pull request also gets a comment reporting coverage for each module whose own tests ran, and for the files the pull request changed.
Conventions for this codebase — comment style, TCA patterns, the confirmation-popup rule and more — live in AGENTS.md. Please read it before opening a pull request. Releasing to the App Store is a maintainer task, documented in docs/releasing.md.
MIT © Johannes Plunien
