- Accordion
- Action Sheet
- Alert Dialog
- Alert
- Audio Player
- Audio Recorder
- Audio Waveform
- Avatar
- AvoidKeyboard
- Badge
- BottomSheet
- Button
- Camera Preview
- Camera
- Card
- Carousel
- Checkbox
- Collapsible
- Color Picker
- Combobox
- Date Picker
- File Picker
- Gallery
- Hello Wave
- Icon
- Image
- Input OTP
- Input
- Link
- MediaPicker
- Mode Toggle
- Onboarding
- ParallaxScrollView
- Picker
- Popover
- Progress
- Radio
- ScrollView
- SearchBar
- Separator
- Share
- Sheet
- Skeleton
- Spinner
- Switch
- Table
- Tabs
- Text
- Toast
- Toggle
- Video
- View
Apple requires a deployed, HTTPS callback — your Convex deployment's .site
URL already qualifies, so there is no local-testing workaround to reach for
here the way there might be with other providers.
Before you start
- A paid Apple Developer account. Sign in with Apple is not available on a free account.
- A Convex deployment, so you have an HTTP Actions URL to point Apple at.
- Two identifiers, not one. Apple needs an App ID and a Services ID,
and it is the Services ID — not the App ID — that becomes your
AUTH_APPLE_ID. Getting these backwards is the most common failure here.
Configure Apple
Create an App ID
- Go to Certificates, Identifiers & Profiles.
- Select Identifiers in the sidebar, make sure App IDs is selected in the dropdown on the right, and click the + button.
- On the Register a New Identifier page, keep App IDs selected and click Continue.
- With App selected, click Continue.
- Fill in Description, and set Bundle ID to Explicit with your
app's identifier — e.g.
com.yourcompany.yourapp. It must match theios.bundleIdentifierin yourapp.json. - Scroll to Capabilities and check Sign In with Apple.
- Click Continue, then Register.
Create a Services ID
- Back on Certificates, Identifiers & Profiles, switch the dropdown to Services IDs and click +.
- Keep Services IDs selected and click Continue.
- Fill in Description and Identifier — e.g.
com.yourcompany.yourapp.service. This identifier is what you will set asAUTH_APPLE_ID, so it must be different from the App ID above. - Click Continue, then Register.
Create a signing key
- Click Keys in the sidebar, then +.
- Give the key a name.
- Check Sign In with Apple and click Configure beside it.
- Select the App ID you created as the Primary App ID, and click Save.
- Click Continue, then Register.
- Download the
.p8file. - Note the Key ID shown next to it, and your Team ID from the top right of the developer portal.
Apple lets you download a signing key exactly once. Store the .p8 somewhere
safe and out of version control — if you lose it you have to revoke the key
and create a new one.
Find your Convex HTTP Actions URL
In the Convex dashboard, open Settings → URL & Deploy Key and copy the
HTTP Actions URL. It ends in .site, not .cloud — the .cloud URL is
what your app talks to for queries and mutations, and it is not what Apple
redirects to.
Configure the Services ID for web authentication
-
Return to Identifiers, switch the dropdown to Services IDs, and click the Services ID you created.
-
Make sure Sign In with Apple is checked and click Configure.
-
Set the Primary App ID to your App ID.
-
In Domains and Subdomains, enter just the domain portion of your HTTP Actions URL — no scheme, no path:
fast-horse-123.convex.site -
In Return URLs, enter the full callback URL:
https://fast-horse-123.convex.site/api/auth/callback/apple -
Click Next, confirm the values, and click Done.
-
Back on the Services ID page, click Continue, then Save.
Generate the JWT client secret
Apple does not issue a static secret. You sign one yourself with the .p8
key, using four pieces of information:
| Value | Where it comes from |
|---|---|
| Team ID | Top right of the Apple Developer portal, 10 characters |
| Services ID | The identifier from step 2, e.g. com.you.app.service |
| Key ID | In the filename of the key, AuthKey_XXXXXXXXXX.p8 |
| Private key | The contents of the .p8 file |
const jwt = require('jsonwebtoken');
const fs = require('fs');
const privateKey = fs.readFileSync('AuthKey_XXXXXXXXXX.p8');
const token = jwt.sign(
{
iss: 'YOUR_TEAM_ID',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 15777000, // 6 months
aud: 'https://appleid.apple.com',
sub: 'YOUR_SERVICE_ID',
},
privateKey,
{ algorithm: 'ES256', header: { kid: 'YOUR_KEY_ID' } }
);
console.log(token);Sign it locally. The .p8 is a private key: pasting it into an online JWT
generator hands whoever runs that page the ability to authenticate as your app.
Set the environment variables
pnpm dlx convex env set AUTH_APPLE_ID your_service_id npx convex env set AUTH_APPLE_SECRET your_generated_jwt
Or add them in the Convex dashboard under Settings → Environment
Variables. Set them on your production deployment too, with --prod.
Six months, maximum. When it lapses, Apple sign-in stops for everyone with no code change and no deploy to blame. Put a calendar reminder on it the day you set it up.
Test it
pnpm dlx expo start
Tap Login with Apple. The browser sheet opens; sign in with an Apple ID.
What Apple sends back
Less than Google, and only once. convex/auth.ts's profile callback reads
whatever is present on that first authorization:
Apple({
profile: (appleInfo) => {
const name = appleInfo.user
? `${appleInfo.user.name.firstName} ${appleInfo.user.name.lastName}`
: undefined;
return {
id: appleInfo.sub,
name: name,
email: appleInfo.email,
};
},
}),appleInfo.user — the name — is only present on the very first sign-in.
Every subsequent one omits it, so name in the returned profile is
undefined from then on. If the user picked Hide My Email, email is a
private relay address, not their real one. Capture whatever you need at
first sign-in; do not build anything that depends on re-reading it later.
When it does not work
| What you see | Usually |
|---|---|
invalid_client | AUTH_APPLE_ID is the App ID instead of the Services ID |
invalid_client after months of working | The six-month JWT secret expired |
| Redirect rejected | Return URL in the Services ID doesn't match /api/auth/callback/apple |
| Browser closes, nothing happens | EXPO_URL/SITE_URL don't cover the redirect — see the redirect allow-list |
| Name is missing after the first sign-in | Working as intended — Apple sends it once |