Impulse AI Docs
Intern dokumentasjon
skipLink.label

Feature Gating & Subscription Tiers

Feature gating styrer hva brukeren kan se og gjøre basert på abonnementsnivå. Arkitekturen skiller mellom to uavhengige lag: feature-synlighet (klient) og kvotehåndhevelse (server).

To-lags modell

heading.anchorLabel
┌─────────────────────────────────────────────────────────┐
│ iOS / Android │
│ │
│ FeatureGate.kt (KMP) QuotaStoreWrapper │
│ ───────────────────── ────────────────── │
│ "Skal jeg VISE dette?" "Kan jeg GJØRE dette?" │
│ Basert på tier-streng Basert på API-svar │
│ Ren logikk, ingen API Synkronisert med server │
│ │
│ hasMasteryFeatures("mastery") canCapture → true/false │
│ requiresUpgrade("trial", canCheckin → true/false │
│ "mastery") canStartSession → true │
└──────────────────────────────┬──────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ API (Railway) │
│ │
│ impulse-quota-service.ts │
│ ───────────────────────── │
│ isSubscriptionActive() → binær: active eller ikke │
│ canCaptureImpulse() → kvotesjekk + tilgang │
│ canCreateCheckin() → kvotesjekk + tilgang │
│ canStartSession() → kvotesjekk + tilgang │
│ │
│ subscription_config DB → kvoter konfigurerbare uten │
│ redeploy │
└─────────────────────────────────────────────────────────┘
LagAnsvarKildeEksempel
Feature visibilitySkal UI-element vises/låses?FeatureGate.kt (tier-streng)Breakthroughs kun for mastery
Quota enforcementKan bruker utføre handling?impulse-quota-service.ts (API)3 impulser på trial

Tier-hierarki

heading.anchorLabel
trial (0) < foundation (1) = partner (1) < mastery (2)
TierRangPrisImpulserSjekk-innMastery-features
Trial0Gratis 7 dager3 totalt7 totaltNei
Foundation (Impulse)1$6/mnd30/mnd90/mndNei
Partner1Gratis30/mnd90/mndNei
Mastery (Pro)2$12/mnd60/mnd (∞)180/mnd (∞)Ja

Trial-brukere ser alle UI-elementer (full visibility), men handlinger er kvotebegrenset. Etter trial-utløp vises paywall.

KMP FeatureGate

heading.anchorLabel

Fil: shared/src/commonMain/kotlin/com/im/shared/models/FeatureGate.kt

Ren Kotlin object — ingen state, ingen store, ingen API-kall. Tar en tier-streng og returnerer booleans.

object FeatureGate {
fun tierRank(tier: String): Int = when (tier) {
"trial" -> 0
"foundation" -> 1
"partner" -> 1
"mastery" -> 2
else -> 0
}
fun hasMasteryFeatures(tier: String): Boolean =
tier == "mastery"
fun requiresUpgrade(currentTier: String, requiredTier: String): Boolean =
tierRank(currentTier) < tierRank(requiredTier)
fun hasFullVisibility(tier: String): Boolean = true
}

iOS-integrasjon

heading.anchorLabel

QuotaStoreWrapper.swift eksponerer FeatureGate som computed properties:

QuotaStoreWrapper.swift
var tier: String { quotaStatus?.tier ?? "trial" }
var hasMasteryFeatures: Bool { FeatureGate.shared.hasMasteryFeatures(tier: tier) }
func requiresUpgrade(to targetTier: String) -> Bool {
FeatureGate.shared.requiresUpgrade(currentTier: tier, requiredTier: targetTier)
}

Views bruker dette direkte:

// InsightsView — vise/skjule breakthroughs
private var isMastery: Bool {
KMPStoreProvider.shared.quotaStore.hasMasteryFeatures
}

Legge til ny feature-gate

heading.anchorLabel
  1. Legg til funksjon i FeatureGate.kt:

    fun hasAdvancedAnalytics(tier: String): Boolean =
    tierRank(tier) >= 2
  2. Legg til computed property i QuotaStoreWrapper.swift:

    var hasAdvancedAnalytics: Bool {
    FeatureGate.shared.hasAdvancedAnalytics(tier: tier)
    }
  3. Bruk i view:

    if quotaStore.hasAdvancedAnalytics {
    AdvancedAnalyticsCard()
    }

Legge til nytt tier

heading.anchorLabel
  1. Oppdater FeatureGate.kt — legg til i tierRank() switch
  2. Oppdater subscription-tiers.ts — legg til i TIER_HIERARCHY
  3. Oppdater QuotaStatus.kt — legg til i tierDisplayLabel
  4. Oppdater subscription_config DB-tabell — legg til kvoter

Binær tilgangsmodell

heading.anchorLabel

Abonnement er binært — enten aktiv eller ikke. Ingen mellomtilstander.

isSubscriptionActive():
trial → trial_ends_at > nå
foundation → status === "active"
mastery → status === "active"
cancelled → subscription_end_date > nå
partner → status === "active"
alt annet → false → paywall

Apple/Google grace period

heading.anchorLabel

App Store og Google Play håndterer grace period og billing retry på sin side. Vi mottar webhook-events som oppdaterer iap_subscriptions-tabellen, men disse statusene påvirker ikke brukerens tilgang.

StatusHvorPåvirker tilgang?
activeprofiles + iap_subscriptionsJa — aktiv
expiredprofiles + iap_subscriptionsJa — paywall
cancelledprofiles + iap_subscriptionsJa — aktiv til end_date
grace_periodKun iap_subscriptionsNei — kun audit-data
billing_retryKun iap_subscriptionsNei — kun audit-data
pausedKun iap_subscriptions (Android)Nei — kun audit-data

Webhook skriver grace_periodprofiles.subscription_status forblir active → bruker beholder tilgang → Apple sender EXPIRED → status settes til expired → paywall.

Kvotehåndhevelse (server-side)

heading.anchorLabel

Kvoter håndheves utelukkende server-side via impulse-quota-service.ts. Klienten viser kvote-status men kan ikke omgå den.

Konfigurering uten redeploy

heading.anchorLabel

Kvoter lagres i subscription_config-tabellen:

TierImpulserPeriodeSjekk-innPeriodeTrial-dager
trial3total7total7
foundation30monthly90monthly
mastery60monthly180monthly
partner30monthly90monthly

Endre verdier i Supabase Studio → Redis-cache utløper etter 5 min → nye kvoter aktive.

GET /api/v1/quota/status
{
"tier": "foundation",
"status": "active",
"can_capture": true,
"can_checkin": true,
"can_start_session": true,
"impulses": { "used": 12, "limit": 30, "period": "monthly" },
"checkins": { "used": 5, "limit": 90, "period": "monthly" },
"marketed_as_unlimited": false
}

Filreferanser

heading.anchorLabel
KomponentFil
FeatureGate (KMP)shared/.../models/FeatureGate.kt
QuotaStatus (KMP)shared/.../models/QuotaStatus.kt
QuotaStore (KMP)shared/.../stores/quota/QuotaStore.kt
QuotaStoreWrapper (iOS)iosApp/Stores/QuotaStoreWrapper.swift
Kvote-service (API)apps/api/src/services/impulse-quota-service.ts
Subscription middleware (API)apps/api/src/middleware/subscription.ts
Tier-konstanter (TS)packages/shared/src/constants/subscription-tiers.ts
Subscription-typer (TS)packages/shared/src/types/subscription.ts
Kvote-konfig (DB)subscription_config-tabellen
Paywall (iOS)iosApp/Features/Subscription/SubscriptionPaywallView.swift