# Sign in with Apple

This guide covers the Speed-specific parts of adding Sign in with Apple to an iOS app backed by Speed. It assumes you know how to use `AuthenticationServices`; it does not repeat Apple's documentation.

## Why you need it

App Store Review Guideline 4.8 requires that an app offering a third-party login service also offer an equivalent option that lets users hide their email address. "Continue with Speed" counts as a third-party login service, so if you offer it, you must also offer Sign in with Apple.

Guideline 5.1.1(v) separately requires in-app account deletion. See [Account deletion](#account-deletion).

## Two kinds of account

Your login screen offers two options, and they produce different kinds of Speed account:

| | Sign in with Apple | Continue with Speed |
|---|---|---|
| Flow | Native `ASAuthorizationController` | `ASWebAuthenticationSession` → `dash.speed.so` |
| Account | **App-local** — usable only in your app | Regular Speed account, shared across all Speed apps |
| Balance | App-local, with an optional linked Speed balance | Shared across all Speed apps |

Apple scopes its user identifier to your developer team, so a user signing in with Apple in your app cannot be recognised as the same person in any other Speed app. That is Apple's design, not a Speed limitation. Users who want a shared account can [link](#account-linking) their app-local account to a regular Speed account.

App-local accounts can use every platform API your app calls on their behalf. They cannot own Speed apps or sign in to `dash.speed.so`.

They start with no balance and cannot top up from the Speed dashboard. An app using the **Local** usage source can add app-local balance after independently processing a purchase or gift on its backend. See [Apple In-App Purchases](./Apple-IAP.md). The underlying Speed resource cost is charged to the app owner's Speed subscription and balance. A linked regular Speed account can be used as a fallback when its owner has granted access to the app.

## Setup

**In your Apple Developer account**: Certificates, Identifiers & Profiles → Identifiers → select your App ID → enable **Sign in with Apple** → Save.

Wildcard App IDs are not supported. Changing capabilities invalidates existing provisioning profiles, so regenerate them.

That is the whole Apple-side setup. The native flow issues the identity token for your Bundle ID and Speed verifies it against Apple's public keys, so there is no Services ID, key, or Return URL to configure.

**In the Speed dashboard**, open your app's settings -> **iOS** and register:

- your Bundle ID (one or more). Identity tokens are only accepted for these.
- your custom URL scheme, without the `://`. The sign-in page refuses to redirect to
  a scheme you have not registered, so add it before testing.

## Sign in with Apple

All endpoints below live on `https://api.speed.so`.

Generate a random nonce, send Apple its SHA-256 as lowercase hex — what Apple's own
`sha256` sample helper returns — and keep the raw value to send to Speed:

```swift
let request = ASAuthorizationAppleIDProvider().createRequest()
request.requestedScopes = [.fullName, .email]
request.nonce = sha256(rawNonce)
```

Then exchange the identity token for a Speed token:

```
POST /api/platform/auth/apple-native
Content-Type: application/json

{
  "appId": 123,                 // or "domain": "yourapp.spdso.app"
  "identityToken": "<credential.identityToken as UTF-8>",
  "rawNonce": "<the unhashed nonce>",
  "fullName": "Jane Doe"        // first authorization only, see below
}
```

Response:

```json
{
  "token": "<Speed JWT>",
  "user": { "id": "...", "name": "...", "email": "..." },
  "role": "app-user",
  "appUserUUID": "3d4c5b2d-0000-5000-8000-000000000000",
  "isNewAccount": true,
  "isLocalUser": true,
  "linkSuggestion": "matching_email"
}
```

`isNewAccount` is true when this request created the account, so nothing in your app has ever been stored under it. `isLocalUser` is true for an app-local account that has not been linked yet — exactly when linking is still possible. `linkSuggestion` is present when we found a regular Speed account worth suggesting; it is absent otherwise. Together they drive the linking prompt — see [Sign-in states](#sign-in-states).

`appUserUUID` is a stable UUID for this app-facing account. It is not a credential. Apps using StoreKit top-ups pass it as Apple's `appAccountToken`; see [Apple In-App Purchases](./Apple-IAP.md). Store it with the session and replace it whenever a link response replaces the user ID.

**Send `fullName` on the first authorization.** Apple returns the user's name exactly once, ever. If you do not forward it, it is gone permanently and Speed will have no name for that user.

**Do not read `credential.email`.** It is populated only on the first authorization and is `nil` afterwards. Speed reads the email from the identity token instead. Some Apple IDs return no email at all, so do not build flows that assume one exists.

### Errors

| | |
|---|---|
| `400 ios_not_configured` | No Bundle ID is registered for this app in the dashboard. |
| `401 invalid_identity_token` | Expired, replayed, or signed for a Bundle ID you did not register. Also what a mismatched nonce returns. |
| `403 bundle_id_conflict` | The Bundle ID is registered on another Speed app too. Remove it from one of them. |
| `404 Target not found` | No app matches the `appId` or `domain` you sent. |

**Use `ASAuthorizationAppleIDButton`.** Apps have been rejected under Guideline 4.0 for Sign in with Apple buttons that deviate from Apple's design, branding, or localization.

## Continue with Speed

Open `ASWebAuthenticationSession` at:

```
https://dash.speed.so/platform/auth?appId=123&redirect=yourscheme://speed-auth
```

The first time a user reaches your app this way, Speed asks them to confirm before
issuing a token, since that first sign-in registers them with your app and hands you
their name and photo. Later sign-ins go straight through.

The callback carries a `_speed_auth_data` query parameter containing the same JSON as above, URL-encoded.

## Using the token

```
Authorization: Bearer <Speed JWT>
X-Speed-Request-From: app
```

## Sessions

Tokens are valid for one week. There is no refresh endpoint — on a `401`, run the sign-in flow again.

**Remember which method the user signed in with** and take them straight back to it. Do not make them choose between Apple and Speed a second time.

## Account linking

An app-local account can be linked to a regular Speed account so the user gets a shared balance across Speed apps. **Linking must be optional** — an app-local account has to be able to use your app fully, or Sign in with Apple stops being an equivalent option under 4.8.

### Sign-in states

Where the user can be when they sign in with Apple, and what your app should do about it.

| Apple account | Speed account | What your app should do |
|---|---|---|
| Created by this sign-in | One holds the same verified email | Before you show the app, tell the user they may already have a Speed account under this email, and offer to sign in to it. Nothing is stored under the new account yet, so link with `keep: "speed"`. There is no need to ask which data to keep. |
| Signed in before, still app-local | One holds the same verified email | Prompt on every sign-in until the user links or declines for good. There may be data on both sides by now, so linking has to ask which side survives. |
| Created by this sign-in | None found. Either no Speed account uses that email, or the user hid it and Apple gave you an anonymous address that can never match one | Go straight into the app. Put a "Link to a Speed account" entry in settings for later. |
| Signed in before, still app-local | None found, for either reason above | No prompt. The settings entry is the only route. |
| Linked, keeping its own data | Backs it: balance and profile come from there | Nothing to offer; hide the linking entry. The user id did not change. |
| Linked, keeping the Speed account's data | Is the account the Apple ID now reaches | Nothing to offer; hide the linking entry. The user id changed to the Speed account's. |

`isNewAccount`, `linkSuggestion` and `isLocalUser` in the sign-in response tell you which row you are in.

A matching email is a suggestion, not proof of identity — we never return the address, and never link accounts on our own. Users who hid their email land in the third or fourth row, where the settings entry is the only route they will ever see.

### Linking flow

1. Open `ASWebAuthenticationSession` at
   `https://dash.speed.so/platform/auth?appId=123&link=1&redirect=yourscheme://speed-link`
   Note the `link=1`. Without it this is the Continue with Speed sign-in URL.
2. The user signs in to Speed.
3. The callback carries `_speed_auth_data`, as with Continue with Speed. Do **not** adopt the token in it as a session — pass it straight to the call below.
4. Exchange it, authenticated with the app-local account's token:

```
POST /api/platform/auth/link
Authorization: Bearer <app-local account token>

{ "speedToken": "<token from _speed_auth_data>" }
```

The app-local token stays valid: the account's id did not change, only the account
backing it. You do not need to sign in again. The one exception is `keep: "speed"`
below, which hands you a new token to replace it with.

Linking is platform-level, so the Speed account does not need to have used your app
before.

### Handling the data conflict

If the Speed account the user is linking to already has data in your app — because they previously signed in to it with Continue with Speed — the request fails:

```
409 { "error": "app_data_conflict" }
```

Both accounts have data in your app and only one set can survive. Ask the user which one, then retry with the answer:

```
POST /api/platform/auth/link
{ "speedToken": "<same token>", "keep": "speed" }
```

- **`keep: "speed"`** — the user keeps what they had in your app before. Apple sign-in resolves to their Speed account from now on, and the response carries a **new token and a new user id**. Switch to both, and delete the rows under the id returned as `previousUserId`.
- **`keep: "local"`** — the user keeps what they built up since signing in with Apple. The user id does not change. The Speed account's earlier rows in your app are not deleted, but nothing will reach them again.

App-local balance follows the surviving app-facing account:

- `keep: "local"` leaves the balance and `appUserUUID` unchanged.
- `keep: "speed"` merges the old app-local balance into the Speed account's balance for this app and moves its adjustment history. The response contains the new `appUserUUID`; use it for future StoreKit purchases.

Say which data is being given up before the user confirms. Neither direction can be undone through the API.

### Linking errors

| | |
|---|---|
| `400 not_linkable` | The token you authenticated with is not an unlinked app-local account. Check `isLocalUser` before offering the entry. |
| `400 invalid_speed_token` | The `speedToken` is not a regular Speed account — expired, or itself app-local. Run the `link=1` flow again. |
| `409 app_data_conflict` | Both sides have data. Retry with `keep`, as above. |

`keep` is honoured whenever you send it, conflict or not — the first row of [Sign-in states](#sign-in-states) uses `keep: "speed"` on a brand-new account, where there is nothing to give up. Omitting it never changes the user id, and is only accepted when the Speed account has no data in your app.

## Account deletion

Guideline 5.1.1(v) requires that users can start account deletion inside the app. Put it somewhere findable, typically account settings.

Two steps, in order, both on your app's own backend:

1. **Delete the user's data in your own database** — every row keyed by the Speed user ID.
2. Call:

```
DELETE /api/platform/auth/me
Authorization: Bearer <Speed JWT>
```

Afterwards your app holds no rows for that user and Speed has revoked its authorization. Speed also deletes that account's app-local balance. An account that only ever existed inside your app loses its Apple identity, so the same Apple ID signs in as a brand-new user next time and does not recover the previous balance.

A regular Speed account is never deleted by this, only its access to your app. It is the user's account across every Speed app, and their balance lives on it.

Deletion cannot be initiated by email or a support flow — it has to happen in the app.

Before confirmation, tell the user that any unused app-local balance will be permanently lost.

## WebView shells

If your app is a WebView wrapper, do the Apple sign-in natively, then load your web app with the auth data attached:

```
https://yourapp.spdso.app/?_speed_auth_data=<url-encoded JSON>
```

The Speed SDK picks it up automatically and stores the session.

A Speed-generated web backend can call `unlinkSpeedAccount(ctx.var.userToken)` from
`speed-framework` in the same endpoint that deletes its own rows.
