Files
supabase/apps/docs/content/guides/auth/auth-email-passwordless.mdx
T
Miranda Limonczenko 7ce4ee53ae chore(docs) Retire supa-mdx-lint (#50602)
Closes
[DOCS-1289](https://linear.app/supabase/issue/DOCS-1289/get-the-linter-to-fix-what-it-flags-or-retirereplace-the-linter)

Stacked on #50600, which points contributors at the authoring skills.
Merge that one first.

## Problem

Contributors experienced friction with the linter. They felt nickle and
dimed for tiny nits and felt detracted from the work itself. PRs would
become noisy with tiny one-word suggestions.

Additionally, our homegrown linter is not very intelligent, causing
frequent overrides.

## Solution

This removes the linter entirely in favor of directing contributors to
use SKILLS instead.

The removal entails...

- **CI.** Delete the three `docs_lint` workflows: the PR check, the
external-PR comment companion, and the nightly `--fix` bot. Drop the
stale `zizmor.yml` ignore entry for the deleted workflow.
- **Tooling.** Delete `supa-mdx-lint.config.toml` and the 14 rule files.
Drop the `lint:mdx` script and the `@supabase/supa-mdx-lint` dependency
from docs, learn, and ui-library, and regenerate the lockfile.
- **Content.** Remove the 181 directives. A separate commit carries
Prettier's reformatting of the tables and blank lines those comments had
suppressed, so the deletion commit stays readable. No prose changes.
- **Style guide.** The word list states each rule directly instead of
describing what the linter flagged. Every term survives, including the
phrase groups that mirrored `Rule004ExcludeWords`.
- **Skills.** `write-the-docs`, `edit-the-docs`, and `review-the-docs`
drop `pnpm lint:mdx` from their self-review commands and check the word
list directly. `ask-the-docs`'s CI reference drops both workflows.

## Manual testing

1. Run `git grep -i supa-mdx-lint -- . ':!pnpm-lock.yaml'`. No matches.
2. Run `pnpm install --frozen-lockfile --lockfile-only`. It passes, so
the lockfile matches the three trimmed manifests.
3. Run `git diff master...HEAD --name-only --diff-filter=ACMR | grep -E
'\.(md|mdx)$' | xargs npx prettier --config prettier.config.mjs
--check`. All changed markdown passes.
4. Open the [reformatted filter
table](https://docs-git-docs-retire-mdx-linter-supabase.vercel.app/docs/guides/observability/logs#filter-events)
on the preview and compare it with
[production](https://supabase.com/docs/guides/observability/logs#filter-events).
The table renders the same.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Documentation guidance now uses manual prose and terminology review
with the shared word list.
* Clarified storage configuration and common Realtime channel mistakes.
* Improved table formatting, text wrapping, and selected reference
links.
  * Updated documentation authoring and review guidance.

* **Chores**
* Retired automated MDX linting from workflows and local validation
commands.
* Removed lint-suppression markers throughout documentation without
changing instructions.
  * Added targeted documentation review guidance for pull requests.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-09-22 10:00:41 -07:00

399 lines
11 KiB
Plaintext

---
title: 'Passwordless email sign-in'
subtitle: 'Email sign-in using Magic Links or One-Time Passwords (OTPs)'
---
Supabase Auth provides several passwordless sign-in methods. Passwordless sign-in allows users to sign in without a password, by clicking a confirmation link or entering a verification code.
Passwordless sign-in can:
- Improve the user experience by not requiring users to create and remember a password
- Increase security by reducing the risk of password-related security breaches
- Reduce support burden of dealing with password resets and other password-related flows
Supabase Auth offers two passwordless sign-in methods that use the user's email address:
- [Magic Link](#with-magic-link)
- [OTP](#with-otp)
## With Magic Link
Magic Links are a form of passwordless sign-in where users click on a link sent to their email address to sign in to their accounts. Magic Links only work with email addresses and are one-time use only.
### Enabling Magic Link
Email authentication methods, including Magic Links, are enabled by default.
Configure the Site URL and any additional redirect URLs. These are the only URLs that are allowed as redirect destinations after the user clicks a Magic Link. You can change the URLs on the [URL Configuration page](/dashboard/project/_/auth/url-configuration) for hosted projects, in the `config.toml` [file](/docs/guides/local-development/cli/config#auth.additional_redirect_urls) for local development, or in the `.env` configuration file for [self-hosted Supabase](/docs/guides/self-hosting/docker).
By default, a user can only request a magic link once every <SharedData data="config">auth.rate_limits.magic_link.period</SharedData> and they expire after <SharedData data="config">auth.rate_limits.magic_link.validity</SharedData>.
### Signing in with Magic Link
Call the "sign in with OTP" method from the client library.
Though the method is labelled "OTP", it sends a Magic Link by default. The two methods differ only in the content of the confirmation email sent to the user.
If the user hasn't signed up yet, they are automatically signed up by default. To prevent this, set the `shouldCreateUser` option to `false`.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
// ---cut---
async function signInWithEmail() {
const { data, error } = await supabase.auth.signInWithOtp({
email: 'valid.email@supabase.io',
options: {
// set this to false if you do not want the user to be automatically signed up
shouldCreateUser: false,
emailRedirectTo: 'https://example.com/welcome',
},
})
}
```
</TabPanel>
<TabPanel id="react-native" label="Expo React Native">
```ts
import { makeRedirectUri } from 'expo-auth-session'
const redirectTo = makeRedirectUri()
const { error } = await supabase.auth.signInWithOtp({
email: 'valid.email@supabase.io',
options: {
emailRedirectTo: redirectTo,
},
})
```
Read the [Deep Linking Documentation](/docs/guides/auth/native-mobile-deep-linking) to learn how to handle deep linking.
</TabPanel>
<$Show if="sdk:dart">
<TabPanel id="dart" label="Dart">
```dart
Future<void> signInWithEmail() async {
await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io');
}
```
</TabPanel>
</$Show>
<$Show if="sdk:swift">
<TabPanel id="swift" label="Swift">
```swift
try await supabase.auth.signInWithOTP(
email: "valid.email@supabase.io",
redirectTo: URL(string: "https://example.com/welcome"),
// set this to false if you do not want the user to be automatically signed up
shouldCreateUser: false
)
```
</TabPanel>
</$Show>
<$Show if="sdk:kotlin">
<TabPanel id="kotlin" label="Kotlin">
```kotlin
suspend fun signInWithEmail() {
supabase.auth.signInWith(OTP) {
email = "valid.email@supabase.io"
}
}
```
</TabPanel>
</$Show>
<$Show if="sdk:python">
<TabPanel id="python" label="Python">
```python
response = supabase.auth.sign_in_with_otp({
'email': 'valid.email@supabase.io',
'options': {
# set this to false if you do not want the user to be automatically signed up
'should_create_user': False,
'email_redirect_to': 'https://example.com/welcome',
},
})
```
</TabPanel>
</$Show>
<$Show if="sdk:csharp">
<TabPanel id="csharp" label="C#">
```c#
var options = new SignInOptions { RedirectTo = "https://example.com/welcome" };
var didSendMagicLink = await supabase.Auth.SendMagicLink("valid.email@supabase.io", options);
```
</TabPanel>
</$Show>
</Tabs>
That's it for the implicit flow.
If you're using PKCE flow, edit the Magic Link [email template](/docs/guides/auth/auth-email-templates) to send a token hash:
```html
<h2>Sign in to your account</h2>
<p>Use this link to sign in to your account:</p>
<p><a href="{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email">Sign in</a></p>
```
At the `/auth/confirm` endpoint, exchange the hash for the session:
```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
// ---cut---
const { error } = await supabase.auth.verifyOtp({
token_hash: 'hash',
type: 'email',
})
```
## With OTP
Email one-time passwords (OTP) are a form of passwordless sign-in where users key in a six-digit code sent to their email address to sign in to their accounts.
### Enabling email OTP
Email authentication methods, including Email OTPs, are enabled by default.
Email OTPs share an implementation with Magic Links. To send an OTP instead of a Magic Link, alter the **Magic Link** [email template](/dashboard/project/_/auth/templates/magic-link-or-otp). Refer to the [Email Templates guide](/docs/guides/auth/auth-email-templates) for more information.
Modify the template to include the `{{ .Token }}` variable, for example:
```html
<h2>One time login code</h2>
<p>Please enter this code: {{ .Token }}</p>
```
By default, a user can only request an OTP once every <SharedData data="config">auth.rate_limits.otp.period</SharedData>, and they expire after <SharedData data="config">auth.rate_limits.otp.validity</SharedData>. This is configurable via **Authentication > Sign In / Providers > Auth Providers > Email > Email OTP expiration**. An expiry duration of more than 86,400 seconds (one day) is strongly discouraged and can only be set via the [Management API](/docs/reference/api/v1-update-auth-service-config). Make sure to read the [security recommendations](/docs/guides/deployment/going-into-prod#security) before going into production.
<Admonition type="caution">
The **Email OTP Expiration** setting also governs the validity of Magic Links and other email links, including confirmation, password recovery, email change, and [invitation](/docs/guides/auth/users#inviting-users) links.
</Admonition>
### Signing in with email OTP
#### Step 1: Send the user an OTP code
Get the user's email and call the "sign in with OTP" method from your client library.
If the user hasn't signed up yet, they are automatically signed up by default. To prevent this, set the `shouldCreateUser` option to `false`.
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
// ---cut---
const { data, error } = await supabase.auth.signInWithOtp({
email: 'valid.email@supabase.io',
options: {
// set this to false if you do not want the user to be automatically signed up
shouldCreateUser: false,
},
})
```
</TabPanel>
<$Show if="sdk:dart">
<TabPanel id="dart" label="Dart">
```dart
Future<void> signInWithEmailOtp() async {
await supabase.auth.signInWithOtp(email: 'valid.email@supabase.io');
}
```
</TabPanel>
</$Show>
<$Show if="sdk:swift">
<TabPanel id="swift" label="Swift">
```swift
try await supabase.auth.signInWithOTP(
email: "valid.email@supabase.io",
// set this to false if you do not want the user to be automatically signed up
shouldCreateUser: false
)
```
</TabPanel>
</$Show>
<$Show if="sdk:kotlin">
<TabPanel id="kotlin" label="Kotlin">
```kotlin
suspend fun signInWithEmailOtp() {
supabase.auth.signInWith(OTP) {
email = "valid.email@supabase.io"
}
}
```
</TabPanel>
</$Show>
<$Show if="sdk:python">
<TabPanel id="python" label="Python">
```python
response = supabase.auth.sign_in_with_otp({
'email': 'valid.email@supabase.io',
'options': {
# set this to false if you do not want the user to be automatically signed up
'should_create_user': False,
},
})
```
</TabPanel>
</$Show>
<$Show if="sdk:csharp">
<TabPanel id="csharp" label="C#">
```c#
await supabase.Auth.SendMagicLink("valid.email@supabase.io");
```
</TabPanel>
</$Show>
</Tabs>
If the request is successful, you receive a response with `error: null` and a `data` object where both `user` and `session` are null. Let the user know to check their email inbox.
```json
{
"data": {
"user": null,
"session": null
},
"error": null
}
```
#### Step 2: Verify the OTP to create a session
Provide an input field for the user to enter their one-time code.
Call the "verify OTP" method from your client library with the user's email address, the code, and a type of `email`:
<Tabs
scrollable
size="small"
type="underlined"
defaultActiveId="js"
queryGroup="language"
>
<TabPanel id="js" label="JavaScript">
```js
import { createClient } from '@supabase/supabase-js'
const supabase = createClient('https://your-project-id.supabase.co', 'sb_publishable_...')
// ---cut---
const {
data: { session },
error,
} = await supabase.auth.verifyOtp({
email: 'email@example.com',
token: '123456',
type: 'email',
})
```
</TabPanel>
<$Show if="sdk:swift">
<TabPanel id="swift" label="Swift">
```swift
try await supabase.auth.verifyOTP(
email: email,
token: "123456",
type: .email
)
```
</TabPanel>
</$Show>
<$Show if="sdk:kotlin">
<TabPanel id="kotlin" label="Kotlin">
```kotlin
supabase.auth.verifyEmailOtp(type = OtpType.Email.EMAIL, email = "email", token = "151345")
```
</TabPanel>
</$Show>
<$Show if="sdk:python">
<TabPanel id="python" label="Python">
```python
response = supabase.auth.verify_otp({
'email': email,
'token': '123456',
'type': 'email',
})
```
</TabPanel>
</$Show>
<$Show if="sdk:csharp">
<TabPanel id="csharp" label="C#">
```c#
var session = await supabase.Auth.VerifyOTP("email@example.com", "123456", EmailOtpType.Email);
```
</TabPanel>
</$Show>
</Tabs>
If successful, the user is now signed in, and you receive a valid session that looks like:
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhdXRoZW50aWNhdGVkIiwiZXhwIjoxNjI3MjkxNTc3LCJzdWIiOiJmYTA2NTQ1Zi1kYmI1LTQxY2EtYjk1NC1kOGUyOTg4YzcxOTEiLCJlbWFpbCI6IiIsInBob25lIjoiNjU4NzUyMjAyOSIsImFwcF9tZXRhZGF0YSI6eyJwcm92aWRlciI6InBob25lIn0sInVzZXJfbWV0YWRhdGEiOnt9LCJyb2xlIjoiYXV0aGVudGljYXRlZCJ9.1BqRi0NbS_yr1f6hnr4q3s1ylMR3c1vkiJ4e_N55dhM",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "LSp8LglPPvf0DxGMSj-vaQ",
"user": {...}
}
```