Skip to content
Skip to content
Goodspeed

BUILT INTO EVERY GOODSPEED APP

Minimum Version Gate

A MinVersionGate component wraps the entire app tree and shows a blocking update prompt when the client runtime version is below the configured floor, preventing users on incompatible old builds from hitting backend breaking changes.

  • Tier: Core
  • Status: Config-toggled
  • Config: app.minRuntimeVersion

WHY IT MATTERS

Backend APIs evolve. Migrations rename columns, Edge Functions change their response shape, and authentication flows gain new required fields. When a production app rolls out a breaking change to its backend, every client still running an old binary can start hitting 400s, empty screens, or silent data corruption. The standard workaround is to support both old and new clients indefinitely, but that doubles the test surface and lets technical debt compound across multiple release cycles. The gas-template takes a different position: ship a hard floor. MinVersionGate wraps the entire React Native tree at the root layout level. On every cold launch it reads the current runtimeVersion from expo-updates and compares it against the minRuntimeVersion field in gasConfig.app. If the comparison fails, the app tree is covered by a non-dismissable modal that links the user directly to the App Store or Play Store listing. The app itself is still rendered in the background so the OS does not report an unresponsive app, but the user cannot interact with any feature until they update.

The server-side path runs through a Supabase Edge Function called check-min-version. The function accepts a platform and clientVersion body, queries the operator-controlled min_versions config table, and returns a mustUpdate boolean alongside an optional server-supplied message. This lets operators push a new floor without shipping a code change: update the database row, and every client that restarts within the next few seconds will see the gate if it is below the new floor. The client implementation in lib/min-version.ts is fail-open by design. If the Edge Function is unreachable, times out, or returns an unexpected payload, checkMinVersion() returns mustUpdate: false, so a transient infrastructure outage never locks users out of their own app. Sentry breadcrumbs are emitted on every failure path so operators can distinguish a real version-floor event from a network-connectivity false alarm.

HOW IT IS WIRED

Real code from the GAS template

The code below is drawn from components/MinVersionGate.tsx in the gas-template repository. This is the code your generated app gets, not pseudocode, not a description of intent.

// components/MinVersionGate.tsx
import React from 'react';
import { Modal, View, Text, Pressable, Linking } from 'react-native';
import * as Updates from 'expo-updates';
import { gasConfig } from '../gas.config';
import { compareVersions } from '../lib/semver';
import { useThemeColors } from '../context/ThemeContext';

export function MinVersionGate({ children }: { children: React.ReactNode }) {
  const { colors } = useThemeColors();
  const runtimeVersion = Updates.runtimeVersion;

  // In Expo Go / dev builds runtimeVersion is undefined -- let the app through.
  if (!runtimeVersion) return <>{children}</>;

  const minVersion = gasConfig.app.minRuntimeVersion;
  let mustUpdate = false;
  try {
    mustUpdate = compareVersions(runtimeVersion, minVersion) === -1;
  } catch {
    mustUpdate = false;
  }

  if (!mustUpdate) return <>{children}</>;

  return (
    <>
      {children}
      <Modal visible transparent animationType="fade" statusBarTranslucent
        onRequestClose={() => {}}
        accessibilityViewIsModal>
        <View className="flex-1 items-center justify-center px-8"
          style={{ backgroundColor: 'rgba(0,0,0,0.6)' }} accessibilityRole="alert">
          <View className="w-full rounded-2xl p-6" style={{ backgroundColor: colors.surface }}>
            <Text className="text-xl font-bold mb-3 text-center" style={{ color: colors.text }}>
              Update Required
            </Text>
            <Pressable onPress={() => Linking.openURL(gasConfig.app.appStoreUrl)}
              className="rounded-xl py-3 px-6 items-center"
              style={{ backgroundColor: colors.primary }}
              accessibilityRole="button" accessibilityLabel="Open App Store to update">
              <Text className="text-base font-semibold" style={{ color: '#ffffff' }}>
                Open App Store
              </Text>
            </Pressable>
          </View>
        </View>
      </Modal>
    </>
  );
}

Source: goodspeed-apps/gas-template components/MinVersionGate.tsx

HONEST LIMITS

When Minimum Version Gate is the wrong choice

Consumer apps with large, slow-updating user bases should be cautious about raising the min version floor aggressively. When you bump minRuntimeVersion, every user below that version is instantly blocked on next launch until they install the update. App Store and Play Store update rollouts are not instant: Apple and Google control the update delivery pipeline, and in practice a significant share of active users may be on the previous version for days after the new binary goes live. If your app has a meaningful daily active user count, blocking even 10% of them during a transition window is a real support burden and a real retention cost. The gate is best reserved for genuinely breaking backend changes, not every release. The gate has no partial-block mode. It is all or nothing: every user below the floor sees the update prompt and cannot proceed. If you need softer upgrade nudges, such as an in-app banner that encourages but does not force an update, implement that at a layer above MinVersionGate using a separate notification system. Additionally, the client-side check reads runtimeVersion from expo-updates, which is undefined in Expo Go and development builds. The gate intentionally passes all dev-mode sessions through without blocking. If you are testing the blocking behavior locally, you must run a production or preview EAS build with a real runtimeVersion value assigned in eas.json.

Tier: Core · Config-toggled

  1. Evaluate your use case

    Check whether minimum version gate aligns with your target audience, platform constraints, and regulatory environment before enabling it.

  2. Audit the config

    The `app.minRuntimeVersion` flag controls this feature. Set it to false in gas.config.ts to disable the feature entirely with no residual code paths.

  3. Seek alternatives

    If the built-in implementation does not fit, the generated codebase is standard React Native + Expo code. Any library in the Expo ecosystem can replace the default.

APPS USING THIS FEATURE

Every generated Goodspeed app includes minimum version gate. Browse the ideas catalog to see apps across all categories that ship with this feature wired in.

CAPABILITIES

Minimum Version Gate capability breakdown

Concrete dimensions of what the built-in minimum version gate implementation covers. These reflect the actual template code, not a marketing summary.

ItemDescriptionStrength
Version sourceThe client version compared against the floor is runtimeVersion from expo-updates, which matches the value set in eas.json.expo-updates runtimeVersion
Floor configurationminRuntimeVersion is set in gasConfig.app and can be updated server-side via the check-min-version Edge Function without a code deploy.gasConfig + Edge Function
Failure modeIf the Edge Function is unreachable or returns an error, mustUpdate defaults to false. Users are never locked out by a network outage.Fail-open
Modal dismissabilityThe update prompt is a non-dismissable Modal. onRequestClose is a no-op. The user cannot access any screen until they update.Non-dismissable
Dev build behaviorruntimeVersion is undefined in Expo Go and development builds. The gate passes all dev-mode sessions through without evaluating the floor.Dev-mode bypass

COMMON QUESTIONS

Does the gate work the same way on iOS and Android?

Yes. MinVersionGate reads runtimeVersion from expo-updates, which is populated identically on both platforms by the EAS build pipeline. The appStoreUrl in gasConfig.app should be set to the App Store URL for iOS submissions and the Play Store URL for Android submissions. If you target both stores from a single binary, you can detect Platform.OS inside the gate and conditionally open the platform-appropriate store URL. The blocking Modal implementation uses React Native primitives that behave consistently on iOS, Android, and the web export target. On web, runtimeVersion is typically undefined, so the gate passes web sessions through without blocking, which is correct behavior for web exports that update on page refresh.

How do I test the update prompt without shipping a real binary?

Run an EAS preview build with a runtimeVersion that is intentionally lower than the minRuntimeVersion value set in gasConfig.app. Because Expo Go and development client sessions have runtimeVersion undefined, the gate skips them entirely. You must use a production or preview EAS build to see the blocking modal. Alternatively, you can temporarily hardcode a fake runtimeVersion value in a local branch for manual testing, then revert before merging. The gate logic lives entirely in components/MinVersionGate.tsx and lib/min-version.ts, so it is easy to unit test by mocking Updates.runtimeVersion and gasConfig.app.minRuntimeVersion. The existing test suite in __tests__/components/MinVersionGate.test.tsx covers the must-update and pass-through branches, the undefined runtimeVersion bypass, and invalid-semver fail-open behavior.

Can I supply a custom message in the update prompt?

Yes, via the server-side path. When the check-min-version Edge Function returns a mustUpdate response, it can include a message field with operator-supplied copy. The MinVersionGate component reads this value and, if present, replaces the default body text with the server-supplied string. This lets operators explain what changed and why the update is required without shipping a new binary. For example, if the update introduces a data migration, the message might read 'We have updated how your data is stored. Please update to keep everything in sync.' The client-side-only path, the compareVersions check against gasConfig.app.minRuntimeVersion, always uses the default copy because there is no server round-trip to supply a custom message. To use server-supplied messages, ensure the check-min-version Edge Function is enabled and gasConfig.app.minRuntimeVersion is kept in sync with the server floor.

What if an operator raises the floor higher than the current live binary?

All users whose installed version is below the new floor will see the blocking modal on their next cold launch. Users who are mid-session are not interrupted because the MinVersionGate check only runs at the root layout mount, which happens on cold start. An app that is backgrounded and then foregrounded does not re-run the gate check unless the OS kills and restarts the process. Before raising the floor, verify that the new binary is already live in the stores and that sufficient time has passed for the update to propagate to the majority of active users. A common pattern is to wait until store analytics show the new binary at 80% or more of your active installs before raising the floor on the old one. The fail-open design ensures that if the Edge Function is temporarily unreachable during a floor change, no user is incorrectly blocked. If you need to roll back a floor bump, lower the minRuntimeVersion in the database row and the next cold launch from any version will pass the gate again.

GET IT BUILT INTO YOUR APP

Score your idea and get minimum version gate wired in from day one