An iOS app with no server — records stay on the device, and a phone move is an encrypted file
On-device SQLite migrations, AES-256-GCM backups through CryptoKit, and a one-time purchase gated twice. How the goshuin-ledger app “Goshuin Ledger” is built.
Live on the App Store (released September 2026). Free to use, with a one-time purchase for extra features.
Why this is in a consulting portfolio
For a consumer app, the most expensive thing is the server. Its monthly bill keeps growing, and someone has to keep operating it — including knowing how to switch it off. Goshuin Ledger shipped to production without a single server of its own, and in exchange keeps records, phone moves and purchases entirely on the device.
Why I built it
A goshuin is the calligraphy stamp a Japanese shrine or temple gives visitors. Once you own several goshuin books, logging them falls behind: the photos exist, but there is no way to sort out afterwards which shrine you visited and when.
Reading the complaints about existing goshuin apps, the top reasons for one-star reviews were “you have to register” and “ads”. So three things were decided first: no accounts, no ads, and no user data held on my server. The surest way to keep the third is to have no server.
The hardest part
Without a server, everything a server used to do has to happen on the device.
- Moving phones. With no sync, I had to provide a way to carry records to the new phone — and that file contains visit history and photos.
- Schema changes. On a server you migrate once. On phones, every user's database is stopped at a different version, and each must be able to climb to the latest.
- Unlocking purchases. With no server to confirm entitlement, any hole in a screen becomes a free unlock.
How I decided (and what I gave up)
“The app never talks to a server of mine” is a test, not a spec. An architecture test statically checks that the app code and modules contain no fetch / XMLHttpRequest / WebSocket path. It is not an intention; writing one fails CI.
I decided not to implement encryption myself. JavaScript crypto can't be audited here and offers no side-channel guarantees, so I wrote a native Swift module that calls the OS's CryptoKit and CommonCrypto.
What I gave up: sync, sharing, multiple devices and a web version. The cost is explicit, too. Lose the backup passphrase and nobody can restore it — including me.
What actually runs
Backups use a custom .goshuin format. The key comes from PBKDF2-HMAC-SHA256 at 600,000 iterations (the OWASP 2023 recommendation), and the content is encrypted chunk by chunk with AES-256-GCM. The whole 45-byte header is additional authenticated data on every chunk, so tampering with the iteration count or chunk length fails authentication on decrypt. The plaintext length is checked too, so a truncated tail is detected.
Monetisation is a single one-time purchase and three feature gates: a second goshuin book, bulk filing of photos, and PDF/CSV export. The unlock is checked in two places — the UI and the service layer, and the service layer also rejects calls that don't qualify, so a missed check in a screen does not become a free unlock. All of this runs on the device, so it is not designed to stop a tampered phone.
The code follows ports and adapters with a single composition root. Swap the SQLite and crypto adapters and the domain layer runs in Jest without a simulator.
Crash reports and usage analytics are on by default (they can be turned off in settings). The content of your records is never sent.
Numbers counted from the code
158 commits (excluding merges) between 2026-09-08 and 09-25. 40 architecture decision records, each noting the options not taken and the conditions for reversing the decision.
429 test files, plus 11 on-device E2E flows in Maestro (numbered flows only; shared sub-flows excluded). The on-device SQLite carries 6 migration versions and can climb from any of them to the latest.
The display name, landing page and App Store listing ship in 14 languages. The only fixed costs are the Apple Developer Program fee and the landing page's domain, and stopping either is simply “don't renew”.
What this proves for your project
“We don't hold personal data on our servers” is easy to write in a spec. This is a worked example of making it a promise a test won't let you break. For apps where not holding the data is itself the value — health, children, journals, personal finance — this structure carries over as is.
Having no server removes operating cost, and it also removes legal and review questions you would otherwise have to answer. In exchange, sync, backup and purchase checks have to be built on the device, which is where the engineering effort goes.
The on-device SQLite migrations, encrypted backups and StoreKit 2 one-time purchase are written up with code in designing a serverless, offline-first iOS app with Expo.
I take on offline-first mobile apps as mobile app development.
Numbers counted from the code
- Architecture decision records
- 40 ADRsEach with an adoption date, revision history and reversal conditions — including the decision to have no server
- Tests
- 429 filesThe domain layer runs in Jest with no simulator, plus 11 on-device E2E flows in Maestro
- On-device schema migrations
- 6 versionsSQLite schema stacked up on the phone — there is no server-side way to migrate everyone at once
- Listing languages
- 14 languagesApp display name, landing page and App Store listing
FAQ
- Where can I download Goshuin Ledger?
- The iOS app is on the App Store (released September 2026). Core features are free; a second goshuin book, bulk filing and PDF/CSV export are a one-time purchase. There is no Android version.
- Where are records and photos stored?
- Only on the device. The app never talks to a server of mine, and an architecture test statically checks that its code has no network path. Crash reports and usage analytics are on by default, but the content of your records is never sent.
- What happens to my records when I change phones?
- Besides the standard iOS backup, records move with an encrypted `.goshuin` backup file. The key is derived with PBKDF2-HMAC-SHA256 at 600,000 iterations and the content is encrypted with AES-256-GCM. If the passphrase is lost, nobody can restore it.
- Do I need to register or log in?
- No. The app has no account system at all. Registration was the most common complaint about existing apps, so it was the first thing removed.
- With no server, how are purchases protected?
- The App Store purchase is checked on the device, and the unlock is checked in both the UI and the service layer, which rejects calls that don't qualify — so a missed check in a screen does not become a free unlock. Because it all runs on the device, it is not designed to stop a tampered phone.
Try Goshuin Ledger
Live on the App Store (released September 2026). Free to use, with a one-time purchase for extra features.
同様の課題、抱えていませんか?
あなたのビジネス課題も、最新の技術で解決できます。 まずは30分の無料技術相談から、状況をお聞かせください。
自社の課題もSaaS化できるか相談するプロジェクト単位(請負)・技術顧問、どちらにも対応可能です