This roadmap maps directly onto our GitLab tracking:
- GitLab Milestone:
Project Neo / 5.0— the umbrella that contains everything in this document. All issues for the v5.0 release are filed against this milestone. - GitLab Issues: each
M0–M10section below is an independent GitLab issue under the milestone above. TheM-prefix is just a stable identifier; in GitLab they're issues, not milestones. Issues are independently scheduled, assigned, and shipped. - Tasks within an issue: checklist items inside the issue's description.
Project Neo is the Windscribe iOS/tvOS modernization effort that lands as version 5.0. The end state: iOS 18 floor, full SwiftUI rebuild, @Observable + async/await, GRDB persistence, constructor DI, Swinject/Combine/Realm/SnapKit retired, Swift 6 language mode.
What's true today (April 2026):
- Working integration branch is
neo-5.0. - No firm UI designs yet. Major flows are stable in concept (Main + connect + server list + preferences), but pixel-level redesigns are pending.
- Releases are infrequent — every issue must ship clean on the first prod release; no fast-follow window.
Two pieces of modernization work are already in flight and are not re-planned here. They're tracked as parallel dependencies:
- Keychain consolidation — actively being worked on now.
- Realm → GRDB migration — drafted on
aw/realm-to-grdb, parked behind the Keychain work. Has its own multi-step planning document; will move intodocs/neo/plans/M2-grdb-migration.mdwhen M2 work resumes.
This document is the umbrella roadmap. Each issue below should ultimately get its own plan file in docs/neo/plans/ when work begins on it (M0's plan is at M0-guardrails.md).
The driving principle is foundational work + guardrails for teammates. That dictates the sequencing:
- Guardrails before code. Lint rules + scaffolding + a canonical reference module first, so any contributor's next PR is automatically pulled toward the target architecture instead of away from it.
- Lift the floor early. iOS 18 + tvOS 18 + Swift 6-ready settings unlock the manifesto's patterns and let us delete a lot of
#availablecruft. - Strangler over big-bang. Existing screens stay legacy until they're rebuilt. New modules use only the modern stack. We never mass-migrate.
- Track Realm→GRDB and Keychain as dependencies, not duplicates. Their plans already exist; Neo pulls them in by reference.
- Defer UI rebuild issues until designs land. Sketch the shape (M7–M9) so the path is visible, but don't commit detail.
- Platform-agnostic foundation. M0–M6 are explicitly not iOS-first. The patterns they establish apply identically to iOS and tvOS — both use SwiftUI on the modern stack. The UI rebuilds in M8/M9 are independently orderable: whichever platform's designs land first goes first.
See ../DECISIONS.md for the full architectural decision log. Headlines:
mainstays launchable at all times. Neo work lands onmainonly if strictly forward-compatible.- Two-track landing strategy with explicit per-issue landing rules (table below).
- tvOS floor lifts to 18 in M1, on
neo-5.0only. - Canonical reference module:
Windscribe/Features/About/(per ND-005, supersedes ND-003),@available(iOS 17.0, *)-gated. - Enforcement: custom SwiftLint regex rules scoped to
Windscribe/Features/**andWindscribeTV/Features/**.
| Issue | Default landing | Why |
|---|---|---|
| M0 — Guardrails | main → neo-5.0 |
Docs + lint rules + @available-gated reference module. Purely additive. |
| M1 — iOS 18 floor | neo-5.0 only |
Deployment-target bump breaks shippability to current floor. |
| M2 — Persistence (Keychain + GRDB) | main → neo-5.0 |
Keychain + GRDB plans designed to ship from main on the existing floor. |
| M3 — DI modernization (shims) | main → neo-5.0 |
Adapter shims are additive; legacy DI graph untouched. |
| M4 — Async/Sendable boundaries | mixed (see issue) | T4.1, T4.2 are additive → main. T4.3 depends on M1 → neo-5.0. |
| M5 — Testing modernization | main → neo-5.0 |
Swift Testing runs alongside XCTest; additive. |
| M6 — Tunnel/extensions cleanup | mixed (see issue) | IKEv2 consolidation can land on main (no behavior change). Guard deletion + SiriIntents removal → neo-5.0. |
| M7 — Second rebuilt screen | main → neo-5.0 |
Same @available gating as M0 reference. |
| M8 — iOS UI rebuild | neo-5.0 only |
Replacement of legacy surface; relies on iOS 18 floor. |
| M9 — tvOS UI rebuild | neo-5.0 only |
Same as M8 for tvOS. |
| M10 — v5.0 launch | neo-5.0 → main (the merge) |
The single moment we collapse the two branches. |
Cadence: main → neo-5.0 merge runs at least weekly (or after any non-trivial PR lands on main). neo-5.0 → main runs only at v5.0 launch.
| Dimension | Today | Target |
|---|---|---|
| iOS deployment target | 15.0 (one outlier at 16.0, one extension at 17.5) | 18.0 |
| tvOS deployment target | 17.5 | 18.0 |
| Swift version | 5.0 dominant; tests partially on 6.0 | 6.0 across all targets |
SWIFT_STRICT_CONCURRENCY |
Unset everywhere | targeted → complete per module |
| Swinject registrations | ~238 across iOS + tvOS modules | 0 (constructor injection + @Environment) |
| Realm-touching files | 48, leaked beyond Data/Database/ |
0 |
| Combine-touching files | 204 | 0 (in new code; Apple-API boundaries converted with .values) |
| SnapKit files | 14 (mostly Modules/PlanUpgrade/) |
0 |
| RxSwift | 0 (already removed) | 0 |
| GRDB | not present | primary persistence |
| SwiftUI views in tree | ~89 (Auth, Preferences, Popups, News Feed, Widget) | all UI |
| Major UIKit hubs remaining | MainViewController (~5,200 lines), PlanUpgrade (~2,666 lines), tvOS (3,300+ lines) |
rebuilt |
@Observable view models |
0 (all 37+ existing VMs are ObservableObject) |
all VMs |
| Test framework | XCTest (36 files); Swift Testing 0 | Swift Testing for new tests |
Each issue below is one GitLab issue under the Project Neo / 5.0 milestone. They are ordered by readiness — earlier issues unblock later ones. Tasks inside a single issue can usually run in parallel.
For the up-to-date task breakdown of any active issue, see its plan file in this directory. M0's plan is at M0-guardrails.md.
Lands on: main → neo-5.0. All M0 work is forward-compatible. Docs and lint rules are purely additive. The About reference module ships behind @available(iOS 17.0, *) and runtime-branches against the existing legacy About — users on iOS 15/16 are unaffected.
Goal: Make it cheaper for teammates — and their AI agents — to write Neo-shaped code than to copy a legacy pattern. Establishes the rules, the working examples, and the shared context layer before the rebuild begins.
Outcome:
- Shared context layer in tree (this directory,
../DECISIONS.md,../STATE.md,../../PROJECT_NEO.md). - SwiftLint custom rules forbidding the legacy patterns inside
Windscribe/Features/**andWindscribeTV/Features/**. - Canonical reference module
Windscribe/Features/About/end-to-end Neo:@Observable@MainActorVM, protocol-injected services, SwiftUI view via@Environment(@Entry), Swift Testing tests with mock services. WindscribeTV/Features/skeleton + tvOS-pattern doc section so tvOS isn't blocked if its designs land first.
For per-task detail, see M0-guardrails.md.
Lands on: neo-5.0 only. Every task in M1 commits the codebase to a floor that current main-branch users haven't been migrated to yet. PRs open against neo-5.0.
Goal: Lift the foundation so the manifesto's patterns are unconditionally available, and so we shed #available cruft.
Outcome:
- All iOS targets at
IPHONEOS_DEPLOYMENT_TARGET = 18.0. tvOS targets atTVOS_DEPLOYMENT_TARGET = 18.0. - All targets at Swift 6 language mode or Swift 5 with
SWIFT_STRICT_CONCURRENCY = complete. Per-target push: WindscribeTests → HomeWidget → AppIntents → SiriIntents → tunnel extensions →Windscribe. - All
#available(iOS 15.x)and#available(iOS 16.x)guards deleted (12 known sites inVPNUserSettings.swift,VPNManagerUtils+IKEV2Credentials.swift,ConfigurationsManager+Connect.swift). legacyOS(<= 13) branch inconfigureIKEV2deleted.HomeWidget#available(iOSApplicationExtension 17.0)fallbacks deleted;PreviewProviderreplaced with#Previewmacro.
Verification: All schemes build. Test suite passes. grep -rn "#available(iOS 1[5-7]" Windscribe/ WindscribeTV/ PacketTunnel/ WireGuardTunnel/ returns zero.
Lands on: main → neo-5.0 for D2.1 (already merged) and D2.2. The Realm-removal PR (D2.4) lands on whichever branch is active when the long-tail soak window closes — see "Launch cadence" below.
Goal: Move sensitive state into the Keychain (done — !1323), then port the remaining LocalDatabase-resident state from Realm to GRDB, then ship a normalization pass, and finally retire Realm once /CheckUpdate force-upgrade has bedded in.
This issue is a dependency tracker, not a new plan. Source-of-truth plans:
- Keychain consolidation: ✅ merged to
mainas !1323 (#1040), 2026-05-01. - Realm→GRDB migration:
aw/realm-to-grdb, plan + execution log at~/.claude/plans/we-ve-maybe-briefly-touched-delegated-pike.md. Plan migrates intoM2-grdb-migration.mdonce D2.2 is opened for review.
Outcome:
- D2.1 ✅ Keychain consolidation merged to
mainas !1323 (2026-05-01). Sessions, OldSession, OpenVPN/IKEv2 server credentials, custom-config credentials, and session auth hash now live in the Keychain viaSessionKeychainStore/Preferences+Keychain.LocalDatabasegained threeclear*methods used byMigrationRepositoryto wipe stale Realm rows post-port.LocalDatabase.{saveSession, getSessionPublisher, saveOldSession, getOldSession, save{OpenVPN,IKEv2}ServerCredentials}are gone. - D2.2 ✅ GRDB Migration PR merged as !1311 (2026-05-05) on
main(the Pre-Neo release). 14 entity families mirrored 1:1; sessions/credentials remain Keychain-resident. Realm +RealmToGRDBMigratorstay linked as the migration source + fallback. M2 #1050 closed. - D2.3 → #1065 (split out from #1050): pure GRDB v1→v2 migration covering the ~10 surviving normalization items (the original 12 minus the two moot'd by the Keychain move — flatten Session.alc / merge OpenVPN+IKEv2 credentials tables). Decoupled from D2.4; can ship as soon as one main release after D2.2.
- D2.4 → #1066 (split out from #1050): Realm-removal PR ships post-Neo + 3-month forced-upgrade soak (see Launch cadence). Drops
LocalDatabaseImpl, therealm-swiftSPM packages, target linkage, the migrator, and the keychainclear*migration hooks.
Launch cadence (gated by /CheckUpdate nag + force-upgrade):
- Phase 1 — "Pre-Neo" release (D2.2 ships): GRDB migration goes live to
main. Begin nag-to-update via/CheckUpdatefor users below this version. Introduce the force-upgrade capability via/CheckUpdate(the mechanism, not yet enforced for this version). Goal: maximum natural-update soak time before Neo lands; ramp telemetry on the migrator (success rate, digest hits, fallback frequency). - Phase 2 — "Neo 5.0" launch (M10): UI rebuild + iOS 18 floor + the bulk of Neo. Continue nagging users below Neo via
/CheckUpdate; don't force-upgrade them — Neo is a bigger jump and users get to choose their moment. Realm +RealmToGRDBMigratormust still ship in Neo — anyone upgrading from a pre-Pre-Neo build directly to Neo still needs the migrator to run before their custom configs are lost. - Phase 3 — "Post-Neo" cleanup: ≥3 months after Neo ships, flip
/CheckUpdateto force-upgrade everyone below Pre-Neo (target = "at least Pre-Neo," not Neo itself, so iOS 15-17 users still on Pre-Neo are unaffected; only stragglers below Pre-Neo get pushed). Once that's bedded in (every active user has run the migrator at least once), ship #1066 (D2.4 — Realm removal).
The 3-month gate is the explicit soak window. The forced-upgrade-to-Pre-Neo (not to Neo) preserves the iOS-floor separation set up in M0/M1's branching strategy: iOS 15-17 users stay on Pre-Neo indefinitely with Realm linked; only iOS 18+ users see Neo.
Once GRDB is in tree (post D2.2 step 2), Windscribe/App/Environment+Dependencies.swift should expose persistence stores via @Entry so the M0 reference module switches to consuming GRDB through these.
Lands on: main → neo-5.0. Adapter shims are additive — they live alongside the existing Swinject graph and don't change legacy behavior.
Goal: Stop adding to the Swinject graph. New modules use constructor injection + @Environment only. Existing 238 registrations decay as features migrate; we don't proactively rewrite legacy DI.
Outcome:
- A documented boundary policy: the Swinject
Assembler.resolvesite for a Neo feature is at the composition seam only. Inside the feature, DI is constructor + environment. - Adapter shims for the 5–10 most-used legacy services. Initial set:
VPNConnecting,ServerProviding,CredentialStoring,PreferencesReading,SessionProviding. Each: protocol + adapter holdinglet legacy: LegacyTyperesolved once via Swinject at the seam. @Entryvalues added toEnvironmentValues.- SwiftLint rule forbidding
Assembler.resolvecalls inWindscribe/Features/**andWindscribeTV/Features/**.
Verification: grep -rn "Assembler.resolve" Windscribe/Features/ WindscribeTV/Features/ returns zero. M0 reference module has no Swinject reference anywhere in its files.
Lands on: mixed.
- T4.1, T4.2 →
main → neo-5.0. Wrapping concrete types in protocols and adding anAsyncStreamsurface alongside the existing Combine surface is purely additive. - T4.3 →
neo-5.0only. The async overrides forstartTunnel/stopTunnel/sleep/handleAppMessageonly become available with iOS 18 lifecycle changes from M1.
Goal: Convert the Apple-API boundaries that new Neo code will consume — wrap them so feature code never sees Combine or completion handlers.
Outcome:
- T4.1: Swift protocols wrapping
WSNetServerAPIandWSNetPingManager(parallel to existingWSNetBridgeAPIType). DI sites updated inManagers/VPN/ControlPlane.swiftandManagers/Latency/LocalPingManager.swift. Mockable for tests. - T4.2:
VPNConnecting.status: AsyncStream<ConnectionStatus>wrappingNEVPNStatusDidChange. - T4.3:
async-style overrides forstartTunnel/stopTunnel/sleep/handleAppMessagein bothPacketTunnel/PacketTunnelProvider.swiftandWireGuardTunnel/PacketTunnelProvider.swift. - T4.4: "Combine → AsyncSequence" conversion guideline appended to
../../PROJECT_NEO.md. - T4.5 (main → neo-5.0): Retire M3's narrow
@unchecked Sendableexemption (per ND-006). M3 introduced@unchecked Sendableon six legacy-adapter classes and four retroactive struct conformances; M4 replaces them with proper Sendability:- Mark these legacy protocols
Sendabledirectly:VPNManager,LocationListRepository,CredentialsRepository,Preferences,UserSessionRepository,LookAndFeelRepositoryType— and drop@uncheckedfromLegacyVPNConnector,LegacyServerProvider,LegacyCredentialStore,LegacyPreferencesReader,LegacySessionProvider, and M0'sLegacyLookAndFeelObserver. - Move the retroactive
@unchecked Sendableextensions onLocationModel,DatacenterModel,SessionModel,ServerCredentialsModel(currently at the bottom of the M3 protocol files inWindscribe/Services/Protocols/) to directSendableconformance on each type's definition. - Delete the "@unchecked Sendable is allowed at the legacy-adapter seam" subsection from
../../PROJECT_NEO.mdonce the workarounds are gone.
- Mark these legacy protocols
Verification: A new feature module can subscribe to VPN status with for await status in vpn.status without import Combine. Tunnel logs show no behavior regression on physical hardware. T4.5: grep -rn "@unchecked Sendable" Windscribe/Services/Protocols/ returns zero.
Lands on: main → neo-5.0. Swift Testing runs alongside XCTest in the existing test target — purely additive.
Goal: Adopt Swift Testing for new tests; preserve XCTest for legacy.
Outcome:
- M0's reference module has a Swift Testing suite (delivered as part of M0).
- Mock guidance documented: how to write a
MockX: ProtocolXinstead of mocking concrete impls. Two worked examples (struct mock with stored callbacks, actor mock for state) live alongside the reference module. - GRDB in-memory
DatabaseQueuetest fixture pattern documented (depends on M2 D2.2). - CI's
fastlane testcollects Swift Testing results (XCResult parsing) alongside XCTest.
Lands on: mixed.
- T6.1 →
main → neo-5.0. Consolidating the two IKEv2 assembly sites into one (without deleting any#availablebranches) is a pure refactor. - T6.2, T6.3 →
neo-5.0only. Deleting theSiriIntentsextension drops Shortcuts support for iOS 15 users (AppIntents requires iOS 16+).
Goal: Apply the iOS 18 floor benefits to the tunnel code. Consolidate duplication.
Depends on M1 (for T6.2/T6.3).
Outcome:
- T6.1: Single IKEv2 protocol-assembly site. Currently duplicated between
IKEv2VPNConfiguration.buildProtocolandconfigureIKEV2WithSavedCredentials.#availablebranches preserved on this PR. - T6.2:
SiriIntentsextension deleted.ShowLocationIntentHandlerfolded intoAppIntents/ShowLocation.swift. The 3 dead files inSiriIntents/VPN Managers/go with it. - T6.3: Provisioning profiles + entitlements verified after target removal.
- Tunnel exclusion flags applied unconditionally (
includeAllNetworks,excludeLocalNetworks) — guards removed in M1; this is the cleanup pass.
Verification: Connect/disconnect on hardware, IKEv2 + WG + OpenVPN. Shortcuts that used SiriIntents still resolve via AppIntents.
Lands on: main → neo-5.0. Same @available(iOS 17.0, *) gating as M0's reference module.
Goal: Prove the M0 reference module is truly copy-able. M0 ships About; M7 picks a sibling Preferences leaf (candidates: GeneralSettings or LookAndFeel — the latter is intentionally heavier and serves as a stress test of the recipe) and applies the recipe verbatim.
Depends on M0 (About reference) and M3 (DI shims established).
Outcome:
- A second migrated screen — candidate:
GeneralSettingsView+GeneralSettingsViewModelmoved toWindscribe/Features/General/. - Migration recipe documented in
../../PROJECT_NEO.md: "How to convert anObservableObjectview model to@Observablein this codebase." Steps, gotchas, before/after diff. - Swift Testing tests for the migrated VM.
If the recipe required any judgment calls beyond mechanical transformation, that's a signal to refine the M0 scaffolding before scaling.
Verification: Screen behaves identically to before, ships in a release. Recipe doc is concrete enough that a teammate can apply it to the next preferences leaf without help.
Lands on: neo-5.0 only. Major-flow rebuilds replace existing legacy surface and rely on the iOS 18 floor from M1.
Goal: Rebuild the major iOS flows — Main + connect, server list, preferences hub, PlanUpgrade.
Cannot start in detail until iOS UI designs land. This issue is a placeholder in this plan; each flow gets its own sub-issue (and plan file in docs/neo/plans/) when designs are ready.
M8 and M9 are independently orderable. Either can go first based on which platform's designs land first. Both depend on the same foundation (M0–M6).
Sketch of scope:
- Server list — likely the easiest to rebuild against existing data; depends on GRDB (
ValueObservation). - Main / Connect / Status — biggest single legacy hub (~5,200 lines across
MainViewController/). Will likely be subdivided into Connect, Status, and Quick-Connect sub-features. - Remaining Preferences VMs — ~15 view models to migrate from
ObservableObject→@Observable(M7's recipe applied repeatedly; About is done as M0's reference, M7's pick is the next). Views are already SwiftUI. - PlanUpgrade — full UIKit + SnapKit rebuild as SwiftUI. Retires the SnapKit dependency.
- Popups — 13 view models to migrate (views already SwiftUI).
- Authentication VMs — 5 view models to migrate (views already SwiftUI).
Each sub-rebuild ships behind whatever feature flag / staged rollout the designs dictate.
Lands on: neo-5.0 only. Same reasoning as M8 — major rebuild relies on the tvOS 18 floor from M1.
Goal: Bring tvOS to the same modern stack. tvOS UI is currently 100% UIKit (3,300+ lines, 13 view controllers, 5 routers).
Cannot start in detail until tvOS UI designs land. This issue gets its own plan file in docs/neo/plans/ when designs are ready.
M8 and M9 are independently orderable. If tvOS designs land first, M9 runs first — the foundation (M0–M6) serves both platforms equally, and WindscribeTV/Features/ is set up at M0 specifically so tvOS isn't blocked on an iOS reference module. The only sequencing constraints are GRDB (M2) for any rebuild that consumes persistence, and M1's tvOS 18 lift before any tvOS Neo screen.
Sketch of scope:
- All 13 tvOS view controllers rebuilt as SwiftUI views with
@Observableview models inWindscribeTV/Features/. - All 5 tvOS routers replaced with
NavigationStack-based flows. - The first rebuilt tvOS screen plays the same role for tvOS that About does for iOS — it's the canonical reference once it lands.
- DI shims (M3) reused; the Swinject graph in
WindscribeTV/Dependencies/decays as features migrate.
Lands on: neo-5.0 → main (the merge). This is the single moment we collapse the two branches: neo-5.0 merges back to main, the v5.0 tag is cut from the merge commit, and the team's working branch becomes main again. From this point on, main is the iOS 18 / tvOS 18 / Neo-only branch.
Goal: Ship Project Neo as Windscribe iOS/tvOS 5.0.
Outcome:
- App Store metadata refresh.
- Versioning bump in
Config.xcconfigand Info.plists. - Release-notes draft surfacing user-visible changes.
- Final verification on physical hardware across all three tunnel protocols and both platforms.
- Realm fallback retention plan: even after the GRDB migration ships, we keep the Realm reader linked for one release — per the persistence plan's "infrequent release" safety net.
| Concern | File |
|---|---|
| Contributor + AI-agent rules | ../../PROJECT_NEO.md |
| Architectural decision log | ../DECISIONS.md |
| Execution log | ../STATE.md |
| Current contributor guidance | ../../../AGENTS.md |
| Current build settings | Windscribe.xcodeproj/project.pbxproj, Windscribe/Environments/Config.xcconfig |
| Lint config (extend in M0) | .swiftlint.yml |
| Current DI graph | Windscribe/Dependencies/AppModules/iOS/AppModulesCommon.swift, AppModulesViewModels.swift, AppModulesViewController.swift, AppModulesRouters.swift, AppModulesManagers.swift |
| Realm scope | Windscribe/Data/Database/, Windscribe/Models/ |
| Tunnel paths | Windscribe/Managers/VPN/Utils/ConfigurationsManager.swift, VPNUserSettings.swift, VPNManagerUtils+IKEV2Credentials.swift |
| Legacy hub to rebuild later | Windscribe/ViewControllers/MainViewController/ |
- Each issue ships independently — none of the
main → neo-5.0issues depend on the v5 release. They land onmain, ship in a normal release, and flow toneo-5.0via the periodic merge. - Each issue has a "ship it" gate: build clean on all schemes,
fastlane testpasses, lint passes, and where physical hardware testing applies (M1, M4, M6) the three tunnel protocols connect/disconnect cleanly. ../../PROJECT_NEO.mdand../../../AGENTS.mdare updated as each issue changes the rules or ships work.- Branch hygiene:
main → neo-5.0runs at least weekly and after any non-trivial PR lands onmain.neo-5.0 → mainruns only at v5.0 launch. A merge that takes more than 30 minutes to resolve is a signal that something Neo-only landed onmainby mistake — investigate before merging. - Cross-agent alignment is maintained by this in-tree doc surface. Every PR that closes a task updates
../STATE.md(including which branch it landed on). Every architectural decision is logged in../DECISIONS.mdas part of the PR that implements it. Private agent memory is for in-flight thinking only — anything another teammate would need is in the repo.