Subscription-aware feature gating for the Forge package family.
ForgeAccess is the feature-gating library in the Forge family of iOS packages. Generic over your own Subscription and Feature enums — bring your own payment SDK; the library never assumes one.
- Generic over host-defined
SubscriptionandFeatureenums. - Pluggable subscription source — adapt RevenueCat, StoreKit 2, your backend, or a static value via
SubscriptionStatusProviding. - Two override channels (feature-level + subscription-level) with built-in persisted, in-memory, and time-frame providers.
- Detailed
AccessProviderEvaluationfor debug UIs and analytics ("why was this feature blocked?"). - Ready-made
AccessControlViewdebug screen (iOS) — list every subscription and feature, force overrides, drill into evaluation details.
- iOS 18+
- macOS 15+ (debug screens are iOS-only)
- Swift 6.3+ (Xcode 26 or later)
- File → Add Package Dependencies…
- Paste
https://github.com/stefanprojchev/ForgeAccess.git - Set rule to Up to Next Major from
1.0.0
dependencies:[.package(url:"https://github.com/stefanprojchev/ForgeAccess.git", from:"1.0.0")],targets:[.target(
name:"YourApp",
dependencies:["ForgeAccess"])]import ForgeAccess
enumSubscription:String,SubscriptionProtocol{case pro, gold
vardescription:String{ rawValue.capitalized }}enumFeature:String,FeatureProtocol{case advancedExport, basicSearch, cloudSync
typealiasSubscription=AppSubscriptionvarname:String{ rawValue }varavailableInSubscriptions:Set<Subscription>{switchself{case.advancedExport:[.pro,.gold]case.cloudSync:[.gold]case.basicSearch:[] // free; default policy decides
}}vardescription:String{ name }}structMyStatusAdapter:SubscriptionStatusProviding{func cachedActive()->Set<Subscription>{ /* read from cache */ []}func refreshActive()asyncthrows->Set<Subscription>{ /* refresh */ []}}letaccess=ForgeAccess<Subscription,Feature>(configuration:.init(
status:MyStatusAdapter(),
defaultPolicy:.deny
))if access.isEnabled(.advancedExport){
// …
}The host app supplies subscriptions via SubscriptionStatusProviding:
cachedActive()— synchronous snapshot used for in-view feature gates. Must not block.refreshActive()— async refresh from the source of truth.
ForgeAccess never assumes a payment SDK — write a thin adapter over RevenueCat, StoreKit 2, your backend, or a hard-coded value. For previews and tests, use StaticSubscriptionStatusProvider(active: [.pro]).
Two channels, each evaluated in provider order — the first .on/.off wins:
| Channel | Affects | Built-in providers |
|---|---|---|
| Subscription | The effective active subscription set | PersistedSubscriptionOverrideStore, InMemorySubscriptionOverrideStore, TimeFrameSubscriptionOverrideProvider |
| Feature | The feature decision directly (short-circuits subscription check) | PersistedFeatureOverrideStore, InMemoryFeatureOverrideStore, TimeFrameFeatureOverrideProvider |
The persisted store for each channel is wired in by default and evaluated first. Add more providers via additionalFeatureOverrides / additionalSubscriptionOverrides.
structRemoteConfigFeatureOverrides:FeatureOverrideProviderProtocol{letname="Remote Config"func isFeatureOverridden(_ feature:Feature)->OverrideState{
// .on / .off / .unchanged
.unchanged
}}letaccess=ForgeAccess<Subscription,Feature>(configuration:.init(
status:MyStatusAdapter(),
additionalFeatureOverrides:[AnyFeatureOverrideProvider(RemoteConfigFeatureOverrides())]))Useful for trials, launch promos, or sunset windows:
lettrial= TimeFrameSubscriptionOverrideProvider<Subscription>{ sub inguard sub ==.pro else{returnnil}return.init(state:.on, start: signupDate, end: signupDate.addingTimeInterval(14*86400))}letaccess=ForgeAccess<Subscription,Feature>(configuration:.init(
status:MyStatusAdapter(),
additionalSubscriptionOverrides:[AnySubscriptionOverrideProvider(trial)]))AccessControlView lists every subscription and feature, surfaces real vs. effective state, and lets the user force overrides via the persisted stores. Wrap it in a NavigationStack and present from a dev menu:
.sheet(isPresented: $showDevMenu){NavigationStack{AccessControlView(access: access)}}Tapping a feature opens FeatureEvaluationDetailsView — a full evaluation trace including which override provider decided.
access.evaluate(feature) returns AccessProviderEvaluation with:
finalDecisiondecidedByOverride+decidingOverrideactiveSubscriptions,requiredSubscriptionsoverrideStates— every provider's decision in evaluation order
Useful for analytics or in-app debug surfaces.
ForgeAccess<Subscription, Feature>
├─ ActiveSubscriptionProvider ← cached/fresh, applies subscription overrides
│ └─ SubscriptionStatusProviding ← host-supplied (RevenueCat / StoreKit / server)
└─ AccessProvider ← evaluates feature, applies feature overrides
Feature overrides short-circuit the subscription check. If every feature-override provider returns .unchanged, the package falls back to the subscription rules and defaultPolicy.
ForgeAccess is part of the Forge family of Swift packages for iOS.
| Package | Description |
|---|---|
| ForgeCore | Thread-safe primitives for iOS Swift packages. |
| ForgeInject | Dependency injection with constructor and property wrapper support. |
| ForgeObservers | Reactive system observers — connectivity, lifecycle, keyboard, and more. |
| ForgeStorage | Type-safe key-value, file, and Keychain storage. |
| ForgeDB | Type-safe repository pattern and GRDB-backed SQLite persistence. |
| ForgeOrchestrator | Orchestrate app flows — startup gates, data pipelines, and continuous monitors. |
| ForgePush | Push notification management — permissions, tokens, and routing. |
| ForgeLocation | Location triggers — geofencing, significant changes, and visits. |
| ForgeBackgroundTasks | Background task scheduling and dispatch. |
| ForgeNetworking | Typed, async/await-first HTTP networking with auth, retry, and background transfers. |
| ForgeLog | Structured logging with pluggable providers and a built-in inspector UI. |
| ForgeAccess | Subscription-aware feature gating with override channels and debug UI. |
ForgeAccess is released under the MIT License. See LICENSE.