Skip to Content
DocumentationGuidesConfig Environment

Config Environment

Overview

The Remote Environment Configuration system provides a flexible way to manage application configuration in React Native/Expo apps by fetching environment variables from a remote Supabase database at runtime, rather than bundling them in the app binary.

Key Features

  • 🔄 Runtime Configuration: Fetch configs from Supabase at app startup
  • 🛡️ Security: Keep sensitive API keys out of app bundle
  • 📦 Multi-tier Fallback: Remote → Cached → Process.env
  • Smart Caching: Configurable cache duration to minimize requests
  • 🔧 Type Safety: Full TypeScript support with type parsing
  • 🎯 Flexible Options: Configure fallback behavior via environment variables

When to Use This Approach

Good For:

  • Feature flags
  • Public API keys (Maps, Analytics)
  • App configuration values
  • A/B testing parameters
  • Remote feature toggles
  • Environment-specific settings

Not Suitable For:

  • Payment processor secrets (Stripe secret keys)
  • Encryption keys
  • Server-to-server tokens
  • Database passwords
  • OAuth client secrets

Security Note: Any configuration fetched to a mobile app can be intercepted by users with tools like Charles Proxy. For truly sensitive operations, use Supabase Edge Functions as a server-side proxy.

Config Environment Management

This document explains how to use useConfigEnvironment (from hooks/useConfigEnvironment.ts) in the starter kit to manage your app configuration via Supabase, with safe fallbacks.


What useConfigEnvironment Does

  • Pulls app config from Supabase (app_configs table) and normalizes types (STRING, NUMBER, BOOLEAN, JSON, etc.).
  • Stores the result in global state.
  • Provides a robust fallback chain when remote fetch fails:
    • 1. Remote Supabase config – highest priority when available.
    • 2. Cached/persisted environment – optional, medium priority.
    • 3. process.env – optional, lowest priority.
  • Exposes loading, error, and refresh state so you can control UX and re-fetch configs.

Look for the Database section in the video above.


Use it early in your app, for example in your root layout or a top-level provider:

import { useConfigEnvironment, getConfigEnvironmentOptionsFromEnv } from "@/hooks/useConfigEnvironment"; import { useEnvironment } from "@/state"; export function RootConfigProvider() { const { isLoading, error } = useConfigEnvironment( getConfigEnvironmentOptionsFromEnv() ); const env = useEnvironment(); if (isLoading) { // Show a minimal loading UI while config loads return null; } if (error) { // Decide how strict you want to be here // For many apps, you may still continue with fallback env return null; } // At this point env contains merged config (remote + fallbacks) const apiBaseUrl = env.EXPO_PUBLIC_API_URL as string | undefined; return ( // children / navigation / providers null ); }

Key points:

  • You do not get the environment object directly from useConfigEnvironment; it just orchestrates fetching+storing.
  • You read the effective environment via useEnvironment() from @/state.
  • The hook automatically updates:
    • environment (in useApp / useEnvironment)
    • appConfigs (raw config records)
    • lastFetchTime
    • remoteEnvReady flag in useCache

Return Value

useConfigEnvironment(options?: ConfigEnvironmentOptions) returns:

  • isLoading: booleantrue while a fetch is in progress (or on first mount).
  • error: Error | null – last fetch error if any.
  • refresh: () => void – forces a re-fetch ignoring the cache age.

Example refresh usage (e.g. in an admin/debug screen):

const { isLoading, error, refresh } = useConfigEnvironment( getConfigEnvironmentOptionsFromEnv(), useProcessEnvOnly: __DEV__ // for local dev without Supabase ); return ( <Button label={isLoading ? "Refreshing..." : "Refresh remote config"} onPress={refresh} disabled={isLoading} /> );

Config Options (ConfigEnvironmentOptions)

ConfigEnvironmentOptions

interface ConfigEnvironmentOptions { /** * Use cached environment as fallback when remote fetch fails * @default true * Priority: MEDIUM (overrides process.env, but not remote) */ usePersistentFallback?: boolean; /** * Use process.env as fallback when remote fetch fails * @default true * Priority: LOWEST */ useProcessEnvFallback?: boolean; /** * Throw error instead of using fallbacks * @default false * Warning: Will crash app if remote fetch fails */ throwOnError?: boolean; /** * Skip remote fetch and use only process.env * @default false * Use case: Development mode or offline testing */ useProcessEnvOnly?: boolean; /** * Cache duration in milliseconds * @default 300000 (5 minutes) * Prevents unnecessary remote fetches */ cacheMaxAge?: number; }

useConfigEnvironment accepts an options.

Environment Variable Configuration

# All options can be set via environment variables # Use cached fallback (true/false/yes/1) EXPO_PUBLIC_REMOTE_ENV_USE_PERSISTENT_FALLBACK=true # Use process.env fallback (true/false/yes/1) EXPO_PUBLIC_REMOTE_ENV_USE_PROCESS_ENV_FALLBACK=true # Throw on error (true/false/yes/1) EXPO_PUBLIC_REMOTE_ENV_THROW_ON_ERROR=false # Cache max age in milliseconds EXPO_PUBLIC_REMOTE_ENV_CACHE_MAX_AGE=300000

1. usePersistentFallback (default: true)

  • What it does: If remote fetch fails, the hook can fall back to the previously cached environment from Zustand storage.
  • Priority: Medium – overrides process.env, but is overridden by fresh remote data.
  • When to keep enabled:
    • You want resilience when offline or when Supabase is temporarily unavailable.
  • When to disable:
    • You explicitly want to ignore any previously cached values on failure.

2. useProcessEnvFallback (default: true)

  • What it does: Adds process.env variables as the lowest-priority fallback.
  • Priority: Lowest – overridden by both cached env and remote env.
  • Typical usage:
    • Keep this true so that build-time env (EXPO_PUBLIC_...) is always the last line of defense.

3. throwOnError (default: false)

  • What it does: If true, the hook re-throws the fetch error instead of quietly falling back.
  • Effect:
    • With false (default), the hook logs the error, sets error, and uses the fallback chain.
    • With true, you’ll need an error boundary or explicit try/catch at the call site in server-like contexts.
  • Recommendation for starter kit customers:
    • Keep this false unless you have strict guarantees and custom error handling.

4. useProcessEnvOnly (default: false)

  • What it does:
    • Skips all remote/Supabase logic.
    • Ignores cached environment.
    • Immediately sets the environment to process.env and marks remoteEnvReady = true.
  • When to use:
    • Local dev without Supabase.
    • CI / test runs where remote fetch is undesirable.
    • Temporary troubleshooting to rule out remote config issues.

5. cacheMaxAge (default: 5 minutes)

  • What it does:
    • Controls how long the last successful remote fetch is considered “fresh”.
    • If the cached data is younger than cacheMaxAge, the hook reuses it and skips a new fetch.
  • When to reduce:
    • You change remote config very frequently and need faster propagation.
  • When to increase:
    • You want to minimize Supabase reads and are okay with slower config changes.

How Values Are Parsed

Remote data is pulled from the Supabase app_configs table and mapped into AppConfig[]. For each record:

  • config.key – becomes the environment key.
  • config.value – raw string from Supabase.
  • config.type – one of the supported types (AppConfig["type"]).

parseConfigValue then converts value by type:

  • NUMBERNumber(value)
  • BOOLEANvalue.toLowerCase() === "true"
  • JSONJSON.parse(value) (falls back to raw string on parse error)
  • STRING, URL, EMAIL, DATE, default → leaves as string

Implication for customers:

  • Ensure your app_configs.type matches the actual value format.
  • For complex objects or arrays, store valid JSON and use type = "JSON".

Example Supabase rows:

keyvaluetype
EXPO_PUBLIC_API_URLhttps://api.example.comSTRING
FEATURE_FLAG_NEW_UItrueBOOLEAN
RETRY_COUNT3NUMBER
APP_FEATURES{"beta":true,"maxUsers":100}JSON

Reading Config Values in Your App

Once useConfigEnvironment has run and completed, read the current environment via useEnvironment:

import { useEnvironment } from "@/state"; export function SomeScreen() { const env = useEnvironment(); const apiUrl = env.EXPO_PUBLIC_API_URL as string | undefined; const isNewUIEnabled = env.FEATURE_FLAG_NEW_UI as boolean | undefined; // It’s your responsibility to handle undefined safely if (!apiUrl) { // Decide: show error, fallback, or disable a feature } return null; }

Fallback System

Priority Chain

The system uses a three-tier fallback system:

1. Remote Config (Supabase) ← HIGHEST PRIORITY ↓ (if fetch fails) 2. Cached Config (Zustand) ← MEDIUM PRIORITY ↓ (if no cache) 3. Process.env (.env file) ← LOWEST PRIORITY

Fallback Scenarios

Scenario 1: Successful Remote Fetch

Remote Config: { API_KEY: "remote-123" } Cached Config: { API_KEY: "cached-456" } Process.env: { API_KEY: "env-789" } Result: { API_KEY: "remote-123" } ✅

Scenario 2: Remote Fetch Fails (with cache)

Remote Config: ❌ Network error Cached Config: { API_KEY: "cached-456" } Process.env: { API_KEY: "env-789" } Result: { API_KEY: "cached-456" } ⚠️

Scenario 3: Remote Fetch Fails (no cache)

Remote Config: ❌ Network error Cached Config: {} (empty) Process.env: { API_KEY: "env-789" } Result: { API_KEY: "env-789" } ⚠️

Scenario 4: All Sources Fail

Remote Config: ❌ Network error Cached Config: {} (empty) Process.env: {} (empty) Result: {} ❌ (app may not function correctly)

Configuring Fallback Behavior

// Strict mode: Only use remote config useConfigEnvironment({ usePersistentFallback: false, useProcessEnvFallback: false, throwOnError: true, // Crash if remote fetch fails }); // Flexible mode: Use all fallbacks useConfigEnvironment({ usePersistentFallback: true, useProcessEnvFallback: true, throwOnError: false, // Continue with fallbacks }); // Development mode: Use only .env file useConfigEnvironment({ useProcessEnvOnly: true, // Skip remote entirely });

Caching Strategy

Cache Behavior

const cacheMaxAge = 5 * 60 * 1000; // 5 minutes // First fetch: Loads from remote useConfigEnvironment({ cacheMaxAge }); // Within 5 minutes: Uses cached data (no network request) // After 5 minutes: Fetches from remote again

Cache Invalidation

// Manual refresh (bypasses cache) const { refresh } = useConfigEnvironment(); refresh(); // Forces immediate remote fetch // Automatic refresh on app resume useEffect(() => { const subscription = AppState.addEventListener('change', (state) => { if (state === 'active') { refresh(); // Refresh when app comes to foreground } }); return () => subscription.remove(); }, [refresh]);

Security Best Practices

1. Separate Public and Private Configs

-- Public configs viewable by everyone CREATE POLICY "public_configs_readable" ON app_configs FOR SELECT USING (is_public = true); -- Authenticated users can view their org configs CREATE POLICY "org_configs_readable" ON app_configs FOR SELECT USING ( auth.uid() IS NOT NULL AND ( is_public = true OR organization_id = ( SELECT organization_id FROM user_organizations WHERE user_id = auth.uid() ) ) ); -- Enable RLS ALTER TABLE app_configs ENABLE ROW LEVEL SECURITY;

2. Implement Rate Limiting

-- Add rate limiting to RLS policy CREATE POLICY "rate_limited_access" ON app_configs FOR SELECT USING ( is_public = true AND ( SELECT COUNT(*) FROM config_access_logs WHERE user_id = auth.uid() AND created_at > NOW() - INTERVAL '1 hour' ) < 100 );