docs(auth): add email link authentication guide - #9170
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9170 +/- ##
============================================
- Coverage 68.10% 68.09% -0.01%
+ Complexity 1912 1911 -1
============================================
Files 510 510
Lines 37877 37877
Branches 5193 5193
============================================
- Hits 25792 25788 -4
- Misses 10261 10262 +1
- Partials 1824 1827 +3
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
| This page focuses on wiring the flow in React Native Firebase, plus two community helpers that make hosting and association | ||
| files easier. |
There was a problem hiding this comment.
We need to follow the upstream as closely as possible to start and emphasize the non-custom / default path first I think
Going with that - please drop the "plus two community helpers…" framing here so the intro matches upstream (official Hosting / App Links / Universal Links only), but tease the custom domain info for advanced integrations. Suggested replacement:
This page focuses on wiring the flow in React Native Firebase. It starts with the same default Hosting-domain path as the official Android and Apple guides; There is information on optionally adding custom domains and branded landing pages later.| `handleCodeInApp` must be `true` for email-link sign-in. Do **not** set deprecated `dynamicLinkDomain`. Prefer omitting | ||
| `linkDomain` unless you have configured a **custom** Hosting link domain for the project — Auth selects the project default | ||
| Hosting domain when it is omitted. Setting `linkDomain` to a bare `*.web.app` / `*.firebaseapp.com` default is often rejected. |
There was a problem hiding this comment.
handleCodeInApp / omit dynamicLinkDomain / omit default linkDomain is correct — please keep that.
Please add a short Migrate from Dynamic Links subsection (after this paragraph or after Console setup) covering the Admin SDK API call integrators need to run from the Android migration guide. This is the #1 failure mode in #8405 (DYNAMIC_LINK_NOT_ACTIVATED, emails still using FDL).
Suggested snippet:
import { getAuth } from 'firebase-admin/auth';
await getAuth().projectConfigManager().updateProjectConfig({
mobileLinksConfig: {
// Literal value from the Firebase migration docs — switches the project
// from Dynamic Links to Firebase Hosting mobile links.
domain: 'HOSTING_DOMAIN',
},
});Also note: if emails still contain *.page.link after this, the project is still on FDL; linkDomain in ActionCodeSettings is only for a custom Hosting domain, not this switch.
| # Choose a hosting path | ||
|
|
||
| You need a domain that can serve association files and act as the continue / App Links host. Pick the path that matches your | ||
| situation: | ||
|
|
||
| | Situation | Use | Links | | ||
| | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | ||
| | **No website yet** — need a branded continue page + association files | Deploy **[email-link-host](https://github.com/rutvik24/email-link-host)** (Firebase Hosting Spark, Cloudflare Pages, Vercel, Netlify, Amplify, or Docker/Go) | [Docs](https://rutvik24.github.io/email-link-host/) · [Live demo](https://fir-email-link-host.web.app) | | ||
| | **Already have a website** — want that domain to open the app for email links (or general deep links) | Generate files with **[App Universal Links Helper](https://github.com/rutvik24/app-universal-links-helper)**, then host them under `/.well-known/` on your domain | [Live helper](https://rutvik24.github.io/app-universal-links-helper/) | | ||
|
|
||
| Both paths end the same way: Authorized domain in Firebase Auth, association files live on the domain, and the React Native | ||
| app claims that domain via App Links / Universal Links. |
There was a problem hiding this comment.
Please make the default Firebase Hosting domain the first and most prominent path. Every project already has PROJECT_ID.firebaseapp.com; that is how people will first integrate, and it matches upstream. Branded / custom-domain hosting is as-needed, not the getting-started path.
Suggested table (community tools mentioned, not promoted):
# Choose a hosting path
You need a domain that can receive `/__/auth/links` (App Links / Universal Links). Pick the simplest path that matches your app:
| Situation | Use |
| --- | --- |
| **Getting started** (no custom domain) | Use the project's default Hosting domain `PROJECT_ID.firebaseapp.com`. Register Android SHA-1 + SHA-256 in Firebase project settings so Firebase can serve association files. |
| **Custom / branded domain** | Attach a custom domain in Firebase Hosting (or reuse a domain you already operate). Serve `/.well-known/assetlinks.json` and `/.well-known/apple-app-site-association` on that host. Set `linkDomain` in `ActionCodeSettings` to that custom domain. |Then keep a short "Optional: custom domain" section with the official association-file format (link Digital Asset Links + Apple associated domains). If you mention generators or sample hosts, one sentence is enough — e.g. "Community generators exist for these JSON files; they are optional." Please don't lead with or deep-link a specific third-party repo as Path A/B.
| ## Path A — no site: email-link-host | ||
|
|
||
| [email-link-host](https://github.com/rutvik24/email-link-host) is a static (or tiny Go/Docker) host purpose-built for this | ||
| flow: branded landing page, optional store buttons, and association JSON generated from environment variables / | ||
| `config/site.json`. | ||
|
|
||
| 1. Clone the repo and copy `.env.example` → `.env` (or set the same vars in your host's CI / console). | ||
| 2. Set at least: | ||
| - `ANDROID_PACKAGE_NAME` | ||
| - `ANDROID_SHA256_CERT_FINGERPRINTS` (comma-separated) | ||
| - `IOS_TEAM_ID` / `IOS_BUNDLE_ID` | ||
| - Optional brand / theme / store URLs (`config/site.json` + `SITE_*` / `THEME_*` vars) | ||
|
|
||
| 3. Build and deploy (Firebase Hosting example): | ||
|
|
||
| ```bash | ||
| # .firebaserc → your Firebase project id | ||
| bun install | ||
| bun run build | ||
| npx -y firebase-tools@latest deploy --only hosting | ||
| ``` | ||
|
|
||
| Other platforms publish the same `out/` directory — see the [hosting overview](https://rutvik24.github.io/email-link-host/docs/hosting/overview). | ||
| Docker uses a pure Go image instead of `out/`. | ||
|
|
||
| 4. Confirm these URLs **render JSON in the browser** (not a file download): | ||
| - `https://YOUR_DOMAIN/.well-known/assetlinks.json` | ||
| - `https://YOUR_DOMAIN/.well-known/apple-app-site-association` | ||
|
|
||
| By default the host includes AASA coverage for Firebase Auth paths (`/__/auth/links*`). Full configuration reference: | ||
| [email-link-host configuration](https://rutvik24.github.io/email-link-host/docs/email-link-host/configuration). |
There was a problem hiding this comment.
Thanks for documenting a full custom-host flow - this will be useful for people that need to step up to doing links with branding.
However, please demote this so it is not Path A. After the default PROJECT_ID.firebaseapp.com path, a short optional subsection is enough focusing on the steps and then showing your projects as possible implementations (what files to publish, Content-Type: application/json, include /__/auth/links*). Please don't clone/deploy a specific third-party host as the recommended getting-started steps; that diverges from upstream style. Mentioning that community hosts/generators exist is fine.
| ## Path B — existing site: App Universal Links Helper | ||
|
|
||
| If the continue URL should live on a domain you already operate: | ||
|
|
||
| 1. Open [App Universal Links Helper](https://rutvik24.github.io/app-universal-links-helper/) (or run it locally from | ||
| [the repo](https://github.com/rutvik24/app-universal-links-helper)). | ||
| 2. Enter hosts, package name, SHA-256 fingerprints, Apple Team ID + bundle ID, and path rules (include | ||
| `/__/auth/links` / `pathPrefix` as needed for Auth links). | ||
| 3. Copy or download: | ||
| - `assetlinks.json` | ||
| - `apple-app-site-association` | ||
| - AndroidManifest `<intent-filter>` snippet | ||
| - Optional `adb` / `xcrun` / `curl` / `swcutil` test commands | ||
|
|
||
| 4. Publish the JSON files on your site at `/.well-known/…` with `Content-Type: application/json` (and no attachment | ||
| disposition). Paste the intent-filter into your app (see below). | ||
|
|
||
| The helper does **not** upload files for you — it only generates what you host. |
There was a problem hiding this comment.
Same ask as Path A: treat this as optional custom-domain material, not a primary path, and mention but don't promote a specific helper UI as the way to generate association files.
Suggested replacement for this whole subsection - official files only:
### Association files (custom domain only)
Publish these at the Hosting / custom domain (inline JSON, not a file download):
- `https://YOUR_DOMAIN/.well-known/assetlinks.json`
- `https://YOUR_DOMAIN/.well-known/apple-app-site-association`
`assetlinks.json` needs `package_name` + SHA-256 fingerprints. AASA needs your Apple Team ID + bundle ID and paths covering `/__/auth/links*`.On the default PROJECT_ID.firebaseapp.com path, Firebase serves these after SHA fingerprints are registered — no separate generator required.
| async function completeFromUrl(url) { | ||
| if (!url || !isSignInWithEmailLink(getAuth(), url)) { | ||
| return; | ||
| } | ||
|
|
||
| const email = await AsyncStorage.getItem(EMAIL_KEY); | ||
| if (!email) { | ||
| // Prompt the user to re-enter email (different device / cleared storage). | ||
| throw new Error('Enter the email that requested the sign-in link.'); | ||
| } | ||
|
|
||
| await signInWithEmailLink(getAuth(), email, url); | ||
| await AsyncStorage.removeItem(EMAIL_KEY); |
There was a problem hiding this comment.
Please don't throw when the stored email is missing - that's the normal cross-device case. Prompt for the email, then call signInWithEmailLink (you already describe that in §3; the snippet should match).
Suggested replacement for the missing-email branch:
const email = await AsyncStorage.getItem(EMAIL_KEY);
if (!email) {
// Same device: shouldn't happen. Other device / cleared storage:
// prompt the user to type the address the link was sent to, then continue.
return;
}Also wrap signInWithEmailLink in try/catch and surface error.code (expired/reused links show up as auth/invalid-action-code).
| ## 3. Different device / missing stored email | ||
|
|
||
| If the user opens the link on another device, ask them to type the email again, then call `signInWithEmailLink` with that | ||
| address and the incoming URL. Firebase requires the email to match the address the link was sent to. |
There was a problem hiding this comment.
Optional but useful - the upstream guides cover these and they're easy to miss:
- Link / re-auth:
EmailAuthProvider.credentialWithLink(email, url)thenlinkWithCredential/reauthenticateWithCredential(same URL handling as sign-in). - Email enumeration protection: projects created after 2023-09-15 should not use
fetchSignInMethodsForEmailfor identifier-first UI; see Firebase's note on the email-link pages.
| # Example app | ||
|
|
||
| End-to-end sample (React Native + Auth modular API + Android App Links + iOS Associated Domains + AsyncStorage): | ||
|
|
||
| - **Repository:** [rutvik24/firebase-email-link-host-demo](https://github.com/rutvik24/firebase-email-link-host-demo) | ||
| - Pairs with the hosted continue domain from [email-link-host](https://github.com/rutvik24/email-link-host) | ||
| ([live demo](https://fir-email-link-host.web.app)) | ||
|
|
||
| Useful files in the sample: | ||
|
|
||
| | Area | Location | | ||
| | ------------------------------------------------ | ------------------------------------------ | | ||
| | Continue URL / package IDs | `src/config/auth.ts` | | ||
| | Auth wrapper (`send` / `complete` / `isSignIn…`) | `src/services/firebaseAuth.ts` | | ||
| | `Linking` + UI state | `src/hooks/useEmailLinkAuth.ts` | | ||
| | Android intent-filters | `android/app/src/main/AndroidManifest.xml` | | ||
| | iOS Associated Domains | `ios/.../*.entitlements` | | ||
| | Universal Links → RN | `ios/.../AppDelegate.swift` | |
There was a problem hiding this comment.
Please don't make a specific third-party sample the “Example app” centerpiece - same style as other RNFB auth pages, which stay in-repo / official.
Suggested replacement: reduce it to one optional sentence at the end of Related, e.g. "Community sample apps exist that wire App Links + Universal Links + the modular Auth APIs." The in-page snippets + checklist should be the worked example.
| # Checklist | ||
|
|
||
| 1. Email/Password + Email link providers enabled. | ||
| 2. Continue URL domain is **Authorized**. | ||
| 3. Association files live at `/.well-known/` and return JSON inline. | ||
| 4. Android SHA fingerprints registered; intent-filter uses `autoVerify` and your host(s). | ||
| 5. iOS Associated Domains match the host(s); `RCTLinkingManager` receives Universal Links. | ||
| 6. App stores the email, sends the link with `handleCodeInApp: true`, then completes with `signInWithEmailLink`. | ||
| 7. Test on a **physical device** (App Links / Universal Links verification is unreliable or limited on many emulators/simulators). |
There was a problem hiding this comment.
Checklist is great. Please add a Troubleshooting heading after it - this is the part people (and agents these days...) actually search for. Suggested rows:
| Symptom | What to check |
|---|---|
DYNAMIC_LINK_NOT_ACTIVATED / email still uses *.page.link |
Run Admin SDK mobileLinksConfig.domain: 'HOSTING_DOMAIN'. Do not set dynamicLinkDomain. |
| Link opens the browser, not the app | Physical device; AASA/assetlinks 200 + application/json; intent-filter / applinks: host is the Hosting link domain (PROJECT_ID.firebaseapp.com/__/auth/links), not only the continue URL. |
| iOS: Firebase email URL doesn't open the app, but pasting the Hosting URL does | Custom SMTP (e.g. SendGrid) wrapping the Auth URL in a redirect. Universal Links do not follow that redirect into the app. Use Firebase's default email sender, or a custom SMTP that does not wrap the destination URL. Reported in #8405. |
auth/invalid-action-code |
Link expired or already used; send a new one. |
auth/internal-error |
Check native logs (adb logcat / Console.app on a real device), not just the JS error. |
| # Related | ||
|
|
||
| - [Authentication usage](/auth/usage) | ||
| - [email-link-host documentation](https://rutvik24.github.io/email-link-host/) | ||
| - [App Universal Links Helper](https://rutvik24.github.io/app-universal-links-helper/) | ||
| - Firebase: [Android email link](https://firebase.google.com/docs/auth/android/email-link-auth) · | ||
| [iOS email link](https://firebase.google.com/docs/auth/ios/email-link-auth) |
There was a problem hiding this comment.
Please lead Related with official docs only (same as phone/social pages):
# Related
- [Authentication usage](/auth/usage)
- Firebase: [Android email link](https://firebase.google.com/docs/auth/android/email-link-auth) · [iOS email link](https://firebase.google.com/docs/auth/ios/email-link-auth) · [Migrate off Dynamic Links](https://firebase.google.com/docs/auth/android/email-link-migration)If you want a community-tools mention, one trailing sentence is enough — no extra doc/demo/helper links in this list.
|
Thank you for this - it's a substantial, carefully researched guide on a genuinely hard flow, and filling this gap in the RNFB docs is really appreciated. Requesting a structural pass so the page matches the style of the upstream Firebase email-link docs: official default / non-branded path first (the way people will first integrate), then optional custom-domain / branded hosting. Community tools can be mentioned where they help, but please don't promote specific projects - same approach as our other auth pages (phone, social), which link official Firebase docs and leave third-party helpers as optional asides. Inline comments have suggested copy where that might reduce back-and-forth. None of this is a knock on the work you've already done; it's aligning the page with how we document the rest of Auth and I think this will help a lot of people in one of the more difficult areas of cross-platform mobile dev |
Summary
Test plan
yarn lint:markdownyarn lint:spellcheck