Skip to content

Server SDKs and Adapters

Seamless Auth currently ships two official server-side packages:

  • @seamless-auth/core
  • @seamless-auth/express

These packages help your backend become the browser-facing security boundary between your app and the private auth server.


PackageUse it whenWhat it gives you
@seamless-auth/expressYour backend is ExpressOfficial adapter, mounted auth routes, middleware, role guards
@seamless-auth/coreYou need a custom adapter or framework integrationFramework-agnostic auth, cookie, OAuth, and role primitives

If you already run Express, start with @seamless-auth/express.


Your backend integration layer is responsible for:

  • receiving browser requests
  • owning signed, HTTP-only auth cookies
  • validating authenticated state
  • enforcing authorization
  • proxying auth flows to the private auth API
  • talking to the auth server with service credentials

That architecture is intentional. Browser cookies should not be forwarded directly to seamless-auth-api as if the auth server were your public application API.


The Express package is the supported batteries-included server integration.

It provides:

  • createSeamlessAuthServer(...)
  • requireAuth({ cookieSecret })
  • requireRole(roleOrRoles)
  • getSeamlessUser(...)
  • createEnsureCookiesMiddleware(...)
  • scoped role helpers re-exported from @seamless-auth/core
import express from 'express'
import cookieParser from 'cookie-parser'
import createSeamlessAuthServer, {
requireAuth,
requireRole,
type SeamlessAuthServerOptions,
} from '@seamless-auth/express'
const app = express()
app.use(express.json())
app.use(cookieParser())
const authOptions: SeamlessAuthServerOptions = {
authServerUrl: process.env.AUTH_SERVER_URL!,
cookieSecret: process.env.COOKIE_SIGNING_KEY!,
serviceSecret: process.env.API_SERVICE_TOKEN!,
issuer: process.env.APP_ORIGIN!,
audience: process.env.AUTH_SERVER_URL!,
jwksKid: process.env.JWKS_KID,
}
app.use('/auth', createSeamlessAuthServer(authOptions))
app.get('/api/me', requireAuth({ cookieSecret: authOptions.cookieSecret }), (req, res) => {
res.json({ user: req.user })
})
app.get(
'/admin/users',
requireAuth({ cookieSecret: authOptions.cookieSecret }),
requireRole('admin:read'),
(_req, res) => {
res.json({ ok: true })
},
)
OptionPurpose
authServerUrlBase URL for the Seamless Auth instance
cookieSecretSecret used to verify and sign adapter cookies
serviceSecretShared service secret for machine-to-machine calls to the auth instance
issuerIssuer/origin value for app-issued cookies
audienceExpected audience, usually the auth server URL

Optional values include jwksKid, cookieDomain, custom cookie names, and messaging.

The options are the same on both paths. Only the values differ, and where the service secret comes from.

OptionManagedSelf-hosted
authServerUrlYour Auth Server URL, for example https://<app-id>.seamlessauth.comThe auth API, for example http://localhost:5312
audienceThe same Auth Server URLThe same auth server URL
serviceSecretA service token generated in the portal (shown once)The auth API’s API_SERVICE_TOKEN from your local stack

On the managed path, generate the service token from your application’s Auth API Configuration card and read it from a server-side environment variable. The portal’s connection snippet reads it from SEAMLESS_SERVICE_TOKEN. See the Managed Quickstart. The serviceSecret is never sent raw: the adapter uses it to sign short-lived service tokens for calls to the auth instance.


When mounted at /auth, the Express adapter exposes auth-compatible browser routes such as:

  • /auth/login
  • /auth/oauth/providers
  • /auth/oauth/:providerId/start
  • /auth/oauth/:providerId/callback
  • /auth/webAuthn/login/start
  • /auth/webAuthn/login/finish
  • /auth/webAuthn/register/start
  • /auth/webAuthn/register/finish
  • /auth/otp/*
  • /auth/magic-link*
  • /auth/step-up/*
  • /auth/organizations/*
  • /auth/users/me
  • /auth/users/credentials
  • DELETE /auth/logout
  • DELETE /auth/logout/all
  • /auth/admin/*
  • /auth/internal/*
  • /auth/system-config/*

The adapter route spelling is webAuthn; the raw auth API route group is /webauthn.


requireAuth({ cookieSecret }) verifies the signed access cookie and attaches the decoded payload to req.user.

app.get('/api/profile', requireAuth({ cookieSecret }), (req, res) => {
res.json({ user: req.user })
})

This middleware verifies an already-issued access cookie. Session refresh is handled by the adapter’s cookie middleware.

With authServerUrl and audience it also accepts the auth API’s own access token in Authorization: Bearer, verified against the auth API’s JWKS (issuer, audience, expiry and typ: "access"). This is how a native client authenticates; see Bearer Transport For Native Clients. A cookie wins when both are present, and the two options must be given together.

const guard = requireAuth({
cookieSecret,
authServerUrl: process.env.AUTH_SERVER_URL,
audience: process.env.AUTH_SERVER_URL,
})

On the bearer path req.user.email and req.user.phone are not set, because the access token does not carry them. getSeamlessUser(req, options) loads the profile for either credential.

requireRole(required) performs authorization only. It expects requireAuth to have populated req.user.

app.post(
'/settings',
requireAuth({ cookieSecret }),
requireRole(['admin:write', 'settings:write']),
updateSettings,
)

Scoped role compatibility:

Granted roleSatisfies
adminadmin, admin:read, admin:write
admin:writeadmin:write, admin:read
admin:readadmin:read only

Browsers get the cookie contract: the adapter holds the auth API’s tokens in signed httpOnly cookies and the page never sees them. A native app has no cookie jar, so the same routes serve a second contract when a request carries the header x-seamless-auth-transport: bearer:

  • The client presents the token a route needs in Authorization: Bearer. Pre-auth routes (OTP, passkey login, magic link) take the ephemeral token /login or /registration/register returned; signed-in routes take the access token.
  • Session-issuing responses come back whole, token and refreshToken included, and no cookie is set. The adapter still verifies the auth API’s signature on the access token first.
  • POST /auth/refresh rotates the session: Authorization: Bearer <refreshToken> in, a new pair out. The auth API treats a replayed refresh token as theft and revokes the chain (401 { "error": "refresh_token_reused" }), so a client refreshes once at a time.
  • Delivery mode, forwarded client IP and user agent, and the service token behave exactly as in cookie transport.

The header selects the transport rather than the presence of Authorization, because the first request of a flow carries no token in either. Requests without the header are unchanged.

POST /auth/login -> { token: <ephemeral>, loginMethods, ... }
POST /auth/otp/verify-login-email-otp -> { token: <access>, refreshToken, ... }
GET /auth/users/me Authorization: Bearer <access>
POST /auth/refresh Authorization: Bearer <refreshToken>

@seamless-auth/react-native speaks this contract for you; the React Native SDK page covers the client side. Your own routes accept the same access token through requireAuth with authServerUrl and audience, above.


The Express adapter can own email and SMS delivery for auth-message flows.

createSeamlessAuthServer({
authServerUrl,
cookieSecret,
serviceSecret,
issuer,
audience,
messaging: {
email: emailTransport,
sms: smsTransport,
handlers: {
magicLinkEmail: async (message) => {
await deliver(message)
},
},
},
})

When messaging is configured, the adapter requests external-delivery payloads from the auth API, delivers OTPs or one-time links locally, and strips those sensitive payloads before responding to the browser.


The server adapter proxies OAuth and organization routes so your browser app can stay on the same cookie boundary.

OAuth provider configuration belongs on seamless-auth-api, not in the adapter. Configure LOGIN_METHODS to include oauth, add oauth_providers, and store provider client secrets in env vars referenced by clientSecretEnv.

Organization routes let authenticated users create organizations, switch active organization, and manage organization members. Admin organization routes sit under /auth/admin/organizations/*.


The adapter proxies admin hardening endpoints used by the dashboard:

  • DELETE /auth/admin/sessions/by-id/:id
  • DELETE /auth/admin/sessions/:userId/revoke-all
  • POST /auth/admin/users/:userId/recovery/device-replacement

Device replacement recovery requires a fresh step-up session in the auth API. It revokes sessions, removes passkeys, disables TOTP credentials, and returns counts only.


The core package is lower level and framework-agnostic.

It is designed for:

  • custom adapters
  • future framework integrations
  • runtimes where you want full control over request and response handling

Key exports include:

  • ensureCookies(...)
  • refreshAccessToken(...)
  • verifyCookieJwt(...)
  • createServiceToken(...)
  • authFetch(...)
  • getSeamlessUser(...)
  • hasScopedRole(...)
  • login, registration, OTP, magic-link, OAuth, session, and organization handlers

Core functions return descriptive result objects. They do not set Express responses, read environment variables, or own framework-specific behavior.


Today, the strongest supported server-side path is Express.

The best-supported current combination is:

@seamless-auth/react or @seamless-auth/react-native
-> your frontend
@seamless-auth/express
-> your backend integration layer
seamless-auth-api
-> your auth engine

Use @seamless-auth/core when Express is not your runtime or you are building an adapter. The Fastify adapter, @seamless-auth/fastify, mirrors the Express one, bearer transport included.