mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
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 -->
399 lines
11 KiB
Plaintext
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": {...}
|
|
}
|
|
```
|