Lore is a native SwiftUI city-history guide. Raise the camera on a street and Lore ranks the catalog places around you by direction and distance, entirely on the device, then opens a sourced story, timeline, gallery and narration for the one you pick. Browse the map from home, follow self-guided walks, and keep a private visit journal.
Status: version 1.2 is live on the App Store
(released September 5, 2026). Build 48 was archived, signed and uploaded to
TestFlight by this repository's manual release lane from commit
261d633.
main carries post-release fixes that have not shipped yet.
Unedited simulator captures of the shipped 1.2 build. Provenance (source SHA,
device, Xcode and SHA-256 per frame) is recorded in
fastlane/promo_screenshots.
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
- Live scanner. Camera, precise location, compass heading and device pitch rank nearby catalog places, with an explicit confidence tier (locked pin, bearing chip or directional hint) instead of a confident wrong answer. On-device Vision adds scene categories and legible signage text.
- Map and Discovery Deck. MapKit city map, category filters, city switcher, search, and an "Around you right now" rail of nearby places.
- Place cards and deep dives. Facts, stories, timelines, galleries, source links, Street View and Look Around, plus studio narration with an on-device voice fallback.
- Walking tours. Curated routes with per-stop stories, route notes and a tour Live Activity on the Lock Screen and Dynamic Island.
- Passport and journal. Visits, private notes and photos, milestones and shareable place cards.
- Accounts and Lore+. Email, Sign in with Apple, Google and Facebook (OAuth with PKCE); sessions in the Keychain; in-app account deletion. StoreKit 2 subscriptions and lifetime purchase with server-side transaction verification and restore. Browsing never requires an account.
- Universal app. iPhone and iPad (sidebar layout, landscape), Dynamic Type and VoiceOver support, offline-tolerant content cache.
flowchart LR
subgraph Device["iPhone / iPad"]
UI["SwiftUI features<br/>Map · Scanner · Place card · Tours · Passport · Profile"]
Stores["@Observable stores<br/>auth, entitlements, visits"]
Scanner["Scanner pipeline<br/>CoreLocation + heading + motion<br/>BearingProjector → ScannerRanking<br/>Vision (category + text)"]
API["LoreAPI<br/>URLSession REST client"]
Cache["AtlasCache<br/>stale-while-revalidate disk cache"]
Widget["LoreWidget extension<br/>tour Live Activity"]
end
subgraph Supabase["Supabase (private backend repo)"]
REST["PostgREST tables + RPCs<br/>row-level security"]
Auth["GoTrue auth"]
Fn["Edge Functions<br/>sync-apple-purchase · delete-account<br/>streetview · landmark-id"]
Storage["Storage<br/>private journal-photos bucket"]
end
UI --> Stores --> API
UI --> Scanner
Scanner --> Stores
API --> Cache
API --> REST & Auth & Fn & Storage
UI -. ActivityKit .-> Widget
| Layer | Path | Responsibility |
|---|---|---|
| App shell | Sources/Lore/App |
Entry point, routing, deep links (lore://place/…, lore://tour/…) |
| Features | Sources/Lore/Features/* |
One folder per surface: Map, Scanner, PlaceCard, Tours, Passport, Profile, Auth, Premium, Culture, Search, Share, Onboarding |
| Models | Sources/Lore/Models |
Codable value types mirroring the Supabase rows (Place, Story, Dive, Tour, Visit, Entitlement, …) |
| Networking | Sources/Lore/Networking |
LoreAPI (hand-written PostgREST/Auth/Functions/Storage client), AtlasCache, Config |
| Design system | Sources/Lore/DesignSystem |
Type scale, color, motion, reusable components |
| Shared | Sources/Shared |
Types compiled into both the app and the widget extension |
| Widget | Sources/LoreWidget |
WidgetKit/ActivityKit extension for the tour Live Activity |
LocationHeadingProviderstreams when-in-use location (10 m accuracy, 3 m distance filter), true heading (magnetic as a fallback) and camera pitch while the scanner is open, and requests temporary full accuracy only for that screen.BearingProjectoris pure great-circle math: bearing and distance from the device to each candidate place, and its angular offset from where the camera points.ScannerRankingscores candidates on proximity, prominence, gaze, novelty and interest, then caps the confidence tier using horizontal and heading accuracy and pitch, so a noisy fix produces a hint rather than a pin.DisambiguationStackpresents close calls as "one of these".VisionRecognitionServiceclassifies the frame and reads visible text on the device. It never claims a specific landmark identity from pixels.GeoScoutingServiceprobes ARKit geo-tracking coverage before offering the precise AR mode (GeoARSessionController).
Place catalogs are fetched per city, not per coordinate, so ranking needs no
location on the server. The ranking and projection code has no I/O and is
covered by unit tests (ScannerLogicTests, VisionRecognitionTests).
- The app talks to Supabase through
LoreAPI, a dependency-freeURLSessionclient. Public catalog reads (city,place_explore,story,dive,tour,city_culture, deal feeds,search_loreRPC) use the publishable anon key; user rows (visit,saved_place,user_prefs,entitlements,user_achievement) require the signed-in user's token and are scoped by row-level security. - The anon key in
Config.swiftis the publishable client key by design. No service-role key exists anywhere in this repository or its history. - Privileged work runs in Edge Functions:
sync-apple-purchasere-verifies StoreKit JWS transactions against Apple's chain before writing an entitlement,delete-accountperforms fenced account deletion,streetviewkeeps the Google Maps key server-side, andlandmark-idserves the opt-in image match. - Schema migrations and Edge Function source live in the private
lorebackend repository.scripts/verify-edge-function-contracts.mjsfails CI if deployable function code ever appears undersupabase/functionshere.supabase/contentholds reviewed editorial seed payloads only.
- Location is when-in-use only. There is no background location mode, no continuous location history and no region monitoring.
- Precise coordinates are not sent to Lore's servers. The scanner, Discovery Deck and tour guide use location on the device. Logging a visit sends only the place ID and its source, so the server records which catalog place a signed-in user marked, not where they were standing.
- The live camera feed is never uploaded. A frame leaves the device only when the user sends it: the opt-in Lore+ "Identify landmark" action requires sign-in, shows a disclosure and asks for confirmation before sending each single still frame, and scanner postcards go only where the user shares them through the system share sheet. Journal photos upload to a private storage bucket and are served through signed URLs.
- No tracking. No ad or analytics SDKs, and
NSPrivacyTrackingis false. - The privacy manifest declares what is
collected and why, and
PrivacyManifestTestschecks it against the shipping data flows. The public policy is at lore-web-liart.vercel.app/privacy.
Requirements: macOS with Xcode 26.3 or newer, XcodeGen,
and Ruby 3.3+ with Bundler for the Fastlane lanes (CI uses Ruby 3.4).
Lore.xcodeproj is generated from project.yml and is not committed.
brew install xcodegen
xcodegen generate
# Unit tests on a simulator (the scheme skips the StoreKit engine journeys)
xcodebuild test -project Lore.xcodeproj -scheme Lore \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro'
# StoreKit 2 purchase, restore, refund and expiry journeys (local StoreKitTest
# engine; CI runs this job on Xcode 26.3, and StoreKitTest activation depends on
# the local Xcode and simulator pairing)
xcodebuild test -project Lore.xcodeproj -scheme LoreStoreKitTests \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
CODE_SIGNING_ALLOWED=YES CODE_SIGN_IDENTITY=-
# The same unit-test gate CI runs, through Fastlane
bundle install
bundle exec fastlane tests
# Offline tests for the release tooling (no Apple credentials needed)
ruby fastlane/spec/lore_release_tooling_test.rbThe unit suite has 236 tests across scanner math, Vision parsing, networking resilience, StoreKit entitlement logic, account flows, privacy-manifest contracts and layout. The app runs against the production Supabase project with the publishable key; no local backend is needed.
| Workflow | Trigger | What it does |
|---|---|---|
| iOS CI and TestFlight | Pull request, push to main, manual |
Unit tests, unsigned Release compile with version/device-family checks, StoreKit journeys. The signed TestFlight job runs only on a manual dispatch from main with upload_to_testflight set, after that run's test jobs pass. |
| Release Tooling Tests | Pull request, push to main |
Offline Minitest suite for the release guards in fastlane/lib. |
| App Store · Preflight / Submit | Manual only | Read-only submission audit, or one explicit, version-pinned App Store Connect action (prepare, select build, submit, hold, swap, auto-release). |
| App Store · Screenshots | Manual only | Captures screenshots on the simulator with fastlane snapshot. No signing, no upload. |
| App Store · Upload Screenshots | Manual only | Uploads the committed screenshot set after proving it was captured from the named release commit. |
Release design:
- Nothing ships from a push. TestFlight uploads and every App Store
Connect action are manual workflow dispatches. The TestFlight job waits for
the same run's test jobs and refuses to run off
main. App Store actions take an explicit marketing version (and build, where one applies) instead of "latest", and mutations fail closed unless theAPP_REVIEW_HOLDvariable allows them. - Signing uses Fastlane Match with encrypted certificates and profiles in a private repository and an App Store Connect API key. Credentials live only in GitHub Actions secrets (CI-SETUP.md).
- Build numbers are one above the latest TestFlight build and never below the project floor, so reruns cannot collide.
- Supply chain. Every third-party action is pinned to a full commit SHA, and Dependabot is configured for both the actions and the Bundler lockfile.
The operator runbook is TESTFLIGHT.md; the App Store guard design is in docs/APP-STORE-RELEASE-TOOLING.md.
| Path | Purpose |
|---|---|
Sources/Lore |
Application source |
Sources/LoreWidget, Sources/Shared |
Widget/Live Activity extension and shared types |
Sources/LoreTests |
Unit, contract and StoreKit journey tests |
Sources/LoreUITests |
Screenshot capture and adaptive-layout UI checks |
StoreKit/Lore.storekit |
Local StoreKit configuration for simulator purchases and tests |
fastlane |
Test, screenshot, signing, TestFlight and App Store Connect lanes; release guards and their tests |
supabase/content |
Reviewed editorial seed payloads (no deployable functions) |
tools |
Content compilers, source-health checks, coverage audit and narration rendering |
docs |
Engineering standards, release tooling notes and working logs |
project.yml |
XcodeGen spec: targets, schemes, Info.plist keys, versions |
Read CONTRIBUTING.md before opening a pull request, and report vulnerabilities privately through SECURITY.md.
The source is published for evaluation and review, not as open source. Copyright 2026 Erick Dronski, all rights reserved: no permission is granted to copy, modify or redistribute it without written consent. See LICENSE.





