diff --git a/.env.example b/.env.example
index df3456410b..93fa4c3124 100644
--- a/.env.example
+++ b/.env.example
@@ -46,3 +46,6 @@ GEOLOCATION_DEV_URL=
# live-events web worker URL
LIVE_EVENTS_DEV_URL=
+
+# app-config web worker URL
+APP_CONFIG_DEV_URL=
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 5de9f82b7b..7dc16ec058 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -22,6 +22,10 @@ import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
+import {
+ prefetchAppConfig,
+ Provider as AppConfigProvider,
+} from '#/state/appConfig'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
@@ -103,6 +107,7 @@ if (IS_ANDROID) {
Geo.resolve()
prefetchAgeAssuranceConfig()
prefetchLiveEvents()
+prefetchAppConfig()
function InnerApp() {
const [isReady, setIsReady] = React.useState(false)
@@ -228,38 +233,40 @@ function App() {
*/
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/App.web.tsx b/src/App.web.tsx
index abf188ea28..701167f392 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -13,6 +13,10 @@ import {ThemeProvider} from '#/lib/ThemeContext'
import I18nProvider from '#/locale/i18nProvider'
import {logger} from '#/logger'
import {Provider as A11yProvider} from '#/state/a11y'
+import {
+ prefetchAppConfig,
+ Provider as AppConfigProvider,
+} from '#/state/appConfig'
import {Provider as MutedThreadsProvider} from '#/state/cache/thread-mutes'
import {Provider as DialogStateProvider} from '#/state/dialogs'
import {Provider as EmailVerificationProvider} from '#/state/email-verification'
@@ -79,6 +83,7 @@ import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottom
Geo.resolve()
prefetchAgeAssuranceConfig()
prefetchLiveEvents()
+prefetchAppConfig()
function InnerApp() {
const [isReady, setIsReady] = useState(false)
@@ -202,31 +207,33 @@ function App() {
*/
return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
)
}
diff --git a/src/env/common.ts b/src/env/common.ts
index 5070b35968..5d8d89aace 100644
--- a/src/env/common.ts
+++ b/src/env/common.ts
@@ -141,3 +141,13 @@ export const LIVE_EVENTS_PROD_URL = `https://live-events.workers.bsky.app`
export const LIVE_EVENTS_URL = IS_DEV
? (LIVE_EVENTS_DEV_URL ?? LIVE_EVENTS_PROD_URL)
: LIVE_EVENTS_PROD_URL
+
+/**
+ * URLs for the app-config web worker. Can be a
+ * locally running server, see `env.example` for more.
+ */
+export const APP_CONFIG_DEV_URL = process.env.APP_CONFIG_DEV_URL
+export const APP_CONFIG_PROD_URL = `https://app-config.workers.bsky.app`
+export const APP_CONFIG_URL = IS_DEV
+ ? (APP_CONFIG_DEV_URL ?? APP_CONFIG_PROD_URL)
+ : APP_CONFIG_PROD_URL
diff --git a/src/state/appConfig.tsx b/src/state/appConfig.tsx
new file mode 100644
index 0000000000..2258efe3ad
--- /dev/null
+++ b/src/state/appConfig.tsx
@@ -0,0 +1,74 @@
+import {createContext, useContext} from 'react'
+import {QueryClient, useQuery} from '@tanstack/react-query'
+
+import {APP_CONFIG_URL} from '#/env'
+
+const qc = new QueryClient()
+const appConfigQueryKey = ['app-config']
+
+/**
+ * Matches the types defined in our `app-config` worker
+ */
+type AppConfigResponse = {
+ liveNow: {
+ allow: string[]
+ exceptions: {
+ did: string
+ allow: string[]
+ }[]
+ }
+}
+
+export const DEFAULT_APP_CONFIG_RESPONSE: AppConfigResponse = {
+ liveNow: {
+ allow: [],
+ exceptions: [],
+ },
+}
+
+async function fetchAppConfig(): Promise {
+ try {
+ const res = await fetch(`${APP_CONFIG_URL}/config`)
+ if (!res.ok) return null
+ const data = await res.json()
+ return data
+ } catch {
+ return null
+ }
+}
+
+const Context = createContext(DEFAULT_APP_CONFIG_RESPONSE)
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+ const {data} = useQuery(
+ {
+ staleTime: Infinity,
+ gcTime: Infinity,
+ queryKey: appConfigQueryKey,
+ async queryFn() {
+ return fetchAppConfig()
+ },
+ },
+ qc,
+ )
+ return (
+
+ {children}
+
+ )
+}
+
+export async function prefetchAppConfig() {
+ const data = await fetchAppConfig()
+ if (data) {
+ qc.setQueryData(appConfigQueryKey, data)
+ }
+}
+
+export function useAppConfig() {
+ const ctx = useContext(Context)
+ if (!ctx) {
+ throw new Error('useAppConfig must be used within a Provider')
+ }
+ return ctx
+}