The console now ships the way the app and the Manager do: every push touching admin/ builds an unsigned IPA and publishes it, together with a generated AltStore source, to a rolling `admin-dev` prerelease. - admin/app-repo.json is the source template the workflow rewrites with jq, same as manager/app-repo.json. - The tag is `admin-dev`, distinct from `dev` and `manager-dev` so the three builds don't overwrite each other's assets. - admin-build.yml is gone; the dev workflow covers the compile check it existed for, and running both would build every admin/ push twice. The repo is private, so "published" here means reachable by whoever already has access — which is the whole distribution model for an operator console. There is still no web/ proxy and no in-app updater: this app only ever calls the session-gated /api/* routes.
FTStore Admin
The FTStore Admin dashboard as an iOS app — for the person who runs the server, and nobody else.
FTStore Admin signs in against your FTStore Admin server with a normal dashboard account and then drives the same API the web UI does: the worker, the library, the queue, the scraper, Telegram, users, settings, backups.
It used to be a second tab inside FTStore Manager, hidden behind a developer-mode switch. It is its own app now, because "hidden" was the wrong tool: the Manager is what a normal user installs to get FTStore onto their phone, and an operator console has no business being one gesture away from that.
What it is not
- Not something to hand out. It ships from the rolling
admin-devprerelease of a private repo, so its AltStore source is only reachable by someone who already has access. There is noweb/proxy and no in-app updater — it never touches the/api/ftstore-*endpoints the app and the Manager use. - Not an installer. It signs nothing and installs nothing — no Zsign, no Vapor, no certificates, no OTA. Installing FTStore is the Manager's job.
- Not useful without an account. See below.
Screens
| Screen | What it does |
|---|---|
| Root | Worker start/stop, headline KPIs, navigation, sign out / lock |
| Dashboard | KPIs, 30-day trends, builds per source, in-flight transfers, recent activity |
| Library | Every build: search, filter, detail (tweaks, entitlements, privacy manifest), rename in the feed, flag corrupted, refresh App Store metadata, delete |
| Upload IPA | Pick an .ipa, attribute it to a source, push it in 8 MiB chunks and run the ingest pipeline |
| Queue | Per-status counts, storage-cap banner, paging, retry / skip / delete, bulk retry-failed and clear-completed |
| Logs | The activity log — filter by type and status, search, paginate |
| Channels & Sources | Telegram channels (add, enable, remove) and GitHub release sources (enable) |
| Telegram | The worker's TDLib login: connect, verification code, 2FA password, disconnect, reset |
| Known Tweaks | Edit known_tweaks: aliases, lock to a Telegram channel or GitHub repo, recommended flag, file-name filters, custom icon (Files, Photos or camera) |
| Users | Admin only — approve, revoke, delete accounts |
| Scraper Actions | Find IPA, scan, scan previous, process queue, publish feed, cleanup, cache, rebuild storage, nuke |
| Server Settings | Source metadata, intervals, limits, feed/storage options; credentials shown read-only and masked |
| Backup & Restore | Export the whole configuration as JSON (with or without secrets) and import one back |
Two things stay in the browser, both because they are bulk work on a table rather than a phone task: multi-select delete/rename across the Library, and reordering channels.
Why decrypting the IPA gets you nothing
The security model is that the app is worthless without an account:
- No secret ships in the app. The only baked-in value is the FTStore Admin base URL, which is the same public domain FTStore already uses as its default source. There is no API key, no token, no privileged URL to extract.
- Every request is authenticated server-side.
web/src/middleware.tsrejects any/api/*call without a session cookie, and each handler re-checks that session against the database throughwithAuth()/withAdmin(). A forged cookie gets a 401 from the handler even though it passes the middleware's presence check. - The password is never stored. It is used for one
POST /api/auth/loginand discarded. What persists is the server-issued session token. - The token lives in the Keychain, as
kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly— so it is not in an iTunes/iCloud backup and does not travel to another device. The app'sURLSessionis ephemeral with cookie storage disabled, so nothing authenticating it is ever written to the app container in the clear. - Sessions are revocable. Signing out ends the session on the server too, and an admin can revoke any account from Settings → Users at any time.
- Nothing sensitive is cached locally. GitHub PATs and the Telegram API hash arrive masked from the server and are read-only here; the Telegram login code and 2FA password are typed here but handled only by your own worker; a settings export is written to the temp directory for the share sheet, never to a browsable one.
- Optional local gate. Face ID / Touch ID (falling back to the device passcode) can be required before the console opens, and re-arms whenever the app leaves the screen. That protects an unlocked, unattended device; the server credentials are what actually protect the data.
Layout
FTStoreAdmin/
FTStoreAdminApp.swift @main — session, tint, the biometric re-lock
Console/ One file per screen, plus the transport
AdminAPI.swift HTTP, multipart upload, raw payloads, login
AdminSession.swift The single door: token, 401 handling, lock
AdminKeychain.swift Where the session token lives
AdminModels.swift Mirrors web/'s handler responses + request bodies
AdminRootView.swift Front door and table of contents
Admin*View.swift The screens in the table above
Support/
AdminConfig.swift Base URL, User-Agent — the only baked-in values
AdminTheme.swift The brand colour
AdminUIKit.swift Share sheet + image resize, inlined from NimbleKit
AnyCodable.swift For the open-ended payloads
Resources/ Info.plist, app icon
The app links no Swift package at all — only system frameworks (SwiftUI, Charts,
Security, LocalAuthentication, PhotosUI). That is deliberate: the few helpers it needed
from the FTStore engine are inlined in Support/, which keeps the binary small and keeps
"a decrypted IPA is a login form" easy to verify.
Configuration
The server address is injected at build time via FTSTORE_ADMIN_BASE_URL in
FTStoreAdmin.xcconfig (it flows into Info.plist as
FTStoreAdminBaseURL); leave it blank to use the built-in default, https://ftstore.app.
Never commit a token here — there is no field for one, and there should never be.
Building
macOS + Xcode required. No submodules, no packages, no deps step:
cd FTStore/admin
make # builds packages/FTStore-Admin.ipa
Or open FTStoreAdmin.xcodeproj in Xcode.
CI works like the app's and the Manager's: Admin Dev Repo builds every push that
touches admin/ and publishes the IPA plus a generated AltStore source
(admin-devrepo.json) to the rolling admin-dev prerelease, so the console updates
itself the same way the other two do. Admin Release cuts a draft release from an
admin-v* tag when a stable one is wanted.
https://github.com/FTStuff/FTStore/releases/download/admin-dev/admin-devrepo.json
The IPA is unsigned — sign it on-device at install with your own certificate.
License & credits
GPL-3.0, like the rest of the repo — see the root LICENSE. The console was
written for FTStore Admin's own API; the two UIKit helpers in Support/AdminUIKit.swift
are adapted from NimbleKit (MIT), and AnyCodable.swift comes
from FTStore's engine, which descends from Feather.