@netlify/identity 0.1.1 → 0.3.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -4,13 +4,38 @@ A lightweight, no-config headless authentication library for projects using Netl
4
4
 
5
5
  > **Status:** Beta. The API may change before 1.0.
6
6
 
7
- For a pre-built login widget, see [netlify-identity-widget](https://github.com/netlify/netlify-identity-widget).
8
-
9
7
  **Prerequisites:**
10
8
 
11
9
  - [Netlify Identity](https://docs.netlify.com/security/secure-access-to-sites/identity/) must be enabled on your Netlify project
12
10
  - For local development, use [`netlify dev`](https://docs.netlify.com/cli/local-development/) so the Identity endpoint is available
13
11
 
12
+ ### How this library relates to other Netlify auth packages
13
+
14
+ | Package | What it is | When to use it |
15
+ | ------------------------------------------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------ |
16
+ | **`@netlify/identity`** (this library) | Headless TypeScript API for browser and server | You want full control over your auth UI and need server-side auth (SSR, Netlify Functions) |
17
+ | [`netlify-identity-widget`](https://github.com/netlify/netlify-identity-widget) | Pre-built login/signup modal (HTML + CSS) | You want a drop-in UI component with no custom design |
18
+ | [`gotrue-js`](https://github.com/netlify/gotrue-js) | Low-level GoTrue HTTP client (browser only) | You're building your own auth wrapper and need direct API access |
19
+
20
+ This library wraps `gotrue-js` in the browser and calls the GoTrue HTTP API directly on the server. It provides a unified API that works in both contexts, handles cookie management, and normalizes the user object. You do not need to install `gotrue-js` or the widget separately.
21
+
22
+ ## Table of contents
23
+
24
+ - [Installation](#installation)
25
+ - [Quick start](#quick-start)
26
+ - [API](#api)
27
+ - [Functions](#functions) -- `getUser`, `login`, `signup`, `logout`, `oauthLogin`, `handleAuthCallback`, `onAuthChange`, `hydrateSession`, and more
28
+ - [Types](#types) -- `User`, `AuthEvent`, `CallbackResult`, `Settings`, etc.
29
+ - [Errors](#errors) -- `AuthError`, `MissingIdentityError`
30
+ - [Framework integration](#framework-integration) -- Next.js, Remix, TanStack Start, Astro, SvelteKit
31
+ - [Guides](#guides)
32
+ - [React `useAuth` hook](#react-useauth-hook)
33
+ - [Listening for auth changes](#listening-for-auth-changes)
34
+ - [OAuth login](#oauth-login)
35
+ - [Password recovery](#password-recovery)
36
+ - [Invite acceptance](#invite-acceptance)
37
+ - [Session lifetime](#session-lifetime)
38
+
14
39
  ## Installation
15
40
 
16
41
  ```bash
@@ -19,18 +44,20 @@ npm install @netlify/identity
19
44
 
20
45
  ## Quick start
21
46
 
22
- ### Browser
47
+ ### Log in (browser)
23
48
 
24
49
  ```ts
25
- import { getUser } from '@netlify/identity'
50
+ import { login, getUser } from '@netlify/identity'
26
51
 
27
- const user = getUser()
28
- if (user) {
29
- console.log(`Hello, ${user.name}`)
30
- }
52
+ // Log in
53
+ const user = await login('jane@example.com', 'password123')
54
+ console.log(`Hello, ${user.name}`)
55
+
56
+ // Later, check auth state synchronously
57
+ const currentUser = getUser()
31
58
  ```
32
59
 
33
- ### Netlify Function
60
+ ### Protect a Netlify Function
34
61
 
35
62
  ```ts
36
63
  import { getUser } from '@netlify/identity'
@@ -43,7 +70,7 @@ export default async (req: Request, context: Context) => {
43
70
  }
44
71
  ```
45
72
 
46
- ### Edge Function
73
+ ### Protect an Edge Function
47
74
 
48
75
  ```ts
49
76
  import { getUser } from '@netlify/identity'
@@ -66,7 +93,9 @@ export default async (req: Request, context: Context) => {
66
93
  getUser(): User | null
67
94
  ```
68
95
 
69
- Returns the current authenticated user, or `null` if not logged in. Synchronous, never throws.
96
+ Returns the current authenticated user, or `null` if not logged in. Synchronous. Never throws.
97
+
98
+ > **Next.js note:** Calling `getUser()` in a Server Component opts the page into [dynamic rendering](https://nextjs.org/docs/app/building-your-application/rendering/server-components#dynamic-rendering) because it reads cookies. This is expected and correct for authenticated pages. Next.js handles the internal dynamic rendering signal automatically.
70
99
 
71
100
  #### `isAuthenticated`
72
101
 
@@ -74,7 +103,7 @@ Returns the current authenticated user, or `null` if not logged in. Synchronous,
74
103
  isAuthenticated(): boolean
75
104
  ```
76
105
 
77
- Returns `true` if a user is currently authenticated. Equivalent to `getUser() !== null`.
106
+ Returns `true` if a user is currently authenticated. Equivalent to `getUser() !== null`. Never throws.
78
107
 
79
108
  #### `getIdentityConfig`
80
109
 
@@ -90,7 +119,220 @@ Returns the Identity endpoint URL (and operator token on the server), or `null`
90
119
  getSettings(): Promise<Settings>
91
120
  ```
92
121
 
93
- Fetches your project's Identity settings (enabled providers, autoconfirm, signup disabled). Throws `MissingIdentityError` if not configured; throws `AuthError` if the endpoint is unreachable.
122
+ Fetches your project's Identity settings (enabled providers, autoconfirm, signup disabled).
123
+
124
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the endpoint is unreachable.
125
+
126
+ #### `login`
127
+
128
+ ```ts
129
+ login(email: string, password: string): Promise<User>
130
+ ```
131
+
132
+ Logs in with email and password. Works in both browser and server contexts.
133
+
134
+ In the browser, uses gotrue-js and emits a `'login'` event. On the server (Netlify Functions, Edge Functions), calls the GoTrue HTTP API directly and sets the `nf_jwt` cookie via the Netlify runtime.
135
+
136
+ **Throws:** `AuthError` on invalid credentials or network failure. In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
137
+
138
+ #### `signup`
139
+
140
+ ```ts
141
+ signup(email: string, password: string, data?: SignupData): Promise<User>
142
+ ```
143
+
144
+ Creates a new account. Works in both browser and server contexts.
145
+
146
+ If autoconfirm is enabled in your Identity settings, the user is logged in immediately: cookies are set and a `'login'` event is emitted. If autoconfirm is **disabled** (the default), the user receives a confirmation email and must click the link before they can log in. In that case, no cookies are set and no auth event is emitted.
147
+
148
+ The optional `data` parameter sets user metadata (e.g., `{ full_name: 'Jane Doe' }`), stored in the user's `user_metadata` field.
149
+
150
+ **Throws:** `AuthError` on failure (e.g., email already registered, signup disabled). In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
151
+
152
+ #### `logout`
153
+
154
+ ```ts
155
+ logout(): Promise<void>
156
+ ```
157
+
158
+ Logs out the current user and clears the session. Works in both browser and server contexts.
159
+
160
+ In the browser, uses gotrue-js and emits a `'logout'` event. On the server, calls GoTrue's `/logout` endpoint with the JWT from the `nf_jwt` cookie, then deletes the cookie. Auth cookies are always cleared, even if the GoTrue call fails.
161
+
162
+ **Throws:** In the browser, `MissingIdentityError` if Identity is not configured. On the server, `AuthError` if the Netlify Functions runtime is not available.
163
+
164
+ #### `oauthLogin`
165
+
166
+ ```ts
167
+ oauthLogin(provider: string): never
168
+ ```
169
+
170
+ Redirects to an OAuth provider. The page navigates away, so this function never returns normally. Browser only.
171
+
172
+ The `provider` argument should be one of the `AuthProvider` values: `'google'`, `'github'`, `'gitlab'`, `'bitbucket'`, `'facebook'`, or `'saml'`.
173
+
174
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if called on the server.
175
+
176
+ #### `handleAuthCallback`
177
+
178
+ ```ts
179
+ handleAuthCallback(): Promise<CallbackResult | null>
180
+ ```
181
+
182
+ Processes the URL hash after an OAuth redirect, email confirmation, password recovery, invite acceptance, or email change. Call on page load. Returns `null` if the hash contains no auth parameters. Browser only.
183
+
184
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if token exchange fails.
185
+
186
+ #### `onAuthChange`
187
+
188
+ ```ts
189
+ onAuthChange(callback: AuthCallback): () => void
190
+ ```
191
+
192
+ Subscribes to auth state changes (login, logout, token refresh, user updates, and recovery). Returns an unsubscribe function. Also fires on cross-tab session changes. No-op on the server. The `'recovery'` event fires when `handleAuthCallback()` processes a password recovery token; listen for it to redirect users to a password reset form.
193
+
194
+ #### `hydrateSession`
195
+
196
+ ```ts
197
+ hydrateSession(): Promise<User | null>
198
+ ```
199
+
200
+ Bootstraps the browser-side gotrue-js session from server-set auth cookies (`nf_jwt`, `nf_refresh`). Returns the hydrated `User`, or `null` if no auth cookies are present. No-op on the server.
201
+
202
+ **When to use:** After a server-side login (e.g., via a Netlify Function or Server Action), the `nf_jwt` cookie is set but gotrue-js has no browser session yet. `getUser()` works immediately (it decodes the cookie), but account operations like `updateUser()` or `verifyEmailChange()` require a live gotrue-js session. Call `hydrateSession()` once on page load to bridge this gap.
203
+
204
+ If a gotrue-js session already exists (e.g., from a browser-side login), this is a no-op and returns the existing user.
205
+
206
+ ```ts
207
+ import { hydrateSession, updateUser } from '@netlify/identity'
208
+
209
+ // On page load, hydrate the session from server-set cookies
210
+ await hydrateSession()
211
+
212
+ // Now browser account operations work
213
+ await updateUser({ data: { full_name: 'Jane Doe' } })
214
+ ```
215
+
216
+ #### `requestPasswordRecovery`
217
+
218
+ ```ts
219
+ requestPasswordRecovery(email: string): Promise<void>
220
+ ```
221
+
222
+ Sends a password recovery email to the given address.
223
+
224
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` on network failure.
225
+
226
+ #### `confirmEmail`
227
+
228
+ ```ts
229
+ confirmEmail(token: string): Promise<User>
230
+ ```
231
+
232
+ Confirms an email address using the token from a confirmation email. Logs the user in on success.
233
+
234
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid or expired.
235
+
236
+ #### `acceptInvite`
237
+
238
+ ```ts
239
+ acceptInvite(token: string, password: string): Promise<User>
240
+ ```
241
+
242
+ Accepts an invite token and sets a password for the new account. Logs the user in on success.
243
+
244
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid or expired.
245
+
246
+ #### `verifyEmailChange`
247
+
248
+ ```ts
249
+ verifyEmailChange(token: string): Promise<User>
250
+ ```
251
+
252
+ Verifies an email change using the token from a verification email.
253
+
254
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid.
255
+
256
+ #### `recoverPassword`
257
+
258
+ ```ts
259
+ recoverPassword(token: string, newPassword: string): Promise<User>
260
+ ```
261
+
262
+ Redeems a recovery token and sets a new password. Logs the user in on success.
263
+
264
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if the token is invalid or expired.
265
+
266
+ #### `updateUser`
267
+
268
+ ```ts
269
+ updateUser(updates: UserUpdates): Promise<User>
270
+ ```
271
+
272
+ Updates the current user's metadata or credentials. Requires an active session. Pass `email` or `password` to change credentials, or `data` to update user metadata (e.g., `{ data: { full_name: 'New Name' } }`).
273
+
274
+ **Throws:** `MissingIdentityError` if Identity is not configured. `AuthError` if no user is logged in, or the update fails.
275
+
276
+ ### Admin Operations
277
+
278
+ The `admin` namespace provides user management functions for administrators. These work in two contexts:
279
+
280
+ - **Server:** Uses the operator token from the Netlify runtime for full admin access. No logged-in user required.
281
+ - **Browser:** Uses the logged-in user's JWT via gotrue-js. The user must have an admin role.
282
+
283
+ ```ts
284
+ import { admin } from '@netlify/identity'
285
+ ```
286
+
287
+ #### `admin.listUsers`
288
+
289
+ ```ts
290
+ admin.listUsers(options?: { page?: number; perPage?: number }): Promise<User[]>
291
+ ```
292
+
293
+ Lists all users. Pagination options are supported on the server; they are ignored in the browser (gotrue-js does not support pagination for this method).
294
+
295
+ **Throws:** `AuthError` if the operator token is missing (server) or no user is logged in (browser).
296
+
297
+ #### `admin.getUser`
298
+
299
+ ```ts
300
+ admin.getUser(userId: string): Promise<User>
301
+ ```
302
+
303
+ Gets a single user by ID.
304
+
305
+ **Throws:** `AuthError` if the user is not found, the operator token is missing (server), or no user is logged in (browser).
306
+
307
+ #### `admin.createUser`
308
+
309
+ ```ts
310
+ admin.createUser(params: { email: string; password: string; data?: Record<string, unknown> }): Promise<User>
311
+ ```
312
+
313
+ Creates a new user. The user is auto-confirmed. Optional `data` is spread into the request body as additional attributes.
314
+
315
+ **Throws:** `AuthError` on failure (e.g., email already exists).
316
+
317
+ #### `admin.updateUser`
318
+
319
+ ```ts
320
+ admin.updateUser(userId: string, attributes: Record<string, unknown>): Promise<User>
321
+ ```
322
+
323
+ Updates an existing user by ID. Pass any attributes to change (e.g., `{ email: 'new@example.com' }`).
324
+
325
+ **Throws:** `AuthError` if the user is not found or the update fails.
326
+
327
+ #### `admin.deleteUser`
328
+
329
+ ```ts
330
+ admin.deleteUser(userId: string): Promise<void>
331
+ ```
332
+
333
+ Deletes a user by ID.
334
+
335
+ **Throws:** `AuthError` if the user is not found or the deletion fails.
94
336
 
95
337
  ### Types
96
338
 
@@ -136,6 +378,27 @@ interface IdentityConfig {
136
378
  type AuthProvider = 'google' | 'github' | 'gitlab' | 'bitbucket' | 'facebook' | 'saml' | 'email'
137
379
  ```
138
380
 
381
+ #### `UserUpdates`
382
+
383
+ ```ts
384
+ interface UserUpdates {
385
+ email?: string
386
+ password?: string
387
+ data?: Record<string, unknown>
388
+ [key: string]: unknown
389
+ }
390
+ ```
391
+
392
+ Fields accepted by `updateUser()`. All fields are optional.
393
+
394
+ #### `SignupData`
395
+
396
+ ```ts
397
+ type SignupData = Record<string, unknown>
398
+ ```
399
+
400
+ User metadata passed as the third argument to `signup()`. Stored in the user's `user_metadata` field.
401
+
139
402
  #### `AppMetadata`
140
403
 
141
404
  ```ts
@@ -146,6 +409,46 @@ interface AppMetadata {
146
409
  }
147
410
  ```
148
411
 
412
+ #### `AUTH_EVENTS`
413
+
414
+ ```ts
415
+ const AUTH_EVENTS: {
416
+ LOGIN: 'login'
417
+ LOGOUT: 'logout'
418
+ TOKEN_REFRESH: 'token_refresh'
419
+ USER_UPDATED: 'user_updated'
420
+ RECOVERY: 'recovery'
421
+ }
422
+ ```
423
+
424
+ Constants for auth event names. Use these instead of string literals for type safety and autocomplete.
425
+
426
+ #### `AuthEvent`
427
+
428
+ ```ts
429
+ type AuthEvent = 'login' | 'logout' | 'token_refresh' | 'user_updated' | 'recovery'
430
+ ```
431
+
432
+ #### `AuthCallback`
433
+
434
+ ```ts
435
+ type AuthCallback = (event: AuthEvent, user: User | null) => void
436
+ ```
437
+
438
+ #### `CallbackResult`
439
+
440
+ ```ts
441
+ interface CallbackResult {
442
+ type: 'oauth' | 'confirmation' | 'recovery' | 'invite' | 'email_change'
443
+ user: User | null
444
+ token?: string
445
+ }
446
+ ```
447
+
448
+ The `token` field is only present for `invite` callbacks, where the user hasn't set a password yet. Pass `token` to `acceptInvite(token, password)` to finish.
449
+
450
+ For all other types (`oauth`, `confirmation`, `recovery`, `email_change`), the user is logged in directly and `token` is not set.
451
+
149
452
  ### Errors
150
453
 
151
454
  #### `AuthError`
@@ -165,6 +468,469 @@ class MissingIdentityError extends Error {}
165
468
 
166
469
  Thrown when Identity is not configured in the current environment.
167
470
 
471
+ ## Framework integration
472
+
473
+ ### Recommended pattern for SSR frameworks
474
+
475
+ For SSR frameworks (Next.js, Remix, Astro, TanStack Start), the recommended pattern is:
476
+
477
+ - **Browser-side** for auth mutations: `login()`, `signup()`, `logout()`, `oauthLogin()`
478
+ - **Server-side** for reading auth state: `getUser()`, `getSettings()`, `getIdentityConfig()`
479
+
480
+ Browser-side auth mutations call the GoTrue API directly from the browser, set the `nf_jwt` cookie and gotrue-js localStorage, and emit `onAuthChange` events. This keeps the client UI in sync immediately. Server-side reads work because the cookie is sent with every request.
481
+
482
+ The library also supports server-side mutations (`login()`, `signup()`, `logout()` inside Netlify Functions), but these require the Netlify Functions runtime to set cookies. After a server-side mutation, you need a full page navigation so the browser sends the new cookie.
483
+
484
+ ### Next.js (App Router)
485
+
486
+ **Server Actions return results; the client handles navigation:**
487
+
488
+ ```tsx
489
+ // app/actions.ts
490
+ 'use server'
491
+ import { login, logout } from '@netlify/identity'
492
+
493
+ export async function loginAction(formData: FormData) {
494
+ const email = formData.get('email') as string
495
+ const password = formData.get('password') as string
496
+ await login(email, password)
497
+ return { success: true }
498
+ }
499
+
500
+ export async function logoutAction() {
501
+ await logout()
502
+ return { success: true }
503
+ }
504
+ ```
505
+
506
+ ```tsx
507
+ // app/login/page.tsx
508
+ 'use client'
509
+ import { loginAction } from '../actions'
510
+
511
+ export default function LoginPage() {
512
+ async function handleSubmit(formData: FormData) {
513
+ const result = await loginAction(formData)
514
+ if (result.success) {
515
+ window.location.href = '/dashboard' // full page load
516
+ }
517
+ }
518
+
519
+ return <form action={handleSubmit}>...</form>
520
+ }
521
+ ```
522
+
523
+ ```tsx
524
+ // app/dashboard/page.tsx
525
+ import { getUser } from '@netlify/identity'
526
+ import { redirect } from 'next/navigation'
527
+
528
+ export default function Dashboard() {
529
+ const user = getUser()
530
+ if (!user) redirect('/login')
531
+
532
+ return <h1>Hello, {user.email}</h1>
533
+ }
534
+ ```
535
+
536
+ Use `window.location.href` instead of Next.js `redirect()` after server-side auth mutations. Next.js `redirect()` triggers a soft navigation via the Router, which may not include the newly-set auth cookie. A full page load ensures the cookie is sent and the server sees the updated auth state. Reading auth state with `getUser()` in Server Components works normally, and `redirect()` is fine for auth gates (where no cookie was just set).
537
+
538
+ ### Remix
539
+
540
+ **Login with Action (server-side pattern):**
541
+
542
+ ```tsx
543
+ // app/routes/login.tsx
544
+ import { login } from '@netlify/identity'
545
+ import { redirect, json } from '@remix-run/node'
546
+ import type { ActionFunctionArgs } from '@remix-run/node'
547
+
548
+ export async function action({ request }: ActionFunctionArgs) {
549
+ const formData = await request.formData()
550
+ const email = formData.get('email') as string
551
+ const password = formData.get('password') as string
552
+
553
+ try {
554
+ await login(email, password)
555
+ return redirect('/dashboard')
556
+ } catch (error) {
557
+ return json({ error: (error as Error).message }, { status: 400 })
558
+ }
559
+ }
560
+ ```
561
+
562
+ ```tsx
563
+ // app/routes/dashboard.tsx
564
+ import { getUser } from '@netlify/identity'
565
+ import { redirect } from '@remix-run/node'
566
+
567
+ export async function loader() {
568
+ const user = getUser()
569
+ if (!user) return redirect('/login')
570
+ return { user }
571
+ }
572
+ ```
573
+
574
+ Remix `redirect()` works after server-side `login()` because Remix actions return real HTTP responses. The browser receives a 302 with the `Set-Cookie` header already applied, so the next request includes the auth cookie. This is different from Next.js, where `redirect()` in a Server Action triggers a client-side (soft) navigation that may not include newly-set cookies.
575
+
576
+ ### TanStack Start
577
+
578
+ **Login from the browser (recommended):**
579
+
580
+ ```tsx
581
+ // app/server/auth.ts - server functions for reads only
582
+ import { createServerFn } from '@tanstack/react-start'
583
+ import { getUser } from '@netlify/identity'
584
+
585
+ export const getServerUser = createServerFn({ method: 'GET' }).handler(async () => {
586
+ const user = getUser()
587
+ return user ?? null
588
+ })
589
+ ```
590
+
591
+ ```tsx
592
+ // app/routes/login.tsx - browser-side auth for mutations
593
+ import { login, signup, onAuthChange } from '@netlify/identity'
594
+ import { getServerUser } from '~/server/auth'
595
+
596
+ export const Route = createFileRoute('/login')({
597
+ beforeLoad: async () => {
598
+ const user = await getServerUser()
599
+ if (user) throw redirect({ to: '/dashboard' })
600
+ },
601
+ component: Login,
602
+ })
603
+
604
+ function Login() {
605
+ const handleLogin = async (email: string, password: string) => {
606
+ await login(email, password) // browser-side: sets cookie + localStorage
607
+ window.location.href = '/dashboard'
608
+ }
609
+ // ...
610
+ }
611
+ ```
612
+
613
+ ```tsx
614
+ // app/routes/dashboard.tsx
615
+ import { logout } from '@netlify/identity'
616
+ import { getServerUser } from '~/server/auth'
617
+
618
+ export const Route = createFileRoute('/dashboard')({
619
+ beforeLoad: async () => {
620
+ const user = await getServerUser()
621
+ if (!user) throw redirect({ to: '/login' })
622
+ },
623
+ loader: async () => {
624
+ const user = await getServerUser()
625
+ return { user: user! }
626
+ },
627
+ component: Dashboard,
628
+ })
629
+
630
+ function Dashboard() {
631
+ const { user } = Route.useLoaderData()
632
+
633
+ const handleLogout = async () => {
634
+ await logout() // browser-side: clears cookie + localStorage
635
+ window.location.href = '/'
636
+ }
637
+ // ...
638
+ }
639
+ ```
640
+
641
+ Use `window.location.href` instead of TanStack Router's `navigate()` after auth changes. This ensures the browser sends the updated cookie on the next request.
642
+
643
+ ### Astro (SSR)
644
+
645
+ **Login via API endpoint (server-side pattern):**
646
+
647
+ ```ts
648
+ // src/pages/api/login.ts
649
+ import type { APIRoute } from 'astro'
650
+ import { login } from '@netlify/identity'
651
+
652
+ export const POST: APIRoute = async ({ request }) => {
653
+ const { email, password } = await request.json()
654
+
655
+ try {
656
+ await login(email, password)
657
+ return new Response(null, {
658
+ status: 302,
659
+ headers: { Location: '/dashboard' },
660
+ })
661
+ } catch (error) {
662
+ return Response.json({ error: (error as Error).message }, { status: 400 })
663
+ }
664
+ }
665
+ ```
666
+
667
+ ```astro
668
+ ---
669
+ // src/pages/dashboard.astro
670
+ import { getUser } from '@netlify/identity'
671
+
672
+ const user = getUser()
673
+ if (!user) return Astro.redirect('/login')
674
+ ---
675
+ <h1>Hello, {user.email}</h1>
676
+ ```
677
+
678
+ ### SvelteKit
679
+
680
+ **Login from the browser (recommended):**
681
+
682
+ ```svelte
683
+ <!-- src/routes/login/+page.svelte -->
684
+ <script lang="ts">
685
+ import { login } from '@netlify/identity'
686
+
687
+ let email = ''
688
+ let password = ''
689
+ let error = ''
690
+
691
+ async function handleLogin() {
692
+ try {
693
+ await login(email, password)
694
+ window.location.href = '/dashboard'
695
+ } catch (e) {
696
+ error = (e as Error).message
697
+ }
698
+ }
699
+ </script>
700
+
701
+ <form on:submit|preventDefault={handleLogin}>
702
+ <input bind:value={email} type="email" />
703
+ <input bind:value={password} type="password" />
704
+ <button type="submit">Log in</button>
705
+ {#if error}<p>{error}</p>{/if}
706
+ </form>
707
+ ```
708
+
709
+ ```ts
710
+ // src/routes/dashboard/+page.server.ts
711
+ import { getUser } from '@netlify/identity'
712
+ import { redirect } from '@sveltejs/kit'
713
+
714
+ export function load() {
715
+ const user = getUser()
716
+ if (!user) redirect(302, '/login')
717
+ return { user }
718
+ }
719
+ ```
720
+
721
+ ### Handling OAuth callbacks in SPAs
722
+
723
+ All SPA frameworks need a callback handler that runs on page load to process OAuth redirects, email confirmations, and password recovery tokens. Use a **wrapper component** that blocks page content while processing tokens. This prevents a flash of unauthenticated content that occurs when the page renders before the callback completes.
724
+
725
+ ```tsx
726
+ // React component (works with Next.js, Remix, TanStack Start)
727
+ import { useEffect, useState } from 'react'
728
+ import { handleAuthCallback } from '@netlify/identity'
729
+
730
+ const AUTH_HASH_PATTERN = /^#(confirmation_token|recovery_token|invite_token|email_change_token|access_token)=/
731
+
732
+ export function CallbackHandler({ children }: { children: React.ReactNode }) {
733
+ const [processing, setProcessing] = useState(
734
+ () => typeof window !== 'undefined' && AUTH_HASH_PATTERN.test(window.location.hash),
735
+ )
736
+ const [error, setError] = useState<string | null>(null)
737
+
738
+ useEffect(() => {
739
+ if (!window.location.hash || !AUTH_HASH_PATTERN.test(window.location.hash)) return
740
+
741
+ handleAuthCallback()
742
+ .then((result) => {
743
+ if (!result) {
744
+ setProcessing(false)
745
+ return
746
+ }
747
+ if (result.type === 'invite') {
748
+ window.location.href = `/accept-invite?token=${result.token}`
749
+ } else if (result.type === 'recovery') {
750
+ window.location.href = '/reset-password'
751
+ } else {
752
+ window.location.href = '/dashboard'
753
+ }
754
+ })
755
+ .catch((err) => {
756
+ setError(err instanceof Error ? err.message : 'Callback failed')
757
+ setProcessing(false)
758
+ })
759
+ }, [])
760
+
761
+ if (error) return <div>Auth error: {error}</div>
762
+ if (processing) return <div>Confirming your account...</div>
763
+ return <>{children}</>
764
+ }
765
+ ```
766
+
767
+ Wrap your page content with this component in your **root layout** so it runs on every page:
768
+
769
+ ```tsx
770
+ // Root layout
771
+ <CallbackHandler>
772
+ <Outlet /> {/* or {children} in Next.js */}
773
+ </CallbackHandler>
774
+ ```
775
+
776
+ If you only mount it on a `/callback` route, OAuth redirects and email confirmation links that land on other pages will not be processed.
777
+
778
+ ## Guides
779
+
780
+ ### React `useAuth` hook
781
+
782
+ The library is framework-agnostic, but here's a simple React hook for keeping components in sync with auth state:
783
+
784
+ ```tsx
785
+ import { useState, useEffect } from 'react'
786
+ import { getUser, onAuthChange } from '@netlify/identity'
787
+ import type { User } from '@netlify/identity'
788
+
789
+ export function useAuth() {
790
+ const [user, setUser] = useState<User | null>(getUser())
791
+
792
+ useEffect(() => {
793
+ return onAuthChange((_event, user) => setUser(user))
794
+ }, [])
795
+
796
+ return user
797
+ }
798
+ ```
799
+
800
+ ```tsx
801
+ function NavBar() {
802
+ const user = useAuth()
803
+ return user ? <p>Hello, {user.name}</p> : <a href="/login">Log in</a>
804
+ }
805
+ ```
806
+
807
+ ### Listening for auth changes
808
+
809
+ Use `onAuthChange` to keep your UI in sync with auth state. It fires on login, logout, token refresh, user updates, and recovery. It also detects session changes in other browser tabs (via `localStorage`).
810
+
811
+ ```ts
812
+ import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
813
+
814
+ const unsubscribe = onAuthChange((event, user) => {
815
+ switch (event) {
816
+ case AUTH_EVENTS.LOGIN:
817
+ console.log('Logged in:', user?.email)
818
+ break
819
+ case AUTH_EVENTS.LOGOUT:
820
+ console.log('Logged out')
821
+ break
822
+ case AUTH_EVENTS.TOKEN_REFRESH:
823
+ console.log('Token refreshed for:', user?.email)
824
+ break
825
+ case AUTH_EVENTS.USER_UPDATED:
826
+ console.log('User updated:', user?.email)
827
+ break
828
+ case AUTH_EVENTS.RECOVERY:
829
+ console.log('Recovery login:', user?.email)
830
+ // Redirect to password reset form, then call updateUser({ password })
831
+ break
832
+ }
833
+ })
834
+
835
+ // Later, to stop listening:
836
+ unsubscribe()
837
+ ```
838
+
839
+ On the server, `onAuthChange` is a no-op and the returned unsubscribe function does nothing.
840
+
841
+ ### OAuth login
842
+
843
+ OAuth login is a two-step flow: redirect the user to the provider, then process the callback when they return.
844
+
845
+ **Step by step:**
846
+
847
+ ```ts
848
+ import { oauthLogin, handleAuthCallback } from '@netlify/identity'
849
+
850
+ // 1. Kick off the OAuth flow (e.g., from a "Sign in with GitHub" button).
851
+ // This navigates away from the page and does not return.
852
+ oauthLogin('github')
853
+ ```
854
+
855
+ ```ts
856
+ // 2. On page load, handle the redirect back from the provider.
857
+ const result = await handleAuthCallback()
858
+
859
+ if (result?.type === 'oauth') {
860
+ console.log('Logged in via OAuth:', result.user?.email)
861
+ }
862
+ ```
863
+
864
+ `handleAuthCallback()` exchanges the token in the URL hash, logs the user in, clears the hash, and emits an auth event via `onAuthChange` (`'login'` for OAuth/confirmation, `'recovery'` for password recovery).
865
+
866
+ ### Password recovery
867
+
868
+ Password recovery is a two-step flow. The library handles the token exchange automatically via `handleAuthCallback()`, which logs the user in and returns `{type: 'recovery', user}`. A `'recovery'` event (not `'login'`) is emitted via `onAuthChange`, so event-based listeners can also detect this flow. You then show a "set new password" form and call `updateUser()` to save it.
869
+
870
+ **Step by step:**
871
+
872
+ ```ts
873
+ import { requestPasswordRecovery, handleAuthCallback, updateUser } from '@netlify/identity'
874
+
875
+ // 1. Send recovery email (e.g., from a "forgot password" form)
876
+ await requestPasswordRecovery('jane@example.com')
877
+
878
+ // 2-3. On page load, handle the callback
879
+ const result = await handleAuthCallback()
880
+
881
+ if (result?.type === 'recovery') {
882
+ // 4. User is now logged in. Show your "set new password" form.
883
+ // When they submit:
884
+ const newPassword = document.getElementById('new-password').value
885
+ await updateUser({ password: newPassword })
886
+ }
887
+ ```
888
+
889
+ If you use the event-based pattern instead of checking `result.type`, listen for the `'recovery'` event:
890
+
891
+ ```ts
892
+ import { onAuthChange, AUTH_EVENTS } from '@netlify/identity'
893
+
894
+ onAuthChange((event, user) => {
895
+ if (event === AUTH_EVENTS.RECOVERY) {
896
+ // Redirect to password reset form.
897
+ // The user is authenticated, so call updateUser({ password }) to set the new password.
898
+ }
899
+ })
900
+ ```
901
+
902
+ ### Invite acceptance
903
+
904
+ When an admin invites a user, they receive an email with an invite link. Clicking it redirects to your site with an `invite_token` in the URL hash. Unlike other callback types, the user is not logged in automatically because they need to set a password first.
905
+
906
+ **Step by step:**
907
+
908
+ ```ts
909
+ import { handleAuthCallback, acceptInvite } from '@netlify/identity'
910
+
911
+ // 1. On page load, handle the callback.
912
+ const result = await handleAuthCallback()
913
+
914
+ if (result?.type === 'invite' && result.token) {
915
+ // 2. The user is NOT logged in yet. Show a "set your password" form.
916
+ // When they submit:
917
+ const password = document.getElementById('password').value
918
+ const user = await acceptInvite(result.token, password)
919
+ console.log('Account created:', user.email)
920
+ }
921
+ ```
922
+
923
+ ### Session lifetime
924
+
925
+ Sessions are managed by Netlify Identity (GoTrue) on the server side. The library stores two cookies:
926
+
927
+ - **`nf_jwt`**: A short-lived JWT access token (default: 1 hour). Automatically refreshed by gotrue-js in the browser using the refresh token.
928
+ - **`nf_refresh`**: A long-lived refresh token used to obtain new access tokens without re-authenticating.
929
+
930
+ In the browser, gotrue-js handles token refresh automatically in the background. On the server, the access token in the `nf_jwt` cookie is validated as-is; if it has expired, `getUser()` returns `null`. The user will need to refresh the page (which triggers a browser-side token refresh) or log in again.
931
+
932
+ Session lifetime is configured in your GoTrue/Identity server settings, not in this library.
933
+
168
934
  ## License
169
935
 
170
936
  MIT