Skip to main content

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 + LocksmithBffClient for the browser (cookie session, same-origin fetch).
  • 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

ImportUse
@getlocksmith/nextjs/serverLocksmithServerClient, locksmithServerClientFromEnv, createLocksmithRouteHandlers, createLocksmithMiddleware
@getlocksmith/nextjs/clientLocksmithAuthProvider, useLocksmithAuth, useLocksmithPoweredBy, LocksmithBffClient, all form/OAuth/OIDC UI components, theme helpers
@getlocksmith/nextjsShared types, LocksmithAuthError, locksmithEnvironmentFromApiKey, isSignInRequiresTotp

Environment variables (server)

VariableRequiredDescription
LOCKSMITH_API_KEYYes (for server helpers)Project key: lsm_live_… (Production) or lsm_sbx_… (Sandbox).
LOCKSMITH_BASE_URLNoLocksmith 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

  1. Your Next app exposes POST/GET /api/locksmith/… (or another base path) implemented with createLocksmithRouteHandlers. Those handlers call Locksmith with X-API-Key and set httpOnly cookies (access + refresh by default).
  2. Client components wrap the tree in LocksmithAuthProvider with the same routePrefix. Hooks and LocksmithBffClient call fetch(..., { credentials: 'include' }) to that prefix.
  3. Optional: createLocksmithMiddleware validates a Bearer access token against Locksmith /api/auth/me and adds x-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:

app/api/locksmith/[[...path]]/route.ts
import {
createLocksmithRouteHandlers,
locksmithServerClientFromEnv,
} from '@getlocksmith/nextjs/server'

const { GET, POST } = createLocksmithRouteHandlers({
...locksmithServerClientFromEnv(),
routeBasePath: '/api/locksmith',
})

export { GET, POST }
  • [[...path]] is required so login, session, oauth/exchange, etc. resolve correctly.
  • routeBasePath must match the URL segment where you mounted the route (here /api/locksmith).

Config (LocksmithRouteHandlerConfig)

OptionDescription
apiKeyProject API key (required unless you only use env helper).
baseUrlLocksmith origin (optional; same as server client).
routeBasePathMount path for the BFF (default '/api/locksmith'). Must match LocksmithAuthProvider / LocksmithBffClient routePrefix.
cookiePrefixPrefix 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.
brandingFallbackPoweredByIf 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 }.

MethodRelative pathPurpose
GETsessionCurrent user from access cookie; refreshes via refresh cookie if needed. Includes poweredByLocksmith.
POSTloginBody: { email, password }. May return MFA challenge via Locksmith; BFF forwards shape for TOTP continuation.
POSTsignupBody: { email, password, meta? }. Sets session cookies on success.
POSTtotpBody: { twoFactorToken, code }. Completes password sign-in after MFA.
POSTlogoutBody: optional { refreshToken }; clears cookies and revokes refresh server-side when possible.
POSTrefreshBody: optional { refreshToken }; uses refresh cookie if omitted. Rotates tokens; returns { expiresIn }. (Not wrapped by LocksmithBffClient today — call with fetch if you need it.)
POSToauth/:providerBody: optional { redirectUrl }. Returns { authorizationUrl }.
POSToauth/exchangeBody: { code }. Exchanges OAuth code; sets session cookies.
POSToidc/grantBody: { requestToken, approved, scopes? }. When approved: true, requires valid access cookie to resolve userId. Returns { redirectUrl }.
POSTpasskey/login/optionsBody: { email }. Returns WebAuthn options + challengeId.
POSTpasskey/login/verifyBody: { 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:

app/providers.tsx
'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:

PropDescription
routePrefixBFF base path (default /api/locksmith). Must match createLocksmithRouteHandlers routeBasePath.
originDefaults 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 (loadingfalse).

useLocksmithAuth() — full API

MemberTypeDescription
userUserMe | nullSigned-in user or null.
loadingbooleanInitial session fetch in flight.
errorstring | nullLast SDK error message (many methods also throw LocksmithAuthError).
pendingTotpTokenstring | nullAfter password sign-in if MFA required; pass to LocksmithTotpForm / completeTotp.
poweredByLocksmithbooleanFrom session; when true, show Powered by Locksmith on Free-tier projects.
refreshSession()asyncRe-runs GET session; updates user / poweredByLocksmith.
signInWithPassword(email, password)async → 'signed_in' | 'needs_totp'Sets pendingTotpToken when MFA needed.
completeTotp(code)asyncCompletes MFA; requires pendingTotpToken.
signUp(email, password, meta?)asyncCreates user and refreshes session.
signOut()asyncClears BFF session (always clears local user state).
startOAuth(provider, redirectUrl?)asyncPOSTs BFF then sets window.location.href to provider.
completeOAuthExchange(code)asyncPOSTs oauth/exchange; refreshes session.
completeOidcConsent({ requestToken, approved, scopes? })async → redirectUrl: stringPOSTs oidc/grant; navigate with window.location.href = redirectUrl.
Throws

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

MethodBFF routeNotes
session()GET session
signIn(email, password)POST loginRaw 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/grantSend 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:

SlotTypical use
rootOuter card (LocksmithFormShell / LocksmithFormSurface)
fieldField group
label, input, buttonControls
errorAlert text
poweredByFooter 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 card CSSProperties + CSS variables.
  • LOCKSMITH_FORM_KEYFRAMES_CSS — spinner animation (injected by LocksmithFormSurface / 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 + poweredByLocksmith boolean; when true, appends LocksmithPoweredBy.

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/branding on Locksmith (same API key) returns { poweredByLocksmith }, or set accountPlan on createLocksmithRouteHandlers to 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.
No opt-out on Free

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)

PropDescription
theme, classNamesStyling (see above).
title, descriptionCard header copy.
emailLabel, passwordLabel, submitLabelField / button labels.
forgotPasswordLabel, forgotPasswordHrefForgot link.
signUpLabel, signUpHref, signUpLinkTextSign-up promo row.
socialProvidersArray of SocialProvider: google, github, apple, microsoft, x, discord, linkedin.
onSocialLoginIf set, social buttons call this instead of default startOAuth.
oauthRedirectUrlPassed to startOAuth for each provider (must be absolute URL of your callback page).
showSocialDividerDefault true; “or continue with email” rule.
onSuccessFires when password sign-in completes without MFA. If MFA required, show LocksmithTotpForm first.
onErrorValidation / 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

PropDescription
showNameFieldAdds name → meta.name.
showConfirmPasswordSecond password field with match validation.
showPasswordStrengthDefault true; visual rules (length, upper, lower, number).
termsTextReactNode below submit (e.g. checkbox copy).
socialProviders, onSocialLogin, oauthRedirectUrl, showSocialDividerSame idea as sign-in.
onSuccess, onErrorAfter 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

PropDescription
codeLength6 or 8 (default 6).
autoSubmitDefault true; submits when code length reached.
onBackShows ghost “back” control when set.
title, description, label, submitLabel, backLabelCopy.

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

  1. startOAuth / initiateOAuth → redirect browser to provider.
  2. User returns to your app with ?code=.
  3. completeOAuthExchange / exchangeOAuthCode → BFF sets cookies.

Premade pieces

ComponentRole
LocksmithOAuthSignInButtonSingle provider button inside a small LocksmithFormSurface; uses startOAuth; optional Powered by footer.
LocksmithOAuthCallbackReads code from useSearchParams, exchanges it, shows loading/success/error states, then router.replace(redirectTo). Wraps Suspense.
LocksmithSignInForm / LocksmithSignUpFormsocialProviders + LocksmithSocialButtonGroup styling.
LocksmithSocialButton, LocksmithSocialButtonGroupLower-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.


Pro feature on Locksmith: OIDC authorize redirects the user to your loginUrl with query params:

ParamMeaning
request_tokenOpaque token for oidc/grant.
app_nameClient display name.
scopeSpace-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.

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, then window.location.reload() so LocksmithAuthProvider picks 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-id
  • x-locksmith-user-email
  • x-locksmith-user-role

On missing/invalid token, returns 401 JSON { error }.

middleware.ts
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)
MethodLocksmith API
signUp, signIn, completeSignInTotp, signOut, refresh, getUserCore auth
getSdkBrandingGET /api/auth/sdk/branding
verifyAccessTokenLocal JWT verify (RS256; default issuer https://getlocksmith.dev)
totpStart, totpConfirm, totpDisableAuthenticator enrollment (bearer)
passkeyRegisterOptions, passkeyRegisterVerify, passkeyLoginOptions, passkeyLoginVerify, passkeyList, passkeyRevokeWebAuthn
sendMagicLink, verifyMagicLinkMagic link
sendPasswordReset, updatePasswordPassword reset
initiateOAuth, exchangeOAuthCodeSocial OAuth (server-side)
completeOidcGrantOIDC 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:

  • LocksmithAuthErrorcode, message, status.
  • isSignInRequiresTotp — narrows signIn result.
  • 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”

GoalPremadeManual
Email/password + MFALocksmithSignInForm + LocksmithTotpFormsignInWithPassword + completeTotp
Sign upLocksmithSignUpFormsignUp
Social login buttonLocksmithOAuthSignInButton or social row on formsstartOAuth / bff.initiateOAuth
OAuth returnLocksmithOAuthCallbackcompleteOAuthExchange in useEffect
OIDC consentLocksmithOidcConsentcompleteOidcConsent / bff.oidcGrant
PasskeyLocksmithPasskeySignInButtonbff.passkeyLoginOptions + @simplewebauthn/browser + bff.passkeyLoginVerify
Custom styled formLocksmithFormShell + theme helpersSame hooks / LocksmithBffClient
Server CRUD / adminN/ALocksmithServerClient only

Reference