For AI agents: fetch the complete documentation index at /llms.txt, the full documentation corpus at /llms-full.txt, the agent skill at /skill.md, or this page as Markdown at /docs/web/apple-sign-in.md (or send Accept: text/markdown).
Add Apple sign-in
Register an App ID, a Services ID, and a Sign in with Apple key in the Apple Developer portal, then wire the client pair into the app.
Beginner walkthrough
Prerequisites: the web app running locally (Ship in 5 minutes), a migrated database for real accounts (Set up database), and an Apple Developer Program membership — the same $99-per-year account the mobile accounts guide enrolls, so complete that step before continuing here if you have not already. Everything inside the developer portal below costs nothing extra.
Why it matters: App Store review requires Sign in with Apple whenever an iOS app offers third-party social sign-in such as Google or GitHub, and it is a solid low-friction button on the web too — Apple users sign in with the account already on their devices.
Quick terms: an App ID is your app's registered identifier (com.yourcompany.yourapp). A Services ID is a second identifier Apple issues for web and server sign-in — that one becomes your client ID. A return URL is the exact address Apple may send a signed-in user back to — the kit's Better Auth callback. A secret key is the .p8 file Apple gives you once; you turn it into the client secret yourself.
1. Register an App ID with the Sign in with Apple capability. 2. Register a Services ID that points at it and holds your domain plus return URL. 3. Create a Sign in with Apple key and download the .p8 file. 4. Turn the key into a client-secret JWT and set the three env values.
One honest limitation: Apple's return URLs must be absolute https:// addresses on a registered domain — localhost and plain http are rejected, so the button cannot be fully exercised on a bare local server. Verify on your deployed URL, or expose the local server through an https tunnel (for example cloudflared tunnel or ngrok) and register the tunnel domain plus its callback while testing.
Register the App ID
1. Open developer.apple.com/account and sign in with the account holding the Developer Program membership.
2. Open Certificates, Identifiers & Profiles → Identifiers and click the blue + button.
3. Select App IDs → Continue, keep the type set to App → Continue.
4. Description: your product name. Bundle ID: choose Explicit and enter the reverse-domain form com.yourcompany.yourapp — if you ship the iOS app, use exactly the ios.bundleIdentifier in apps/mobile/app.json so both platforms share one App ID.
5. In the capability list tick Sign in with Apple → Continue → Register.
Expected: the identifier appears in the Identifiers list with the Sign in with Apple capability enabled. Reuse this one App ID — the Services ID and the key both attach to it as the primary app.
Register the Services ID and return URL
1. Back in Identifiers click + again, choose Services IDs → Continue.
2. Description: something like yourapp web sign-in. Identifier: another reverse-domain name — com.yourcompany.yourapp.web is the convention — deliberately different from the App ID. This Services ID is the client ID the server uses for web sign-in; copy it exactly, then Continue → Register.
3. Select the new Services ID in the list, tick Sign in with Apple, and click Configure.
4. In the dialog set Primary App ID to the App ID from the previous section. Under Website URLs add your domain (for example yourdomain.com), and under Return URLs add https://yourdomain.com/api/auth/callback/apple — that path is the kit's Better Auth callback, {APP_URL}/api/auth/callback/apple. Save → Continue → Save.
Each return URL must be an absolute https:// address on a registered domain: localhost, bare IP addresses, and http are rejected. For local development run the app behind an https tunnel, add the tunnel domain to the Services ID, register https://<tunnel-domain>/api/auth/callback/apple, and set APP_URL to the tunnel URL while testing.
Expected: the Services ID reads Sign in with Apple → Enabled. If the portal rejects your domain, finish domain ownership verification on the account page first.
Create the Sign in with Apple key
1. Open Keys in the left sidebar → +. Name the key (for example yourapp sign-in key), tick Sign in with Apple, click Configure, choose the App ID as the Primary App ID → Save → Continue → Register.
2. On the confirmation screen record the Key ID, then click Download — the .p8 file is offered exactly once. If you lose it, revoke the key and create another; Apple does not keep a copy.
3. Note your Team ID too — it is printed in the top-right corner of the developer account pages, next to your account name.
Expected: you now hold three values — the .p8 file, a Key ID, and your Team ID. Keep the .p8 file out of Git: it signs as your developer account.
Add the values to the app
The kit expects exactly the three lines below — the same clientId/clientSecret pair every social provider uses. APPLE_CLIENT_ID is the Services ID (com.yourcompany.yourapp.web), not the App ID.
Apple issues no static client secret, so APPLE_CLIENT_SECRET is a JWT you sign yourself from the Team ID, the Key ID, and the .p8 private key — the Better Auth documentation ships the short generator script for it. A signed secret stays valid at most six months: set a reminder to regenerate it and update the env value, because sign-in fails the moment it lapses. Never paste the .p8 file's contents or the raw Key ID into env — only the generated JWT belongs there.
The client secret and the .p8 file are server-only: never in NEXT_PUBLIC_/EXPO_PUBLIC_ variables and never committed — in production set them through your host's secret store. Restart the dev server after editing; env changes are read at startup.
# apps/web/.env.local
AUTH_APPLE_ENABLED=true
APPLE_CLIENT_ID=com.yourcompany.yourapp.web # the Services ID, not the App ID
APPLE_CLIENT_SECRET=<JWT signed with Team ID + Key ID + .p8>Verify and troubleshoot
Open your APP_URL — a deployed URL or the https tunnel — at /auth, click the Apple button, sign in with any Apple ID (it does not need to be the developer account), and confirm you return signed in.
Most failures are one of three mix-ups: Services ID vs App ID — the client ID must be the Services ID for web sign-in; a return-URL mismatch — the callback must equal a registered Return URL exactly, protocol, domain and path included; or an expired/invalid client secret — regenerate the JWT, which is only a signed token, not the .p8 contents.
Until AUTH_APPLE_ENABLED=true is set, the Apple option on /auth stays disabled — check the env names and restart. Enabling the flag without both APPLE_CLIENT_ID and APPLE_CLIENT_SECRET fails startup and names the missing variable.
The iOS app reaches this same backend — ship Sign in with Apple inside it so App Store review accepts the other social buttons. The mobile accounts guide explains what review looks for.
If you want to continue setup, read Add Stripe payments.