@tahag/react-native-feature-system
v1.0.0
Published
Cache-first entitlements and feature flags for React Native
Maintainers
Readme
@tahag/react-native-feature-system
Cache-first entitlements and feature flags for React Native. Owns the caching, hook lifecycle, and foreground refetch. You plug in your own fetchers — Supabase, Firebase, REST, anything.
Install
npm install @tahag/react-native-feature-systemPeer dependencies (you likely already have these)
npm install @react-native-async-storage/async-storage react-native-get-random-values uuidMigration from inline implementation
App.tsx — initFlags
Before
import { initFlags, refreshBucketingId } from './lib/flags/flagsStore'
initFlags().catch(console.warn)After
import { initFlags, refreshBucketingId } from '@tahag/react-native-feature-system'
import { supabase } from './lib/supabase'
initFlags({
fetcher: async () => {
const { data, error } = await supabase
.from('feature_flags')
.select('key, value, rules')
if (error) throw error
return data
},
getUserId: async () => {
const { data: { user } } = await supabase.auth.getUser()
return user?.id ?? null
},
}).catch(console.warn)refreshBucketingId import path changes but the call site is identical:
await refreshBucketingId()App.tsx — clearCachedEntitlements on sign out
Before
import { clearCachedEntitlements } from './lib/entitlements/useEntitlement'After
import { clearCachedEntitlements } from '@tahag/react-native-feature-system'Call site is identical:
await clearCachedEntitlements(activeProfile.uid)HomeScreen.tsx — useFlag
Before
import { useFlag } from '../lib/flags/useFlag'
const showWelcomeBanner = useFlag('show_welcome_banner', false)After
import { useFlag } from '@tahag/react-native-feature-system'
const showWelcomeBanner = useFlag('show_welcome_banner', false)No call site changes.
HomeScreen.tsx — useEntitlement
Before
import { useEntitlement } from '../lib/entitlements/useEntitlement'
const { granted: hasProFeature, loading: entitlementLoading } = useEntitlement(
'demo.pro_feature',
activeProfile?.uid ?? null
)After
import { useEntitlement } from '@tahag/react-native-feature-system'
import { supabase } from '../lib/supabase'
const { granted: hasProFeature, loading: entitlementLoading } = useEntitlement(
'demo.pro_feature',
{
userId: activeProfile?.uid ?? null,
fetcher: async (userId) => {
const { data, error } = await supabase
.from('user_entitlements')
.select('capability')
.eq('user_id', userId)
if (error) throw error
return data.map((row: { capability: string }) => row.capability)
},
}
)API
initFlags(options)
Call once at app startup.
initFlags({
fetcher: () => Promise<FlagRow[]>, // required — fetch all flags from your backend
getUserId?: () => Promise<string | null>, // optional — auth user ID for bucketing; falls back to anon UUID
cacheKey?: string, // default: 'flags.cache.v1'
anonIdKey?: string, // default: 'flags.anon_id'
})refreshBucketingId()
Call after auth state changes so percent rollouts re-evaluate against the signed-in user.
await refreshBucketingId()useFlag(key, defaultValue)
const showBanner = useFlag('show_welcome_banner', false)useEntitlement(capability, options)
const { granted, loading } = useEntitlement('pro_feature', {
userId: string | null,
fetcher: (userId: string) => Promise<string[]>,
cacheKeyPrefix?: string, // default: 'entitlements.cache'
cacheVersion?: string, // default: 'v1'
refetchOnForeground?: boolean // default: true
})clearCachedEntitlements(userId)
Call on sign out to clear the entitlements cache for that user.
await clearCachedEntitlements(activeProfile.uid)Caching behaviour
Both entitlements and flags use a cache-then-refresh strategy:
- On mount — serve from AsyncStorage immediately (no loading flash if cache exists)
- In background — fetch fresh from your backend and update cache + state
- On foreground — re-fetch when app returns to active state (configurable via
refetchOnForeground)
Flags — percent rollouts
Flags support percent rollout rules out of the box. The bucketing uses FNV1a hashing on flagKey:bucketingId which is stable per user and requires no backend calls to evaluate.
// Flag row shape your fetcher should return
{
key: 'new_onboarding',
value: false, // default value
rules: {
type: 'percent',
percent: 20, // show to 20% of users
value: true // value for users in the bucket
}
}