diff --git a/apps/swift/Resources/PackRat-iOS.entitlements b/apps/swift/Resources/PackRat-iOS.entitlements
index 0c67376eba..a812db506f 100644
--- a/apps/swift/Resources/PackRat-iOS.entitlements
+++ b/apps/swift/Resources/PackRat-iOS.entitlements
@@ -1,5 +1,10 @@
-
+
+ com.apple.developer.applesignin
+
+ Default
+
+
diff --git a/packages/api/.dev.vars.e2e.example b/packages/api/.dev.vars.e2e.example
index 72f0971454..4b263a8f2a 100644
--- a/packages/api/.dev.vars.e2e.example
+++ b/packages/api/.dev.vars.e2e.example
@@ -58,6 +58,7 @@ PACKRAT_GUIDES_BASE_URL=https://guides.packratai.com/
EXPO_PUBLIC_GOOGLE_IOS_CLIENT_ID=
EXPO_PUBLIC_GOOGLE_WEB_CLIENT_ID=
GOOGLE_CLIENT_ID=
+GOOGLE_IOS_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
# ── Maps ─────────────────────────────────────────────────────────────────────
@@ -68,6 +69,7 @@ SENTRY_DSN=
# ── Apple Sign In ─────────────────────────────────────────────────────────────
APPLE_CLIENT_ID=
+APPLE_SWIFT_CLIENT_ID=
APPLE_PRIVATE_KEY=
APPLE_KEY_ID=
APPLE_TEAM_ID=
diff --git a/packages/api/src/auth/index.ts b/packages/api/src/auth/index.ts
index 4242b46c8a..8705b71196 100644
--- a/packages/api/src/auth/index.ts
+++ b/packages/api/src/auth/index.ts
@@ -193,7 +193,11 @@ async function buildAuth(env: ValidatedEnv): Promise {
socialProviders: {
google: {
- clientId: env.GOOGLE_CLIENT_ID ?? '',
+ // iOS native Google Sign-In requests an id token whose `aud` claim is the
+ // iOS OAuth client ID, distinct from the web client ID — accept both audiences.
+ clientId: [env.GOOGLE_CLIENT_ID, env.GOOGLE_IOS_CLIENT_ID].filter((id): id is string =>
+ Boolean(id),
+ ),
clientSecret: env.GOOGLE_CLIENT_SECRET ?? '',
},
// Always register Apple when clientId is present so the native id-token
@@ -201,7 +205,9 @@ async function buildAuth(env: ValidatedEnv): Promise {
// verifies against Apple's public JWKS; the secret is only used for the
// web OAuth redirect flow).
// audience covers all EAS build variants — Apple puts the bundle ID in
- // the `aud` claim, which differs per variant (.dev, .preview, base).
+ // the `aud` claim, which differs per variant (.dev, .preview, base) — plus
+ // the native Swift app's distinct bundle ID, which is a separate Xcode
+ // target and not an EAS build variant of the Expo app.
...(env.APPLE_CLIENT_ID
? {
apple: {
@@ -212,6 +218,7 @@ async function buildAuth(env: ValidatedEnv): Promise {
env.APPLE_CLIENT_ID,
`${env.APPLE_CLIENT_ID}.dev`,
`${env.APPLE_CLIENT_ID}.preview`,
+ ...(env.APPLE_SWIFT_CLIENT_ID ? [env.APPLE_SWIFT_CLIENT_ID] : []),
],
},
}
diff --git a/packages/api/src/utils/env-validation.ts b/packages/api/src/utils/env-validation.ts
index 7d0ab34d59..3597e42e7d 100644
--- a/packages/api/src/utils/env-validation.ts
+++ b/packages/api/src/utils/env-validation.ts
@@ -48,9 +48,15 @@ export const apiEnvObjectSchema = z.object({
E2E_TEST_USER_ID: z.string().uuid().optional(),
// Google OAuth (Better Auth social provider)
GOOGLE_CLIENT_ID: z.string(),
+ // iOS native Google Sign-In uses a separate OAuth client whose ID appears in the
+ // ID token's `aud` claim — Better Auth must accept both audiences or native sign-in fails.
+ GOOGLE_IOS_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string(),
// Apple Sign In (Better Auth social provider)
APPLE_CLIENT_ID: z.string(), // bundle ID e.g. world.packrat.app
+ // Native Swift app's distinct bundle ID (separate Xcode target, not an EAS build
+ // variant of the Expo app) — appears in its Apple ID token's `aud` claim.
+ APPLE_SWIFT_CLIENT_ID: z.string().optional(),
APPLE_PRIVATE_KEY: z.string(), // .p8 key contents — store via wrangler secret
APPLE_KEY_ID: z.string(),
APPLE_TEAM_ID: z.string(),