Next.js (@getlocksmith/nextjs)
The official Next.js integration for Locksmith. It gives you:
- Server-only
LocksmithServerClient(API key) for Route Handlers, Server Actions, and middleware. - BFF route handlers that proxy Locksmith with httpOnly cookies so the browser never sees your project API key.
LocksmithAuthProvider+LocksmithBffClientfor the browser (cookie session, same-originfetch).- Themed UI for sign-in, sign-up, TOTP, social OAuth, OIDC consent, OAuth callback, passkey sign-in, and low-level layout primitives.
For non-Next backends, use the HTTP SDK @getlocksmith/sdk.
Install
npm install @getlocksmith/nextjs
Passkeys (optional peer — only if you use LocksmithPasskeySignInButton):
npm install @simplewebauthn/browser
Peer requirements: next ≥ 14.2, react / react-dom ≥ 18. Node ≥ 18.
Package entry points
| Import | Use |
|---|---|
@getlocksmith/nextjs/server | LocksmithServerClient, locksmithServerClientFromEnv, createLocksmithRouteHandlers, createLocksmithMiddleware |
@getlocksmith/nextjs/client | LocksmithAuthProvider, useLocksmithAuth, useLocksmithPoweredBy, LocksmithBffClient, all form/OAuth/OIDC UI components, theme helpers |
@getlocksmith/nextjs | Shared types, LocksmithAuthError, locksmithEnvironmentFromApiKey, isSignInRequiresTotp |
Environment variables (server)
| Variable | Required | Description |
|---|---|---|
LOCKSMITH_API_KEY | Yes (for server helpers) | Project key: lsm_live_… (Production) or lsm_sbx_… (Sandbox). |
LOCKSMITH_BASE_URL | No | Locksmith API origin. Falls back to NEXT_PUBLIC_LOCKSMITH_URL, then https://getlocksmith.dev. |
The browser must not receive the API key. All browser auth goes through your BFF (below) or public OAuth redirects.
Architecture
- Your Next app exposes
POST/GET /api/locksmith/…(or another base path) implemented withcreateLocksmithRouteHandlers. Those handlers call Locksmith withX-API-Keyand sethttpOnlycookies (access+refreshby default). - Client components wrap the tree in
LocksmithAuthProviderwith the sameroutePrefix. Hooks andLocksmithBffClientcallfetch(..., { credentials: 'include' })to that prefix. - Optional:
createLocksmithMiddlewarevalidates a Bearer access token against Locksmith/api/auth/meand addsx-locksmith-user-*headers for API routes.
BFF route handlers
Setup (App Router)
Create a catch-all route so every subpath (session, login, oauth/github, …) is handled by one module:
import {
createLocksmithRouteHandlers,
locksmithServerClientFromEnv,
} from '@getlocksmith/nextjs/server'
const { GET, POST } = createLocksmithRouteHandlers({
...locksmithServerClientFromEnv(),
routeBasePath: '/api/locksmith',
})
export { GET, POST }
[[...path]]is required sologin,session,oauth/exchange, etc. resolve correctly.routeBasePathmust match the URL segment where you mounted the route (here/api/locksmith).
Config (LocksmithRouteHandlerConfig)
| Option | Description |
|---|---|
apiKey | Project API key (required unless you only use env helper). |
baseUrl | Locksmith origin (optional; same as server client). |
routeBasePath | Mount path for the BFF (default '/api/locksmith'). Must match LocksmithAuthProvider / LocksmithBffClient routePrefix. |
cookiePrefix | Prefix for cookie names (default 'locksmith' → locksmith_at, locksmith_rt). Non-alphanumeric chars become _. |
accountPlan | 'FREE' | 'SOLO' | 'PRO'. If set, skips GET /api/auth/sdk/branding and sets session poweredByLocksmith when plan is FREE. |
brandingFallbackPoweredBy | If accountPlan is unset and the branding HTTP call fails, use this boolean (default false). |
Cookies
- Access token cookie: short max-age (matches token
expiresIn). - Refresh token cookie: long-lived (60 days).
- In production, cookies use
Secure.SameSite=Lax,Path=/.
BFF HTTP surface (reference)
All responses follow Locksmith’s envelope: success { data: T }, error { error, message }.
| Method | Relative path | Purpose |
|---|---|---|
| GET | session | Current user from access cookie; refreshes via refresh cookie if needed. Includes poweredByLocksmith. |
| POST | login | Body: { email, password }. May return MFA challenge via Locksmith; BFF forwards shape for TOTP continuation. |
| POST | signup | Body: { email, password, meta? }. Sets session cookies on success. |
| POST | totp | Body: { twoFactorToken, code }. Completes password sign-in after MFA. |
| POST | logout | Body: optional { refreshToken }; clears cookies and revokes refresh server-side when possible. |
| POST | refresh | Body: optional { refreshToken }; uses refresh cookie if omitted. Rotates tokens; returns { expiresIn }. (Not wrapped by LocksmithBffClient today — call with fetch if you need it.) |
| POST | oauth/:provider | Body: optional { redirectUrl }. Returns { authorizationUrl }. |
| POST | oauth/exchange | Body: { code }. Exchanges OAuth code; sets session cookies. |
| POST | oidc/grant | Body: { requestToken, approved, scopes? }. When approved: true, requires valid access cookie to resolve userId. Returns { redirectUrl }. |
| POST | passkey/login/options | Body: { email }. Returns WebAuthn options + challengeId. |
| POST | passkey/login/verify | Body: { challengeId, email, response }. Sets session cookies on success. |
GET only implements session; any other GET returns 404.
LocksmithAuthProvider and useLocksmithAuth
Wrap client UI that should share session state:
'use client'
import { LocksmithAuthProvider } from '@getlocksmith/nextjs/client'
export function Providers({ children }: { children: React.ReactNode }) {
return (
<LocksmithAuthProvider routePrefix="/api/locksmith">
{children}
</LocksmithAuthProvider>
)
}
Provider props
Same as LocksmithBffClientOptions:
| Prop | Description |
|---|---|
routePrefix | BFF base path (default /api/locksmith). Must match createLocksmithRouteHandlers routeBasePath. |
origin | Defaults to same origin (''). Set full origin (e.g. https://myapp.com) if the browser calls a different host than the current page (unusual). |
On mount, the provider loads GET …/session once (loading → false).
useLocksmithAuth() — full API
| Member | Type | Description |
|---|---|---|
user | UserMe | null | Signed-in user or null. |
loading | boolean | Initial session fetch in flight. |
error | string | null | Last SDK error message (many methods also throw LocksmithAuthError). |
pendingTotpToken | string | null | After password sign-in if MFA required; pass to LocksmithTotpForm / completeTotp. |
poweredByLocksmith | boolean | From session; when true, show Powered by Locksmith on Free-tier projects. |
refreshSession() | async | Re-runs GET session; updates user / poweredByLocksmith. |
signInWithPassword(email, password) | async → 'signed_in' | 'needs_totp' | Sets pendingTotpToken when MFA needed. |
completeTotp(code) | async | Completes MFA; requires pendingTotpToken. |
signUp(email, password, meta?) | async | Creates user and refreshes session. |
signOut() | async | Clears BFF session (always clears local user state). |
startOAuth(provider, redirectUrl?) | async | POSTs BFF then sets window.location.href to provider. |
completeOAuthExchange(code) | async | POSTs oauth/exchange; refreshes session. |
completeOidcConsent({ requestToken, approved, scopes? }) | async → redirectUrl: string | POSTs oidc/grant; navigate with window.location.href = redirectUrl. |
On failure, methods typically set error and throw LocksmithAuthError (see root import). Use try/catch in manual flows.
useLocksmithPoweredBy()
Returns ctx?.poweredByLocksmith === true when inside the provider; false when outside the provider. Use for custom components that should mirror the built-in Powered by behavior.
LocksmithBffClient (browser, no React context)
Use when you cannot use useLocksmithAuth but can still call fetch with credentials: 'include' (same site as the BFF).
import { LocksmithBffClient } from '@getlocksmith/nextjs/client'
const bff = new LocksmithBffClient({ routePrefix: '/api/locksmith' })
Methods
| Method | BFF route | Notes |
|---|---|---|
session() | GET session | |
signIn(email, password) | POST login | Raw result may be MFA challenge — use isSignInRequiresTotp from @getlocksmith/nextjs. |
signUp({ email, password, meta? }) | POST signup | |
completeTotp(twoFactorToken, code) | POST totp | |
initiateOAuth(provider, redirectUrl?) | POST oauth/:provider | |
exchangeOAuthCode(code) | POST oauth/exchange | |
oidcGrant({ requestToken, approved, scopes? }) | POST oidc/grant | Send cookies when approved: true. |
signOut(refreshToken?) | POST logout | |
passkeyLoginOptions(email) | POST passkey/login/options | |
passkeyLoginVerify({ challengeId, email, response }) | POST passkey/login/verify |
There is no refresh() wrapper on the class; you can fetch POST …/refresh manually if needed.
Theming and layout primitives
Themes
locksmith(default) — dark surface, steel-blue accent (aligned with getlocksmith.dev).minimal— light neutral card.
Pass theme="locksmith" or theme="minimal" to any themed component.
classNames (LocksmithFormClassNames)
Optional Tailwind or CSS classes merged onto slots:
| Slot | Typical use |
|---|---|
root | Outer card (LocksmithFormShell / LocksmithFormSurface) |
field | Field group |
label, input, button | Controls |
error | Alert text |
poweredBy | Footer link block |
Use mergeFormClasses from @getlocksmith/nextjs/client to combine strings safely.
Inline style helpers
Exported for fully custom UIs that should still match tokens:
locksmithFormThemeStyle(theme)— full cardCSSProperties+ CSS variables.LOCKSMITH_FORM_KEYFRAMES_CSS— spinner animation (injected byLocksmithFormSurface/LocksmithFormShell).labelStyle,inputStyle,inputStyleLarge,primaryButtonStyle,outlineButtonStyle,errorStyle,fieldStackStyle, etc.
See locksmithMarketingFontNote for matching Barlow fonts on marketing pages.
LocksmithFormSurface and LocksmithFormShell
LocksmithFormSurface— themed container + keyframes; no powered-by footer.LocksmithFormShell— same as surface +poweredByLocksmithboolean; whentrue, appendsLocksmithPoweredBy.
Use these to build custom forms while staying on-brand.
LocksmithPoweredBy
Standalone footer link component. On Free plan, do not hide attribution in production unless the project is paid / accountPlan is configured so poweredByLocksmith is false.
Free plan: “Powered by Locksmith”
GET /api/auth/sdk/brandingon Locksmith (same API key) returns{ poweredByLocksmith }, or setaccountPlanoncreateLocksmithRouteHandlersto skip the HTTP call.- The BFF caches branding per API key for about 5 minutes. After changing plan in the DB, wait or restart the server if the footer does not update immediately.
Built-in components respect poweredByLocksmith from session. There is no prop to remove the footer on Free; upgrade the account or set accountPlan appropriately for development.
Sign-in (LocksmithSignInForm)
Premade component
import { LocksmithSignInForm } from '@getlocksmith/nextjs/client'
export function LoginPage() {
return (
<LocksmithSignInForm
theme="locksmith"
oauthRedirectUrl={`${typeof window !== 'undefined' ? window.location.origin : ''}/auth/callback`}
socialProviders={['github', 'google']}
/>
)
}
Props (high level)
| Prop | Description |
|---|---|
theme, classNames | Styling (see above). |
title, description | Card header copy. |
emailLabel, passwordLabel, submitLabel | Field / button labels. |
forgotPasswordLabel, forgotPasswordHref | Forgot link. |
signUpLabel, signUpHref, signUpLinkText | Sign-up promo row. |
socialProviders | Array of SocialProvider: google, github, apple, microsoft, x, discord, linkedin. |
onSocialLogin | If set, social buttons call this instead of default startOAuth. |
oauthRedirectUrl | Passed to startOAuth for each provider (must be absolute URL of your callback page). |
showSocialDivider | Default true; “or continue with email” rule. |
onSuccess | Fires when password sign-in completes without MFA. If MFA required, show LocksmithTotpForm first. |
onError | Validation / Locksmith error message string. |
Manual sign-in (hook)
const { signInWithPassword, error } = useLocksmithAuth()
const outcome = await signInWithPassword(email, password)
if (outcome === 'needs_totp') {
/* show TOTP UI */
}
Manual sign-in (LocksmithBffClient)
import { LocksmithBffClient } from '@getlocksmith/nextjs/client'
import { isSignInRequiresTotp } from '@getlocksmith/nextjs'
const bff = new LocksmithBffClient({ routePrefix: '/api/locksmith' })
const result = await bff.signIn(email, password)
if (isSignInRequiresTotp(result)) {
/* POST .../totp with result.twoFactorToken + code */
} else {
/* result.user — session cookies set by BFF */
}
Sign-up (LocksmithSignUpForm)
Premade component
Includes optional name field (stored in meta.name), confirm password, password strength meter, optional terms slot, and the same social grid pattern as sign-in.
Notable props
| Prop | Description |
|---|---|
showNameField | Adds name → meta.name. |
showConfirmPassword | Second password field with match validation. |
showPasswordStrength | Default true; visual rules (length, upper, lower, number). |
termsText | ReactNode below submit (e.g. checkbox copy). |
socialProviders, onSocialLogin, oauthRedirectUrl, showSocialDivider | Same idea as sign-in. |
onSuccess, onError | After signUp succeeds, session is refreshed. |
Manual sign-up
const { signUp } = useLocksmithAuth()
await signUp(email, password, { plan: 'trial' })
TOTP / MFA (LocksmithTotpForm)
Renders only when pendingTotpToken is set (after signInWithPassword returns needs_totp). Typically place below LocksmithSignInForm on the same page.
Props
| Prop | Description |
|---|---|
codeLength | 6 or 8 (default 6). |
autoSubmit | Default true; submits when code length reached. |
onBack | Shows ghost “back” control when set. |
title, description, label, submitLabel, backLabel | Copy. |
Manual MFA
const { completeTotp, pendingTotpToken } = useLocksmithAuth()
if (!pendingTotpToken) return null
await completeTotp(code)
Social OAuth
Dashboard: enable providers and allowed redirect URLs on the Locksmith project.
Flow
startOAuth/initiateOAuth→ redirect browser to provider.- User returns to your app with
?code=. completeOAuthExchange/exchangeOAuthCode→ BFF sets cookies.
Premade pieces
| Component | Role |
|---|---|
LocksmithOAuthSignInButton | Single provider button inside a small LocksmithFormSurface; uses startOAuth; optional Powered by footer. |
LocksmithOAuthCallback | Reads code from useSearchParams, exchanges it, shows loading/success/error states, then router.replace(redirectTo). Wraps Suspense. |
LocksmithSignInForm / LocksmithSignUpForm | socialProviders + LocksmithSocialButtonGroup styling. |
LocksmithSocialButton, LocksmithSocialButtonGroup | Lower-level; use in custom layouts. |
LocksmithOAuthSignInButton props
provider (string, e.g. github), redirectUrl (absolute callback URL), theme, classNames, children, onSuccess (if redirect fails), onError.
LocksmithOAuthCallback props
redirectTo (default /), theme, classNames, className, onSuccess, onError, and copy overrides: title, description, successTitle, successDescription, errorTitle, errorDescription.
Manual OAuth (hook)
See startOAuth / completeOAuthExchange in the useLocksmithAuth table.
Manual OAuth (LocksmithBffClient)
Use the same routePrefix as your BFF mount; optional origin if the browser calls another host (rare). All requests use credentials: 'include' so session cookies are sent on the callback exchange.
Start the flow (e.g. button handler):
import { LocksmithBffClient } from '@getlocksmith/nextjs/client'
const bff = new LocksmithBffClient({ routePrefix: '/api/locksmith' })
const { authorizationUrl } = await bff.initiateOAuth(
'github',
'https://myapp.com/auth/callback',
)
window.location.href = authorizationUrl
Callback page (after reading code from the query string — same bff options as above):
import { LocksmithBffClient } from '@getlocksmith/nextjs/client'
const bff = new LocksmithBffClient({ routePrefix: '/api/locksmith' })
await bff.exchangeOAuthCode(code)
Suspense
Any component using useSearchParams() (including LocksmithOAuthCallback and LocksmithOidcConsent) must be under <Suspense> if Next.js reports static generation issues. The premade components include an inner or outer Suspense boundary.
Hosted SSO / OIDC consent (LocksmithOidcConsent)
Pro feature on Locksmith: OIDC authorize redirects the user to your loginUrl with query params:
| Param | Meaning |
|---|---|
request_token | Opaque token for oidc/grant. |
app_name | Client display name. |
scope | Space-separated OIDC scopes (e.g. openid email profile). |
Premade component
import {
LocksmithOidcConsent,
LocksmithSignInForm,
LocksmithTotpForm,
} from '@getlocksmith/nextjs/client'
export default function OidcLoginPage() {
return (
<LocksmithOidcConsent
theme="locksmith"
signInSlot={
<>
<LocksmithSignInForm oauthRedirectUrl={/* … */} />
<LocksmithTotpForm />
</>
}
/>
)
}
Behavior: invalid / loading / MFA-pending / signed-out / main consent UI with scope checkboxes (openid fixed on). Allow / Deny call completeOidcConsent and set window.location.href to the returned redirectUrl.
Props
theme, classNames, className, signInSlot, onSuccess (before redirect), onError.
Manual OIDC consent
Read params with useSearchParams, build UI, then:
const { completeOidcConsent } = useLocksmithAuth()
const redirectUrl = await completeOidcConsent({
requestToken,
approved: true,
scopes: ['openid', 'email'],
})
window.location.href = redirectUrl
Deny: approved: false, no session required. Allow: user must have BFF session; scopes must include openid.
Without the provider — instantiate LocksmithBffClient (same routePrefix as your BFF). oidcGrant uses fetch with credentials: 'include' so cookies are sent when approved: true:
import { LocksmithBffClient } from '@getlocksmith/nextjs/client'
const bff = new LocksmithBffClient({ routePrefix: '/api/locksmith' })
const { redirectUrl } = await bff.oidcGrant({
requestToken,
approved: true,
scopes: ['openid', 'email'],
})
window.location.href = redirectUrl
Passkey sign-in (LocksmithPasskeySignInButton)
Requires @simplewebauthn/browser. Your site origin must appear in the project’s CORS allowed origins in Locksmith.
- Takes
bff:LocksmithBffClientOptions(must match your BFF mount). - Takes
email(user must have registered a passkey). - On success: calls
onSuccess, thenwindow.location.reload()soLocksmithAuthProviderpicks up the new cookie session.
Not included in the package: passkey registration UI — use LocksmithServerClient passkeyRegisterOptions / passkeyRegisterVerify from a server context with the user’s access token, or call Locksmith REST directly.
Middleware (createLocksmithMiddleware)
Validates Authorization: Bearer <access_token> using LocksmithServerClient.getUser (Locksmith /api/auth/me). On success, forwards:
x-locksmith-user-idx-locksmith-user-emailx-locksmith-user-role
On missing/invalid token, returns 401 JSON { error }.
import {
createLocksmithMiddleware,
locksmithServerClientFromEnv,
} from '@getlocksmith/nextjs/server'
const client = locksmithServerClientFromEnv()
export default createLocksmithMiddleware(client)
Restrict which routes run this middleware with Next.js export const config = { matcher: [...] } as usual.
LocksmithServerClient (server-only)
Constructed with { apiKey, baseUrl? }. environment is derived from the key prefix (lsm_live_ → production, lsm_sbx_ → Sandbox).
Use for: Server Actions, Route Handlers, cron jobs, middleware, or any code that can hold the API key.
Method list (HTTP mapping)
| Method | Locksmith API |
|---|---|
signUp, signIn, completeSignInTotp, signOut, refresh, getUser | Core auth |
getSdkBranding | GET /api/auth/sdk/branding |
verifyAccessToken | Local JWT verify (RS256; default issuer https://getlocksmith.dev) |
totpStart, totpConfirm, totpDisable | Authenticator enrollment (bearer) |
passkeyRegisterOptions, passkeyRegisterVerify, passkeyLoginOptions, passkeyLoginVerify, passkeyList, passkeyRevoke | WebAuthn |
sendMagicLink, verifyMagicLink | Magic link |
sendPasswordReset, updatePassword | Password reset |
initiateOAuth, exchangeOAuthCode | Social OAuth (server-side) |
completeOidcGrant | OIDC grant completion |
locksmithServerClientFromEnv() reads LOCKSMITH_API_KEY and optional base URL env vars; throws if the key is missing.
Root package: types and errors
From @getlocksmith/nextjs:
LocksmithAuthError—code,message,status.isSignInRequiresTotp— narrowssignInresult.- Types:
UserMe,AuthTokens,SignInRequiresTotp,SignUpResult,OidcGrantResult,OAuthInitiateResult,TokenPayload,SdkBranding,LocksmithEnvironment, etc.
locksmithEnvironmentFromApiKey(key) — returns 'production' | 'sandbox' or throws if prefix invalid.
Choosing “premade” vs “manual”
| Goal | Premade | Manual |
|---|---|---|
| Email/password + MFA | LocksmithSignInForm + LocksmithTotpForm | signInWithPassword + completeTotp |
| Sign up | LocksmithSignUpForm | signUp |
| Social login button | LocksmithOAuthSignInButton or social row on forms | startOAuth / bff.initiateOAuth |
| OAuth return | LocksmithOAuthCallback | completeOAuthExchange in useEffect |
| OIDC consent | LocksmithOidcConsent | completeOidcConsent / bff.oidcGrant |
| Passkey | LocksmithPasskeySignInButton | bff.passkeyLoginOptions + @simplewebauthn/browser + bff.passkeyLoginVerify |
| Custom styled form | LocksmithFormShell + theme helpers | Same hooks / LocksmithBffClient |
| Server CRUD / admin | N/A | LocksmithServerClient only |