about summary refs log tree commit diff
diff options
context:
space:
mode:
authorEric Bailey <git@esb.lol>2025-07-30 12:33:40 -0500
committerGitHub <noreply@github.com>2025-07-30 12:33:40 -0500
commitd4b23d3ab4e8448321fecc7bd46b6531ada80348 (patch)
tree4e690a54a41f033b0df9072bf0dff9e53470f626
parentdb7bdae51a1a06e67856b887e4e63a183fa5f479 (diff)
downloadvoidsky-d4b23d3ab4e8448321fecc7bd46b6531ada80348.tar.zst
[APP-1173] Clean up env (#8701)
* Clean up env files

* Use new env in Sentry setup file

* Use new env in Bitdrift setup file

* Use new env in chat proxy header

* Prefix Bitdrift key with EXPO_PUBLIC

* Deprecate SENTRY_RELEASE since we use package.json now

* Use existing EXPO_PUBLIC_BUNDLE_IDENTIFIER short commit has as Sentry dist value

* Fix missing bundle identifier for Render deploys

* Deprecate SENTRY_DIST in favor of EXPO_PUBLIC_BUNDLE_IDENTIFIER

* Prefix SENTRY_DSN with EXPO_PUBLIC to match others

* Remove debugging field

* Replace NODE_ENV in places where its safe

* Self review

* Properly patch Sentry package

* Echo variables to .env in Dockerfile instead of passing to shell script

* Make sure EXPO_PUBLIC_ENV is set for web container builds

* Update IS_TESTFLIGHT to include testflight-android

* Slice bundle hash to match other platforms, needed for render.com deployments

* [APP-1331] Migrate `app-info` to new env (#8703)

* Move env files into directory with platform specific files

* Migrate usages of app-info to new env

* Fix bad import

* Update BUNDLE_DATE format comment

* Trim RENDER_GIT_COMMIT to first 7 to match --short sha

* Clarify build process env vars and ensure they are explicitly passed in

* Revert Sentry patch as a result of prev commit

* Update webpack Sentry dist value based on prev commits

* Add PACKAGE_VERSION and replace in statsig to fix conflict

* Fix render substitution syntax

* Remove invalid syntax

* Remove unnecessary testflight check

* Just use long commit hash

* Slice full hash for display in app

* Fix missing space in ios workflow

* Pass in sentry CLI env vars, align matching values

* Align on RELEASE_VERSION

* Add new env setup to missed OTA spot

* Update webpack to use same SENTRY_RELEASE var

* Just fallback to package version for Render deploys

* Remove TF check for BUNDLE_DATE

* Set EXPO_PUBLIC_ENV for bundle update

* Consistent naming "Env"

* Add comment

* Use RELEASE_VERSION instead of package.json

* Update PR comment CI
-rw-r--r--.env.example34
-rw-r--r--.github/workflows/build-and-push-bskyweb-aws.yaml17
-rw-r--r--.github/workflows/build-submit-android.yml36
-rw-r--r--.github/workflows/build-submit-ios.yml25
-rw-r--r--.github/workflows/bundle-deploy-eas-update.yml67
-rw-r--r--.github/workflows/pull-request-comment.yml24
-rw-r--r--Dockerfile29
-rw-r--r--docs/build.md4
-rw-r--r--src/components/PostControls/DiscoverDebug.tsx2
-rw-r--r--src/components/PostControls/PostMenu/PostMenuItems.tsx2
-rw-r--r--src/env.ts6
-rw-r--r--src/env/common.ts79
-rw-r--r--src/env/index.ts19
-rw-r--r--src/env/index.web.ts15
-rw-r--r--src/lib/app-info.ts18
-rw-r--r--src/lib/app-info.web.ts18
-rw-r--r--src/lib/hooks/useOTAUpdates.ts2
-rw-r--r--src/lib/statsig/statsig.tsx20
-rw-r--r--src/logger/bitdrift/setup/index.ts3
-rw-r--r--src/logger/index.ts3
-rw-r--r--src/logger/sentry/setup/index.ts29
-rw-r--r--src/screens/Settings/AboutSettings.tsx10
-rw-r--r--src/screens/Settings/AppIconSettings/index.tsx8
-rw-r--r--src/screens/Settings/AppearanceSettings.tsx2
-rw-r--r--src/screens/Settings/Settings.tsx2
-rw-r--r--src/state/queries/messages/const.ts4
-rw-r--r--src/state/session/logging.ts10
-rw-r--r--webpack.config.js2
28 files changed, 313 insertions, 177 deletions
diff --git a/.env.example b/.env.example
index a4d21ac33..e18cda4e5 100644
--- a/.env.example
+++ b/.env.example
@@ -1,8 +1,32 @@
-# Copy this to `.env` and `.env.test` files
+# The env the app is running in e.g. development, testflight, production
+EXPO_PUBLIC_ENV=development
 
-BITDRIFT_API_KEY=
-SENTRY_AUTH_TOKEN=
-EXPO_PUBLIC_LOG_LEVEL=debug
-EXPO_PUBLIC_LOG_DEBUG=
+# This is the semver release version of the app, pulled from package.json
+EXPO_PUBLIC_RELEASE_VERSION=
+
+# This is the commit hash that the current bundle was made from.
 EXPO_PUBLIC_BUNDLE_IDENTIFIER=
+
+# Should be formatted YYMMDDHH so that it increases for each build.
 EXPO_PUBLIC_BUNDLE_DATE=0
+
+# The log level for the app's logger transports
+EXPO_PUBLIC_LOG_LEVEL=debug
+
+# Enable debug logs for specific logger instances
+EXPO_PUBLIC_LOG_DEBUG=session
+
+# Chat service DID
+EXPO_PUBLIC_CHAT_PROXY_DID=
+
+#
+#
+# Bluesky specific values
+#
+#
+
+# Sentry DSN for telemetry
+EXPO_PUBLIC_SENTRY_DSN=
+
+# Bitdrift API key. If undefined, Bitdrift will be disabled.
+EXPO_PUBLIC_BITDRIFT_API_KEY=
diff --git a/.github/workflows/build-and-push-bskyweb-aws.yaml b/.github/workflows/build-and-push-bskyweb-aws.yaml
index 399e8669f..6d573b0f7 100644
--- a/.github/workflows/build-and-push-bskyweb-aws.yaml
+++ b/.github/workflows/build-and-push-bskyweb-aws.yaml
@@ -43,12 +43,11 @@ jobs:
           tags: |
             type=sha,enable=true,priority=100,prefix=,suffix=,format=long
 
-      - name: Set outputs
-        id: vars
+      - name: Env
+        id: env
         run: |
-          echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
-          echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-          echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
 
       - name: Build and push Docker image
         id: build-and-push
@@ -62,8 +61,8 @@ jobs:
           cache-from: type=gha
           cache-to: type=gha,mode=max
           build-args: |
-            EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.vars.outputs.sha_short }}
-            SENTRY_DIST=${{ steps.vars.outputs.SENTRY_DIST }}
-            SENTRY_RELEASE=${{ steps.vars.outputs.SENTRY_RELEASE }}
+            EXPO_PUBLIC_ENV=production
+            EXPO_PUBLIC_RELEASE_VERSION=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
+            EXPO_PUBLIC_BUNDLE_IDENTIFIER=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
+            EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}
             SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
-            SENTRY_DSN=${{ secrets.SENTRY_DSN }}
diff --git a/.github/workflows/build-submit-android.yml b/.github/workflows/build-submit-android.yml
index e862a701f..f1e75f87e 100644
--- a/.github/workflows/build-submit-android.yml
+++ b/.github/workflows/build-submit-android.yml
@@ -62,23 +62,30 @@ jobs:
       - name: Check for i18n compilation errors
         run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
 
-      - name: ✏️ Write environment variables
+      # EXPO_PUBLIC_ENV is handled in eas.json
+      - name: Env
+        id: env
         run: |
           export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
           echo "${{ secrets.ENV_TOKEN }}" > .env
-          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
           echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
-          echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
+          echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
+          echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
           echo "$json" > google-services.json
 
-      - name: Setup Sentry vars for build-time injection
-        id: sentry
-        run: |
-          echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-          echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
-
       - name: 🏗️ EAS Build
-        run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p android --profile ${{ inputs.profile || 'testflight-android' }} --local --output build.aab --non-interactive
+        run: >
+          SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
+          SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
+          SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
+          yarn use-build-number-with-bump
+          eas build -p android
+          --profile ${{ inputs.profile || 'testflight-android' }}
+          --local --output build.aab --non-interactive
 
       - name: ✍️ Rename Testflight bundle
         if: ${{ inputs.profile != 'production' }}
@@ -140,7 +147,14 @@ jobs:
 
       - name: 🏗️ Build Production APK
         if: ${{ inputs.profile == 'production' }}
-        run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p android --profile production-apk --local --output build.apk --non-interactive
+        run: >
+          SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
+          SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
+          SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
+          yarn use-build-number-with-bump
+          eas build -p android
+          --profile production-apk
+          --local --output build.apk --non-interactive
 
       - name: 🚀 Upload Production APK Artifact
         id: upload-artifact-production-apk
diff --git a/.github/workflows/build-submit-ios.yml b/.github/workflows/build-submit-ios.yml
index a197695db..f3759d0dd 100644
--- a/.github/workflows/build-submit-ios.yml
+++ b/.github/workflows/build-submit-ios.yml
@@ -75,22 +75,29 @@ jobs:
       - name: Check for i18n compilation errors
         run: if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compilation errors!\n\n"; fi
 
+      # EXPO_PUBLIC_ENV is handled in eas.json
       - name: ✏️ Write environment variables
+        id: env
         run: |
           echo "${{ secrets.ENV_TOKEN }}" > .env
-          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
           echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
-          echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
+          echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
+          echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
           echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
 
-      - name: Setup Sentry vars for build-time injection
-        id: sentry
-        run: |
-          echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-          echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
-
       - name: 🏗️ EAS Build
-        run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} yarn use-build-number-with-bump eas build -p ios --profile ${{ inputs.profile || 'testflight' }} --local --output build.ipa --non-interactive
+        run: >
+          SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
+          SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
+          SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
+          yarn use-build-number-with-bump
+          eas build -p ios
+          --profile ${{ inputs.profile || 'testflight' }}
+          --local --output build.ipa --non-interactive
 
       - name: 🚀 Deploy
         run: eas submit -p ios --non-interactive --path build.ipa
diff --git a/.github/workflows/bundle-deploy-eas-update.yml b/.github/workflows/bundle-deploy-eas-update.yml
index 475bc087f..1bd0b71c4 100644
--- a/.github/workflows/bundle-deploy-eas-update.yml
+++ b/.github/workflows/bundle-deploy-eas-update.yml
@@ -101,25 +101,30 @@ jobs:
         if: ${{ !steps.fingerprint.outputs.includes-changes }}
         uses: dcarbone/install-jq-action@v2
 
-      - name: ✏️ Write environment variables
+      # eas.json not used here, set EXPO_PUBLIC_ENV
+      - name: Env
+        id: env
         if: ${{ !steps.fingerprint.outputs.includes-changes }}
         run: |
           export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
           echo "${{ secrets.ENV_TOKEN }}" > .env
-          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
+          echo "EXPO_PUBLIC_ENV=${{ inputs.channel || 'testflight' }}" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
           echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
-          echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
+          echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
+          echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
           echo "$json" > google-services.json
 
-      - name: Setup Sentry vars for build-time injection
-        id: sentry
-        run: |
-          echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-          echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
-
       - name: 🏗️ Create Bundle
         if: ${{ !steps.fingerprint.outputs.includes-changes }}
-        run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="${{ inputs.channel || 'testflight' }}" yarn export
+        run: >
+          SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
+          SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
+          SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
+          yarn export
 
       - name: 📦 Package Bundle and 🚀 Deploy
         if: ${{ !steps.fingerprint.outputs.includes-changes }}
@@ -205,16 +210,29 @@ jobs:
       - name: 🔤 Compile translations
         run: yarn intl:build
 
-      - name: ✏️ Write environment variables
+      # EXPO_PUBLIC_ENV is handled in eas.json
+      - name: Env
+        id: env
         run: |
           echo "${{ secrets.ENV_TOKEN }}" > .env
-          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
           echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
-          echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
+          echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
+          echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
           echo "${{ secrets.GOOGLE_SERVICES_TOKEN }}" > google-services.json
 
       - name: 🏗️ EAS Build
-        run: yarn use-build-number-with-bump eas build -p ios --profile testflight --local --output build.ipa --non-interactive
+        run: >
+          SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
+          SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
+          SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
+          yarn use-build-number-with-bump
+          eas build -p ios
+          --profile testflight
+          --local --output build.ipa --non-interactive
 
       - name: 🚀 Deploy
         run: eas submit -p ios --non-interactive --path build.ipa
@@ -282,17 +300,30 @@ jobs:
       - name: 🔤 Compile translations
         run: yarn intl:build
 
-      - name: ✏️ Write environment variables
+      # EXPO_PUBLIC_ENV is handled in eas.json
+      - name: Env
+        id: env
         run: |
           export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
           echo "${{ secrets.ENV_TOKEN }}" > .env
-          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
           echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
-          echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
+          echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
+          echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
           echo "$json" > google-services.json
 
       - name: 🏗️ EAS Build
-        run: yarn use-build-number-with-bump eas build -p android --profile testflight-android --local --output build.apk --non-interactive
+        run: >
+          SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
+          SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
+          SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
+          yarn use-build-number-with-bump
+          eas build -p android
+          --profile testflight-android
+          --local --output build.apk --non-interactive
 
       - name: ⏰ Get a timestamp
         id: timestamp
diff --git a/.github/workflows/pull-request-comment.yml b/.github/workflows/pull-request-comment.yml
index e40eff6c7..d04301002 100644
--- a/.github/workflows/pull-request-comment.yml
+++ b/.github/workflows/pull-request-comment.yml
@@ -152,23 +152,27 @@ jobs:
       - name: 🪛 Setup jq
         uses: dcarbone/install-jq-action@v2
 
-      - name: ✏️ Write environment variables
+      - name: Env
+        id: env
         run: |
           export json='${{ secrets.GOOGLE_SERVICES_TOKEN }}'
           echo "${{ secrets.ENV_TOKEN }}" > .env
-          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse --short HEAD)" >> .env
+          echo "EXPO_PUBLIC_ENV=testflight" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> .env
+          echo "EXPO_PUBLIC_RELEASE_VERSION=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> .env
+          echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
           echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env
-          echo "BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
+          echo "EXPO_PUBLIC_SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
+          echo "EXPO_PUBLIC_BITDRIFT_API_KEY=${{ secrets.BITDRIFT_API_KEY }}" >> .env
           echo "$json" > google-services.json
 
-      - name: Setup Sentry vars for build-time injection
-        id: sentry
-        run: |
-          echo "SENTRY_DIST=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT
-          echo "SENTRY_RELEASE=$(jq -r '.version' package.json)" >> $GITHUB_OUTPUT
-
       - name: 🏗️ Create Bundle
-        run: SENTRY_DIST=${{ steps.sentry.outputs.SENTRY_DIST }} SENTRY_RELEASE=${{ steps.sentry.outputs.SENTRY_RELEASE }} SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} SENTRY_DSN=${{ secrets.SENTRY_DSN }} EXPO_PUBLIC_ENV="testflight" yarn export
+        run: >
+          SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}
+          SENTRY_RELEASE=${{ steps.env.outputs.EXPO_PUBLIC_RELEASE_VERSION }}
+          SENTRY_DIST=${{ steps.env.outputs.EXPO_PUBLIC_BUNDLE_IDENTIFIER }}
+          yarn export
 
       - name: 📦 Package Bundle and 🚀 Deploy
         run: yarn use-build-number bash scripts/bundleUpdate.sh
diff --git a/Dockerfile b/Dockerfile
index 50dc28b4b..43d7f4eb4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -19,28 +19,28 @@ ENV GOARCH="amd64"
 ENV CGO_ENABLED=1
 ENV GOEXPERIMENT="loopvar"
 
+# The latest git hash of the preview branch on render.com
+# https://render.com/docs/docker-secrets#environment-variables-in-docker-builds
+ARG RENDER_GIT_COMMIT
+
 #
 # Expo
 #
+ARG EXPO_PUBLIC_ENV
+ENV EXPO_PUBLIC_ENV=${EXPO_PUBLIC_ENV:-development}
+ARG EXPO_PUBLIC_RELEASE_VERSION
+ENV EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION
 ARG EXPO_PUBLIC_BUNDLE_IDENTIFIER
-ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-dev}
-
-# The latest git hash of the preview branch on render.com
-ARG RENDER_GIT_COMMIT
+# If not set by GitHub workflows, we're probably in Render
+ENV EXPO_PUBLIC_BUNDLE_IDENTIFIER=${EXPO_PUBLIC_BUNDLE_IDENTIFIER:-$RENDER_GIT_COMMIT}
 
 #
 # Sentry
 #
 ARG SENTRY_AUTH_TOKEN
 ENV SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN:-unknown}
-# Will fall back to package.json#version, but this is handled elsewhere
-ARG SENTRY_RELEASE
-ENV SENTRY_RELEASE=$SENTRY_RELEASE
-ARG SENTRY_DIST
-# Default to RENDER_GIT_COMMIT if not set by GitHub workflows
-ENV SENTRY_DIST=${SENTRY_DIST:-$RENDER_GIT_COMMIT}
-ARG SENTRY_DSN
-ENV SENTRY_DSN=$SENTRY_DSN
+ARG EXPO_PUBLIC_SENTRY_DSN
+ENV EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN
 
 #
 # Copy everything into the container
@@ -60,13 +60,16 @@ RUN \. "$NVM_DIR/nvm.sh" && \
   nvm install $NODE_VERSION && \
   nvm use $NODE_VERSION && \
   echo "Using bundle identifier: $EXPO_PUBLIC_BUNDLE_IDENTIFIER" && \
+  echo "EXPO_PUBLIC_ENV=$EXPO_PUBLIC_ENV" >> .env && \
+  echo "EXPO_PUBLIC_RELEASE_VERSION=$EXPO_PUBLIC_RELEASE_VERSION" >> .env && \
   echo "EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER" >> .env && \
   echo "EXPO_PUBLIC_BUNDLE_DATE=$(date -u +"%y%m%d%H")" >> .env && \
+  echo "EXPO_PUBLIC_SENTRY_DSN=$EXPO_PUBLIC_SENTRY_DSN" >> .env && \
   npm install --global yarn && \
   yarn && \
   yarn intl:build 2>&1 | tee i18n.log && \
   if grep -q "invalid syntax" "i18n.log"; then echo "\n\nFound compilation errors!\n\n" && exit 1; else echo "\n\nNo compile errors!\n\n"; fi && \
-  EXPO_PUBLIC_BUNDLE_IDENTIFIER=$EXPO_PUBLIC_BUNDLE_IDENTIFIER EXPO_PUBLIC_BUNDLE_DATE=$() SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$SENTRY_RELEASE SENTRY_DIST=$SENTRY_DIST SENTRY_DSN=$SENTRY_DSN yarn build-web
+  SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN SENTRY_RELEASE=$EXPO_PUBLIC_RELEASE_VERSION SENTRY_DIST=$EXPO_PUBLIC_BUNDLE_IDENTIFIER yarn build-web
 
 # DEBUG
 RUN find ./bskyweb/static && find ./web-build/static
diff --git a/docs/build.md b/docs/build.md
index 7817dd095..87b653bdb 100644
--- a/docs/build.md
+++ b/docs/build.md
@@ -89,9 +89,9 @@ If you change `SENTRY_AUTH_TOKEN`, you need to do `yarn prebuild` before running
 
 ### Adding bitdrift
 
-Adding bitdirft is NOT required. You can keep `BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
+Adding bitdrift is NOT required. You can keep `EXPO_PUBLIC_BITDRIFT_API_KEY=` in `.env` which will avoid initializing bitdrift during startup.
 
-However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
+However, if you're a part of the Bluesky team and want to enable bitdrift, fill in `EXPO_PUBLIC_BITDRIFT_API_KEY` in your `.env` to enable bitdrift.
 
 ### Adding and Updating Locales
 
diff --git a/src/components/PostControls/DiscoverDebug.tsx b/src/components/PostControls/DiscoverDebug.tsx
index 796981f0c..403b50cca 100644
--- a/src/components/PostControls/DiscoverDebug.tsx
+++ b/src/components/PostControls/DiscoverDebug.tsx
@@ -2,13 +2,13 @@ import {Pressable} from 'react-native'
 import * as Clipboard from 'expo-clipboard'
 import {t} from '@lingui/macro'
 
-import {IS_INTERNAL} from '#/lib/app-info'
 import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
 import {useGate} from '#/lib/statsig/statsig'
 import {useSession} from '#/state/session'
 import * as Toast from '#/view/com/util/Toast'
 import {atoms as a, useBreakpoints, useTheme} from '#/alf'
 import {Text} from '#/components/Typography'
+import {IS_INTERNAL} from '#/env'
 
 export function DiscoverDebug({
   feedContext,
diff --git a/src/components/PostControls/PostMenu/PostMenuItems.tsx b/src/components/PostControls/PostMenu/PostMenuItems.tsx
index f0ef9ed05..ecc3d0174 100644
--- a/src/components/PostControls/PostMenu/PostMenuItems.tsx
+++ b/src/components/PostControls/PostMenu/PostMenuItems.tsx
@@ -17,7 +17,6 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useNavigation} from '@react-navigation/native'
 
-import {IS_INTERNAL} from '#/lib/app-info'
 import {DISCOVER_DEBUG_DIDS} from '#/lib/constants'
 import {useOpenLink} from '#/lib/hooks/useOpenLink'
 import {getCurrentRoute} from '#/lib/routes/helpers'
@@ -83,6 +82,7 @@ import {
   useReportDialogControl,
 } from '#/components/moderation/ReportDialog'
 import * as Prompt from '#/components/Prompt'
+import {IS_INTERNAL} from '#/env'
 import * as bsky from '#/types/bsky'
 
 let PostMenuItems = ({
diff --git a/src/env.ts b/src/env.ts
deleted file mode 100644
index 32ce70670..000000000
--- a/src/env.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-export const LOG_DEBUG = process.env.EXPO_PUBLIC_LOG_DEBUG || ''
-export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as
-  | 'debug'
-  | 'info'
-  | 'warn'
-  | 'error'
diff --git a/src/env/common.ts b/src/env/common.ts
new file mode 100644
index 000000000..e68e9fab8
--- /dev/null
+++ b/src/env/common.ts
@@ -0,0 +1,79 @@
+import {type Did} from '@atproto/api'
+
+import packageJson from '#/../package.json'
+
+/**
+ * The semver version of the app, as defined in `package.json.`
+ *
+ * N.B. The fallback is needed for Render.com deployments
+ */
+export const RELEASE_VERSION: string =
+  process.env.EXPO_PUBLIC_RELEASE_VERSION || packageJson.version
+
+/**
+ * The env the app is running in e.g. development, testflight, production
+ */
+export const ENV: string = process.env.EXPO_PUBLIC_ENV
+
+/**
+ * Indicates whether the app is running in TestFlight
+ */
+export const IS_TESTFLIGHT = ENV === 'testflight'
+
+/**
+ * Indicates whether the app is __DEV__
+ */
+export const IS_DEV = __DEV__
+
+/**
+ * Indicates whether the app is __DEV__ or TestFlight
+ */
+export const IS_INTERNAL = IS_DEV || IS_TESTFLIGHT
+
+/**
+ * The commit hash that the current bundle was made from. The user can
+ * see the commit hash in the app's settings along with the other version info.
+ * Useful for debugging/reporting.
+ */
+export const BUNDLE_IDENTIFIER: string =
+  process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER || 'dev'
+
+/**
+ * This will always be in the format of YYMMDDHH, so that it always increases
+ * for each build. This should only be used for StatSig reporting and shouldn't
+ * be used to identify a specific bundle.
+ */
+export const BUNDLE_DATE: number = !process.env.EXPO_PUBLIC_BUNDLE_DATE
+  ? 0
+  : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
+
+/**
+ * The log level for the app.
+ */
+export const LOG_LEVEL = (process.env.EXPO_PUBLIC_LOG_LEVEL || 'info') as
+  | 'debug'
+  | 'info'
+  | 'warn'
+  | 'error'
+
+/**
+ * Enable debug logs for specific logger instances
+ */
+export const LOG_DEBUG: string = process.env.EXPO_PUBLIC_LOG_DEBUG || ''
+
+/**
+ * The DID of the chat service to proxy to
+ */
+export const CHAT_PROXY_DID: Did =
+  process.env.EXPO_PUBLIC_CHAT_PROXY_DID || 'did:web:api.bsky.chat'
+
+/**
+ * Sentry DSN for telemetry
+ */
+export const SENTRY_DSN: string | undefined = process.env.EXPO_PUBLIC_SENTRY_DSN
+
+/**
+ * Bitdrift API key. If undefined, Bitdrift should be disabled.
+ */
+export const BITDRIFT_API_KEY: string | undefined =
+  process.env.EXPO_PUBLIC_BITDRIFT_API_KEY
diff --git a/src/env/index.ts b/src/env/index.ts
new file mode 100644
index 000000000..8558c55b5
--- /dev/null
+++ b/src/env/index.ts
@@ -0,0 +1,19 @@
+import {nativeBuildVersion} from 'expo-application'
+
+import {BUNDLE_IDENTIFIER, IS_TESTFLIGHT, RELEASE_VERSION} from '#/env/common'
+
+export * from '#/env/common'
+
+/**
+ * The semver version of the app, specified in our `package.json`.file. On
+ * iOs/Android, the native build version is appended to the semver version, so
+ * that it can be used to identify a specific build.
+ */
+export const APP_VERSION = `${RELEASE_VERSION}.${nativeBuildVersion}`
+
+/**
+ * The short commit hash and environment of the current bundle.
+ */
+export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${
+  __DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod'
+})`
diff --git a/src/env/index.web.ts b/src/env/index.web.ts
new file mode 100644
index 000000000..66087749b
--- /dev/null
+++ b/src/env/index.web.ts
@@ -0,0 +1,15 @@
+import {BUNDLE_IDENTIFIER, RELEASE_VERSION} from '#/env/common'
+
+export * from '#/env/common'
+
+/**
+ * The semver version of the app, specified in our `package.json`.file. On
+ * iOs/Android, the native build version is appended to the semver version, so
+ * that it can be used to identify a specific build.
+ */
+export const APP_VERSION = RELEASE_VERSION
+
+/**
+ * The short commit hash and environment of the current bundle.
+ */
+export const APP_METADATA = `${BUNDLE_IDENTIFIER.slice(0, 7)} (${__DEV__ ? 'dev' : 'prod'})`
diff --git a/src/lib/app-info.ts b/src/lib/app-info.ts
deleted file mode 100644
index 0749087ea..000000000
--- a/src/lib/app-info.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import {nativeApplicationVersion, nativeBuildVersion} from 'expo-application'
-
-export const IS_TESTFLIGHT = process.env.EXPO_PUBLIC_ENV === 'testflight'
-export const IS_INTERNAL = __DEV__ || IS_TESTFLIGHT
-
-// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
-// along with the other version info. Useful for debugging/reporting.
-export const BUNDLE_IDENTIFIER = process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? ''
-
-// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used
-// for Statsig reporting and shouldn't be used to identify a specific bundle.
-export const BUNDLE_DATE =
-  IS_TESTFLIGHT || __DEV__ ? 0 : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
-
-export const appVersion = `${nativeApplicationVersion}.${nativeBuildVersion}`
-export const bundleInfo = `${BUNDLE_IDENTIFIER} (${
-  __DEV__ ? 'dev' : IS_TESTFLIGHT ? 'tf' : 'prod'
-})`
diff --git a/src/lib/app-info.web.ts b/src/lib/app-info.web.ts
deleted file mode 100644
index 1530d9976..000000000
--- a/src/lib/app-info.web.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import packageDotJson from '../../package.json'
-
-export const IS_TESTFLIGHT = false
-export const IS_INTERNAL = __DEV__
-
-// This is the commit hash that the current bundle was made from. The user can see the commit hash in the app's settings
-// along with the other version info. Useful for debugging/reporting.
-export const BUNDLE_IDENTIFIER =
-  process.env.EXPO_PUBLIC_BUNDLE_IDENTIFIER ?? 'dev'
-
-// This will always be in the format of YYMMDD, so that it always increases for each build. This should only be used
-// for Statsig reporting and shouldn't be used to identify a specific bundle.
-export const BUNDLE_DATE = __DEV__
-  ? 0
-  : Number(process.env.EXPO_PUBLIC_BUNDLE_DATE)
-
-export const appVersion = packageDotJson.version
-export const bundleInfo = `${BUNDLE_IDENTIFIER} (${__DEV__ ? 'dev' : 'prod'})`
diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts
index 864d5d697..ba46b6055 100644
--- a/src/lib/hooks/useOTAUpdates.ts
+++ b/src/lib/hooks/useOTAUpdates.ts
@@ -10,9 +10,9 @@ import {
   useUpdates,
 } from 'expo-updates'
 
-import {IS_TESTFLIGHT} from '#/lib/app-info'
 import {logger} from '#/logger'
 import {isIOS} from '#/platform/detection'
+import {IS_TESTFLIGHT} from '#/env'
 
 const MINIMUM_MINIMIZE_TIME = 15 * 60e3
 
diff --git a/src/lib/statsig/statsig.tsx b/src/lib/statsig/statsig.tsx
index f2d3ffca9..1091c82e0 100644
--- a/src/lib/statsig/statsig.tsx
+++ b/src/lib/statsig/statsig.tsx
@@ -3,12 +3,11 @@ import {Platform} from 'react-native'
 import {AppState, type AppStateStatus} from 'react-native'
 import {Statsig, StatsigProvider} from 'statsig-react-native-expo'
 
-import {BUNDLE_DATE, BUNDLE_IDENTIFIER, IS_TESTFLIGHT} from '#/lib/app-info'
 import {logger} from '#/logger'
 import {type MetricEvents} from '#/logger/metrics'
 import {isWeb} from '#/platform/detection'
 import * as persisted from '#/state/persisted'
-import packageDotJson from '../../../package.json'
+import * as env from '#/env'
 import {useSession} from '../../state/session'
 import {timeout} from '../async/timeout'
 import {useNonReactiveCallback} from '../hooks/useNonReactiveCallback'
@@ -49,12 +48,11 @@ export type {MetricEvents as LogEvents}
 function createStatsigOptions(prefetchUsers: StatsigUser[]) {
   return {
     environment: {
-      tier:
-        process.env.NODE_ENV === 'development'
-          ? 'development'
-          : IS_TESTFLIGHT
-            ? 'staging'
-            : 'production',
+      tier: env.IS_DEV
+        ? 'development'
+        : env.IS_TESTFLIGHT
+          ? 'staging'
+          : 'production',
     },
     // Don't block on waiting for network. The fetched config will kick in on next load.
     // This ensures the UI is always consistent and doesn't update mid-session.
@@ -212,9 +210,9 @@ function toStatsigUser(did: string | undefined): StatsigUser {
       refSrc,
       refUrl,
       platform: Platform.OS as 'ios' | 'android' | 'web',
-      appVersion: packageDotJson.version,
-      bundleIdentifier: BUNDLE_IDENTIFIER,
-      bundleDate: BUNDLE_DATE,
+      appVersion: env.RELEASE_VERSION,
+      bundleIdentifier: env.BUNDLE_IDENTIFIER,
+      bundleDate: env.BUNDLE_DATE,
       appLanguage: languagePrefs.appLanguage,
       contentLanguages: languagePrefs.contentLanguages,
     },
diff --git a/src/logger/bitdrift/setup/index.ts b/src/logger/bitdrift/setup/index.ts
index d6af3fe24..dd2560acc 100644
--- a/src/logger/bitdrift/setup/index.ts
+++ b/src/logger/bitdrift/setup/index.ts
@@ -2,8 +2,7 @@ import {init, SessionStrategy} from '@bitdrift/react-native'
 import {Statsig} from 'statsig-react-native-expo'
 
 import {initPromise} from '#/lib/statsig/statsig'
-
-const BITDRIFT_API_KEY = process.env.BITDRIFT_API_KEY
+import {BITDRIFT_API_KEY} from '#/env'
 
 initPromise.then(() => {
   let isEnabled = false
diff --git a/src/logger/index.ts b/src/logger/index.ts
index e7aaf666a..998d02581 100644
--- a/src/logger/index.ts
+++ b/src/logger/index.ts
@@ -14,9 +14,10 @@ import {
 } from '#/logger/types'
 import {enabledLogLevels} from '#/logger/util'
 import {isNative} from '#/platform/detection'
+import {ENV} from '#/env'
 
 const TRANSPORTS: Transport[] = (function configureTransports() {
-  switch (process.env.NODE_ENV) {
+  switch (ENV) {
     case 'production': {
       return [sentryTransport, isNative && bitdriftTransport].filter(
         Boolean,
diff --git a/src/logger/sentry/setup/index.ts b/src/logger/sentry/setup/index.ts
index f05a7fc83..d062f05d2 100644
--- a/src/logger/sentry/setup/index.ts
+++ b/src/logger/sentry/setup/index.ts
@@ -1,32 +1,15 @@
-/**
- * Importing these separately from `platform/detection` and `lib/app-info` to
- * avoid future conflicts and/or circular deps
- */
-
 import {init} from '@sentry/react-native'
 
-import pkgJson from '#/../package.json'
-
-/**
- * Examples:
- * - `dev`
- * - `1.99.0`
- */
-const release = process.env.SENTRY_RELEASE || pkgJson.version
-
-/**
- * The latest deployed commit hash
- */
-const dist = process.env.SENTRY_DIST || 'dev'
+import * as env from '#/env'
 
 init({
-  enabled: !__DEV__ && !!process.env.SENTRY_DSN,
+  enabled: !env.IS_DEV && !!env.SENTRY_DSN,
   autoSessionTracking: false,
-  dsn: process.env.SENTRY_DSN,
+  dsn: env.SENTRY_DSN,
   debug: false, // If `true`, Sentry will try to print out useful debugging information if something goes wrong with sending the event. Set it to `false` in production
-  environment: process.env.NODE_ENV,
-  dist,
-  release,
+  environment: env.ENV,
+  dist: env.BUNDLE_IDENTIFIER,
+  release: env.RELEASE_VERSION,
   ignoreErrors: [
     /*
      * Unknown internals errors
diff --git a/src/screens/Settings/AboutSettings.tsx b/src/screens/Settings/AboutSettings.tsx
index 0ce127ff3..e48841d9f 100644
--- a/src/screens/Settings/AboutSettings.tsx
+++ b/src/screens/Settings/AboutSettings.tsx
@@ -9,7 +9,6 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 import {useMutation} from '@tanstack/react-query'
 import {Statsig} from 'statsig-react-native-expo'
 
-import {appVersion, BUNDLE_DATE, bundleInfo} from '#/lib/app-info'
 import {STATUS_PAGE_URL} from '#/lib/constants'
 import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {isAndroid, isIOS, isNative} from '#/platform/detection'
@@ -23,6 +22,7 @@ import {Newspaper_Stroke2_Corner2_Rounded as NewspaperIcon} from '#/components/i
 import {Wrench_Stroke2_Corner2_Rounded as WrenchIcon} from '#/components/icons/Wrench'
 import * as Layout from '#/components/Layout'
 import {Loader} from '#/components/Loader'
+import * as env from '#/env'
 import {useDemoMode} from '#/storage/hooks/demo-mode'
 import {useDevMode} from '#/storage/hooks/dev-mode'
 import {OTAInfo} from './components/OTAInfo'
@@ -123,7 +123,7 @@ export function AboutSettingsScreen({}: Props) {
             </SettingsList.PressableItem>
           )}
           <SettingsList.PressableItem
-            label={_(msg`Version ${appVersion}`)}
+            label={_(msg`Version ${env.APP_VERSION}`)}
             accessibilityHint={_(msg`Copies build version to clipboard`)}
             onLongPress={() => {
               const newDevModeEnabled = !devModeEnabled
@@ -146,15 +146,15 @@ export function AboutSettingsScreen({}: Props) {
             }}
             onPress={() => {
               setStringAsync(
-                `Build version: ${appVersion}; Bundle info: ${bundleInfo}; Bundle date: ${BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`,
+                `Build version: ${env.APP_VERSION}; Bundle info: ${env.APP_METADATA}; Bundle date: ${env.BUNDLE_DATE}; Platform: ${Platform.OS}; Platform version: ${Platform.Version}; Anonymous ID: ${stableID}`,
               )
               Toast.show(_(msg`Copied build version to clipboard`))
             }}>
             <SettingsList.ItemIcon icon={WrenchIcon} />
             <SettingsList.ItemText>
-              <Trans>Version {appVersion}</Trans>
+              <Trans>Version {env.APP_VERSION}</Trans>
             </SettingsList.ItemText>
-            <SettingsList.BadgeText>{bundleInfo}</SettingsList.BadgeText>
+            <SettingsList.BadgeText>{env.APP_METADATA}</SettingsList.BadgeText>
           </SettingsList.PressableItem>
           {devModeEnabled && (
             <>
diff --git a/src/screens/Settings/AppIconSettings/index.tsx b/src/screens/Settings/AppIconSettings/index.tsx
index 954bac68a..799873c2d 100644
--- a/src/screens/Settings/AppIconSettings/index.tsx
+++ b/src/screens/Settings/AppIconSettings/index.tsx
@@ -3,20 +3,20 @@ import {Alert, View} from 'react-native'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import * as DynamicAppIcon from '@mozzius/expo-dynamic-app-icon'
-import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
-import {IS_INTERNAL} from '#/lib/app-info'
 import {PressableScale} from '#/lib/custom-animations/PressableScale'
-import {CommonNavigatorParams} from '#/lib/routes/types'
+import {type CommonNavigatorParams} from '#/lib/routes/types'
 import {useGate} from '#/lib/statsig/statsig'
 import {isAndroid} from '#/platform/detection'
 import {AppIconImage} from '#/screens/Settings/AppIconSettings/AppIconImage'
-import {AppIconSet} from '#/screens/Settings/AppIconSettings/types'
+import {type AppIconSet} from '#/screens/Settings/AppIconSettings/types'
 import {useAppIconSets} from '#/screens/Settings/AppIconSettings/useAppIconSets'
 import {atoms as a, useTheme} from '#/alf'
 import * as Toggle from '#/components/forms/Toggle'
 import * as Layout from '#/components/Layout'
 import {Text} from '#/components/Typography'
+import {IS_INTERNAL} from '#/env'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppIconSettings'>
 export function AppIconSettingsScreen({}: Props) {
diff --git a/src/screens/Settings/AppearanceSettings.tsx b/src/screens/Settings/AppearanceSettings.tsx
index d0158aaa8..492d6d172 100644
--- a/src/screens/Settings/AppearanceSettings.tsx
+++ b/src/screens/Settings/AppearanceSettings.tsx
@@ -8,7 +8,6 @@ import Animated, {
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
-import {IS_INTERNAL} from '#/lib/app-info'
 import {
   type CommonNavigatorParams,
   type NativeStackScreenProps,
@@ -26,6 +25,7 @@ import {TextSize_Stroke2_Corner0_Rounded as TextSize} from '#/components/icons/T
 import {TitleCase_Stroke2_Corner0_Rounded as Aa} from '#/components/icons/TitleCase'
 import * as Layout from '#/components/Layout'
 import {Text} from '#/components/Typography'
+import {IS_INTERNAL} from '#/env'
 import * as SettingsList from './components/SettingsList'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppearanceSettings'>
diff --git a/src/screens/Settings/Settings.tsx b/src/screens/Settings/Settings.tsx
index b712c054c..719bbf9a2 100644
--- a/src/screens/Settings/Settings.tsx
+++ b/src/screens/Settings/Settings.tsx
@@ -9,7 +9,6 @@ import {useNavigation} from '@react-navigation/native'
 import {type NativeStackScreenProps} from '@react-navigation/native-stack'
 
 import {useActorStatus} from '#/lib/actor-status'
-import {IS_INTERNAL} from '#/lib/app-info'
 import {HELP_DESK_URL} from '#/lib/constants'
 import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'
 import {useApplyPullRequestOTAUpdate} from '#/lib/hooks/useOTAUpdates'
@@ -66,6 +65,7 @@ import {
   shouldShowVerificationCheckButton,
   VerificationCheckButton,
 } from '#/components/verification/VerificationCheckButton'
+import {IS_INTERNAL} from '#/env'
 import {useActivitySubscriptionsNudged} from '#/storage/hooks/activity-subscriptions-nudged'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Settings'>
diff --git a/src/state/queries/messages/const.ts b/src/state/queries/messages/const.ts
index 7642c5d7b..1c5519a63 100644
--- a/src/state/queries/messages/const.ts
+++ b/src/state/queries/messages/const.ts
@@ -1,3 +1,5 @@
+import {CHAT_PROXY_DID} from '#/env'
+
 export const DM_SERVICE_HEADERS = {
-  'atproto-proxy': 'did:web:api.bsky.chat#bsky_chat',
+  'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`,
 }
diff --git a/src/state/session/logging.ts b/src/state/session/logging.ts
index 98de5a396..bf847f08f 100644
--- a/src/state/session/logging.ts
+++ b/src/state/session/logging.ts
@@ -1,11 +1,11 @@
-import {AtpSessionData, AtpSessionEvent} from '@atproto/api'
+import {type AtpSessionData, type AtpSessionEvent} from '@atproto/api'
 import {sha256} from 'js-sha256'
 import {Statsig} from 'statsig-react-native-expo'
 
-import {IS_INTERNAL} from '#/lib/app-info'
-import {Schema} from '../persisted'
-import {Action, State} from './reducer'
-import {SessionAccount} from './types'
+import {IS_INTERNAL} from '#/env'
+import {type Schema} from '../persisted'
+import {type Action, type State} from './reducer'
+import {type SessionAccount} from './types'
 
 type Reducer = (state: State, action: Action) => State
 
diff --git a/webpack.config.js b/webpack.config.js
index 9a238e549..7f58a7448 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -53,7 +53,7 @@ module.exports = async function (env, argv) {
         project: 'app',
         authToken: process.env.SENTRY_AUTH_TOKEN,
         release: {
-          // env is undefined for Render.com builds, fall back
+          // fallback needed for Render.com deployments
           name: process.env.SENTRY_RELEASE || version,
           dist: process.env.SENTRY_DIST,
         },