npx skills add ...
npx skills add netlify/context-and-tools --skill netlify-identity
Add authentication and user management to a Netlify site with @netlify/identity — signup/login/logout, OAuth social login (Google/GitHub/GitLab/Bitbucket), server-side user verification in Functions, role-based access control (RBAC), admin user management, and Identity event hooks. Use when adding a login/signup flow, "add social login", gating content by user role, protecting a function or page behind auth, assigning roles at signup, customizing auth emails, or handling OAuth/confirmation/recovery callbacks. Not for locking an entire site to a company/team — that is netlify-access-control.
npx skills add netlify/context-and-tools --skill netlify-identity
Auth and user management for a Netlify site without requiring visitors to be Netlify users. Package: @netlify/identity.
Reach for @netlify/identity. Do NOT use the legacy netlify-identity-widget or gotrue-js for new work — same capabilities, simpler API, built-in server-side support.
netlify dev. Test auth flows on a deploy — Deploy Previews work. Local netlify dev cannot exercise /.netlify/identity/*.client_id/secret in code, no custom callback token exchange. Use oauthLogin() + handleAuthCallback(). Raw OAuth beside Identity is the single most common source of rework.api.netlify.com to flip/inspect Identity settings, never read tokens from ~/Library/Preferences/netlify/config.json, never probe undocumented endpoints.login()/signup()/logout() need CSRF protection. Call verifyRequestOrigin(req) first, or an attacker can log a victim into the attacker's account./.netlify/identity/* unreachable, OAuth doesn't return): surface the error, the dashboard URL, and the setting to check — then stop. Do not invent recovery commands.Identity must be enabled in the dashboard first (no API): Project configuration > Identity (https://app.netlify.com/projects/{site_name}/configuration/identity) → Enable Identity.
HTTPS is required. On a custom domain, get HTTPS/SSL working before integrating Identity.
Callback handling is mandatory. Call handleAuthCallback() on your landing page. It processes ALL token types in the URL hash — OAuth redirect, email confirmation, password recovery, invite. Without it, confirmation links and OAuth redirects never complete.
Other client functions:
recoverPassword() — complete a password reset (alternative to letting handleAuthCallback() handle the recovery_token).acceptInvite() — complete invite acceptance (alternative to handleAuthCallback() handling invite_token).refreshSession() — refresh token/session so newly-assigned roles take effect.Don't hard-code which providers exist. Call getSettings() at startup and render the signup form and OAuth buttons from what it returns.
Handlers are modern v2 functions: export default async (req, context) => {}. v1 export { handler } is not supported for getUser()/login()/admin.*.
getUser() works in browser, Netlify Functions, and Edge Functions.
CSRF — always guard exposed login/signup/logout endpoints:
admin.* uses a short-lived admin token and runs only in Netlify Functions — NOT browser, NOT Edge Functions.
admin.listUsers() — array of users.admin.updateUser() — update a user (e.g. roles). Full API: https://www.npmjs.com/package/@netlify/identityJWT stored in cookie nf_jwt, sent automatically. Server-side login/signup/logout read/write nf_jwt and nf_refresh via the runtime, so the browser gets the session in the response.
User objectid, email, roles (array from app_metadata.roles, included in the JWT).
Functions the platform invokes automatically on Identity events (you don't call them).
Modern typed-handler syntax — export a default object with a method per event. Typed handlers require @netlify/functions ≥ 5.2.0.
Handlers and triggers:
| Handler | Fires when |
|---|---|
userValidate | User attempts signup, before account creation — block by email domain, rate-limit, custom validation. |
userSignup | Signup completes (email or external). Fires after email confirmation if confirmation is enabled. Assign roles, sync, notify. |
userLogin | User logs in — track logins, sync, block a user. |
userModified | Profile updated. |
userDeleted | User deleted (notification only). |
Deny an action: call event.deny() from userValidate/userSignup/userLogin/userModified (NOT userDeleted). User gets a 401; no observability error. With multiple subscribers, the first event.deny() aborts the chain.
Assign roles at signup — return { user: {...} } to mutate the persisted record. Payload fields are camelCase (appMetadata, userMetadata, confirmedAt).
Background mode — action completes immediately, handler runs async:
Event types from @netlify/functions: UserValidateEvent, UserSignupEvent, UserLoginEvent, UserModifiedEvent, UserDeletedEvent, Config.
signup()) or Invite only (all new users, including external-provider logins, must be invited first).invite_token → process with handleAuthCallback() or acceptInvite().Stored on the User object; edit in Identity > Users > Edit settings:
user_metadata.full_name.user_metadata.email.app_metadata.roles. Read via getUser().Set roles: at signup via userSignup handler returning { user: {...} }; for existing users via admin.updateUser() in a Function. Role changes take effect on next login or token refresh, not immediately (they don't invalidate the current JWT — client can refreshSession()).
Enforced at the CDN edge (no origin round trip). Add a Role parameter to redirect rules.
Netlify Identity roles resolve at app_metadata.roles.
You may use Identity OR an external JWT provider, not both — you cannot authenticate third-party JWT tokens while Identity is enabled. Set the secret at Project configuration > Access & security > Visitor access > JWT secret (project-level overrides team-level default).
"alg": "HS256", "typ": "JWT".exp (future Unix Epoch); other fields optional.app_metadata.authorization.roles. Different path → contact support for a custom role path (support-configured, not self-service).Default sender no-reply@netlify.com. Custom sender (Pro+): set SMTP hostname/port/username/password under Emails > Outgoing email address (use SendGrid/Mailjet/etc. for volume).
Custom templates (Pro+): publish HTML to a path on your deployed project, set the path (relative to domain, starting /) under Emails. Rules: inline CSS only, absolute image links, NO <html>/<head>/<body> tags. Keep template variables intact — don't let your build rewrite them.
Go template variables: {{ .Email }}, {{ .NewEmail }} (email-change only), {{ .SiteURL }}, {{ .ConfirmationURL }}, {{ .Token }}. Custom link form: {{ .SiteURL }}/path/#confirmation_token={{ .Token }} (also invite_token, recovery_token, email_change_token).
Project configuration > Identity > Identity audit log. Search with a required scope prefix: author:[string] or action:[string]. Action names: login, logout, user_signedup, user_deleted, user_modified, token_revoked, token_refreshed, user_recovery_requested, user_invited.
references/advanced-patterns.md — SSR / session hydration.references/authorization-and-sessions.md.netlify-identity-widget / gotrue-js — superseded by @netlify/identity.identity-validate.ts, identity-signup.ts, identity-login.ts, -background suffix) still work but prefer typed handlers. Legacy denial = return non-2xx status; new code uses event.deny().These are org conventions, not docs facts — merged into the rendered skill by ctx-gen and never generated. Owned by the skills maintainer.
references/advanced-patterns.md
(SSR/session hydration) and references/authorization-and-sessions.md.netlify dev — test auth flows on deploys
(Deploy Previews work).api.netlify.com to flip or inspect Identity settings, never read auth
tokens from ~/Library/Preferences/netlify/config.json, never probe for
undocumented endpoints./.netlify/identity/* unreachable, OAuth flow
doesn't return), surface the error, the dashboard URL, and the setting to
check — then stop. Do not invent recovery commands.client_id/secret in code, no custom
callback token exchange. Use oauthLogin() + handleAuthCallback();
raw OAuth beside Identity is the single most common source of rework.getUser()/login()/admin.* require modern v2 functions
(export default) — v1 export { handler } is not supported. Typed
Identity event handlers (UserSignupEvent, event.deny()) require
@netlify/functions ≥ 5.2.0; older installs use the legacy filenames.getSettings() at
startup and render the signup form and OAuth buttons from what it returns.import { handleAuthCallback } from '@netlify/identity'
const result = await handleAuthCallback()
if (result) console.log(result.type, result.user.email) // may be falsy if nothing to processimport { getUser } from '@netlify/identity'
import type { Context } from '@netlify/functions' // or '@netlify/edge-functions' for Edge
export default async (req: Request, context: Context) => {
const user = await getUser()
if (!user) return new Response('Unauthorized', { status: 401 })
if (!user.roles.includes('admin')) return new Response('Forbidden', { status: 403 })
return Response.json({ id: user.id, email: user.email })
}