WorkOS, ready-made SSO and SCIM for enterprise customers
WorkOS is a set of APIs for the features a large customer's IT department demands before signing: single sign-on over SAML and OIDC, user directory synchronisation over SCIM, and audit event logs. It bills differently from the rest of the authentication market, per identity provider connection rather than per active user, and that single difference decides the whole invoice.
AuthKit and the raw APIs are two different products
The biggest misunderstanding around WorkOS comes from two things with different purposes and different billing models being sold under one name. The SSO documentation opens literally with a choice between two integration paths and gives no hint that either is the default.
The first path is AuthKit. It is a complete authentication layer with a hosted sign-in interface, password support, one-time codes sent by email, passkeys, two-factor authentication and roles. In code it lives under the workos.userManagement namespace, and on the price list under the User Management heading. If you are building a product from scratch, AuthKit sits in exactly the same place as Clerk or Kinde, and those are what it should be compared against.
The second path is the raw Enterprise SSO API. The documentation describes it as middleware and states outright that the service deliberately does not manage your application's user database. You get an authorization URL, a profile carrying identity provider data on the way back, and there its role ends. The session, the accounts table, the organisation assignment and everything downstream you write yourself. In code this is the workos.sso namespace, on the price list the Enterprise SSO line billed per connection.
On top of that come products used regardless of the chosen path: Directory Sync, which is SCIM, Audit Logs, Admin Portal, where the customer's administrator configures their identity provider without your involvement, and Radar for abuse detection. Each has a separate line on the invoice.
The practical consequence is that an application using AuthKit and Directory Sync at the same time pays two kinds of fee at once: for active users and for directory connections. Mixing both namespaces in code, meanwhile, leaves you with two unrelated notions of a user: User from User Management and Profile from SSO, with entirely different field sets.
Version, licence and project health
The Node client is called @workos-inc/node and sits at version 10.10.0, published on 13 August 2026. The matching v10.10.0 release in the workos/workos-node repository carries the same date. The repository responds with code 200 and is not archived.
I checked the licence against three independent sources and this time it comes out exemplary. The LICENSE file on the main branch contains the MIT text with a 2021 copyright notice. The license field in the npm registry reads MIT. The published package, unpacked from its archive, holds twenty five files, among them package/LICENSE with the same MIT text and eight real JavaScript files with code in CommonJS, ESM and a separate Worker entry point. It is neither a name placeholder nor a metapackage without contents.
One discrepancy sits on the Python side and matters only to automated licence compliance tooling. The workos package on PyPI at version 10.2.0 from 11 August 2026 declares License-Expression: MIT in its metadata, and the workos/workos-python repository has a LICENSE file with the MIT text and a 2024 notice. The installation wheel itself, however, contains only WHEEL, METADATA and RECORD inside its dist-info directory, with no licence file at all. The code is there, seven hundred and eighty three source files, but a scanner reading package contents rather than metadata will not find licence text inside.
A naming trap: the npm package under the bare name workos at version 0.21.1 is not a client library but a command line tool from the workos/cli repository. Installing under that name gives you something entirely different from the SDK.
The release rhythm is dense and uneven. Versions 10.4.0 on 18 June, 10.4.1 on 23 June, 10.5.0 on 24 June and both 10.6.0 and 10.7.0 on 25 June 2026 shipped within a week, after which came a gap until 10.8.0 on 17 July, 10.9.0 on 30 July and 10.10.0 on 13 August. The Next.js layer, @workos-inc/authkit-nextjs, sits at version 4.3.1 from 30 July 2026, also under MIT, with a licence file inside the package.
Two technical requirements are easy to miss. @workos-inc/node declares engines.node as >=22.11.0, so on older Node the install will either warn or fail outright, depending on package manager settings. @workos-inc/authkit-nextjs declares peer dependencies on next in the range ^13.5.9 || ^14.2.26 || ^15.2.3 || ^16, on react in the range ^18.0 || ^19.0.0 and on @workos-inc/node in the range ^9.0.0 || ^10.0.0. The narrower windows on Next 13 and 14 follow security patches rather than whim.
The point that matters most for risk assessment: the MIT licence covers the client libraries, not the service. The WorkOS server is closed and hosted solely by the vendor. There is no self-installable variant, no visibility into the code parsing SAML assertions and no exit route other than rewriting the integration.
AuthKit in Next.js from scratch
Configuring AuthKit in a Next application rests on four environment variables the library treats as mandatory, plus a few optional ones governing the session cookie.
# mandatory
WORKOS_API_KEY=sk_test_xxxxxxxx
WORKOS_CLIENT_ID=client_xxxxxxxx
WORKOS_COOKIE_PASSWORD=at_least_32_characters_of_random_string
WORKOS_REDIRECT_URI=http://localhost:3000/callback
# optional, governing the session cookie
WORKOS_COOKIE_NAME=wos-session
WORKOS_COOKIE_MAX_AGE=34560000
WORKOS_COOKIE_SAMESITE=lax
WORKOS_COOKIE_DOMAIN=.example.comInstallation and the first wiring look like this.
npm install @workos-inc/node@10.10.0 @workos-inc/authkit-nextjs@4.3.1
node --version # must be at least 22.11.0The middleware intercepts requests, refreshes the access token before expiry and redirects unauthenticated visitors if you enable middlewareAuth. The unauthenticatedPaths field is required in that object alongside enabled.
// middleware.ts
import { authkitMiddleware } from '@workos-inc/authkit-nextjs'
export default authkitMiddleware({
middlewareAuth: {
enabled: true,
unauthenticatedPaths: ['/', '/pricing', '/sign-in']
},
redirectUri: process.env.WORKOS_REDIRECT_URI,
refreshBufferSeconds: 60,
debug: false
})
export const config = {
matcher: ['/((?!_next/static|_next/image|favicon.ico).*)']
}The post-login callback route comes down to a single function. The returnPathname option says where to send the user, and onSuccess gives you a moment to persist your own record in the database.
// app/callback/route.ts
import { handleAuth } from '@workos-inc/authkit-nextjs'
export const GET = handleAuth({
returnPathname: '/dashboard',
onSuccess: async ({ user, organizationId, accessToken }) => {
await saveLocalProfile({
workosUserId: user.id,
email: user.email,
organizationId
})
}
})In server components the session is read through withAuth. Calling it with ensureSignedIn: true returns UserInfo and redirects when no session exists. Calling it without arguments returns either UserInfo or NoUserInfo with the user field set to null, so the type has to be narrowed by hand.
// app/dashboard/page.tsx
import { withAuth, signOut } from '@workos-inc/authkit-nextjs'
export default async function Dashboard() {
const { user, organizationId, role, permissions, sessionId } = await withAuth({
ensureSignedIn: true
})
const canInvite = permissions?.includes('members:invite') ?? false
return (
<main>
<h1>{user.email}</h1>
<p>Organisation: {organizationId ?? 'none'}</p>
<p>Role: {role ?? 'none'}</p>
<p>Session: {sessionId}</p>
{canInvite && <a href="/invitations">Invite someone</a>}
<form action={async () => { 'use server'; await signOut({ returnTo: '/' }) }}>
<button type="submit">Sign out</button>
</form>
</main>
)
}The permissions field on UserInfo is optional and appears only once you define roles and permissions in the WorkOS dashboard. Without that you get undefined rather than an empty array, which is a frequent source of bugs in access checks.
Raw SSO bolted onto your own sign-in
If you already have working sign-in and only want to add SAML for enterprise customers, you reach for the workos.sso namespace. The authorization URL is built from one of three mutually exclusive fields: connection, organization or provider. Passing two at once will not pass type checking.
import { WorkOS } from '@workos-inc/node'
const workos = new WorkOS(process.env.WORKOS_API_KEY, {
clientId: process.env.WORKOS_CLIENT_ID,
maxRetries: 3,
timeout: 30_000
})
const url = workos.sso.getAuthorizationUrl({
organization: 'org_01H...',
clientId: process.env.WORKOS_CLIENT_ID!,
redirectUri: 'https://my-app.com/sso/callback',
state: signedState,
loginHint: 'anna@customer.com'
})On the way back you exchange the code for a profile. The response carries accessToken and profile, and the profile itself holds id, idpId, organizationId, connectionId, connectionType, email, name, firstName, lastName and rawAttributes with the raw identity provider payload.
const { profile, accessToken } = await workos.sso.getProfileAndToken({
code: codeFromUrl,
clientId: process.env.WORKOS_CLIENT_ID!
})
// WorkOS keeps no user database in this mode
const account = await db.users.upsert({
where: { email: profile.email },
update: { lastLoginAt: new Date() },
create: {
email: profile.email,
firstName: profile.firstName,
lastName: profile.lastName,
organizationId: profile.organizationId,
loginSource: profile.connectionType
}
})
await createOwnSession(account.id)The connectionType values form a named set holding, among others, OktaSAML, AzureSAML, GoogleSAML, EntraIdOIDC, JumpCloudSAML, GenericSAML and GenericOIDC. That last pair earns its keep when a customer runs something nobody anticipated on the list.
For public clients, meaning command line tools, Electron and mobile applications, there is getAuthorizationUrlWithPKCE, which returns an object with url, state and codeVerifier fields. The last one has to be stored and handed to getProfileAndToken along with the code.
Directory Sync, Audit Logs and webhooks
Directory Sync receives data from the customer's directory over SCIM and exposes it under the workos.directorySync namespace. The listUsers method takes directory or group and returns an object with automatic pagination.
const users = await workos.directorySync.listUsers({
directory: 'directory_01H...',
limit: 100
})
for (const u of users.data) {
console.log({
id: u.id,
idpId: u.idpId,
email: u.email,
firstName: u.firstName,
lastName: u.lastName,
state: u.state, // 'active' or 'inactive'
organizationId: u.organizationId,
groups: u.groups.map((g) => g.name),
attributes: u.customAttributes
})
}The state field takes only the values active and inactive. Removing a person from the customer's directory does not delete the record on your side, it flips that flag, so revoking access is something you implement yourself.
Audit Logs is a stream of compliance events the enterprise customer later exports or forwards to their own SIEM system. The createEvent signature is positional: first the organisation identifier, then the event object.
await workos.auditLogs.createEvent(
'org_01H...',
{
action: 'invoice.downloaded',
occurredAt: new Date(),
actor: {
id: 'user_01H...',
name: 'Anna Kowalska',
type: 'user'
},
targets: [
{ id: 'inv_2026_08_412', type: 'invoice', name: 'INV/2026/08/412' }
],
context: {
location: '203.0.113.14',
userAgent: 'Mozilla/5.0'
},
metadata: { amount: 12400, currency: 'PLN' }
},
{ idempotencyKey: crypto.randomUUID() }
)The idempotency key expires after twenty four hours, and repeating a request with the same key returns the same response, which saves you from double entries on retries. Changes on the directory and user side arrive by webhook, whose signature constructEvent verifies.
const event = await workos.webhooks.constructEvent({
payload: requestBody,
sigHeader: headers['workos-signature'],
secret: process.env.WORKOS_WEBHOOK_SECRET!,
tolerance: 300
})
if (event.event === 'dsync.user.deleted') {
await revokeAccess(event.data.idpId)
}The pricing worked out on an example
The workos.com/pricing page renders without JavaScript, so the numbers can be read straight from the server response. Below are all the lines it quotes at the time of writing.
| Line | Price | Unit |
|---|---|---|
| User Management and AuthKit up to 1M monthly active users | 0 USD | month |
| Each additional 1M monthly active users | 2500 USD | month |
| Enterprise SSO, connections 1 to 15 | 125 USD | connection per month |
| Enterprise SSO, connections 16 to 30 | 100 USD, that is 20 percent off | connection per month |
| Enterprise SSO, connections 31 to 50 | 80 USD, that is 36 percent off | connection per month |
| Enterprise SSO, connections 51 to 100 | 65 USD, that is 48 percent off | connection per month |
| Directory Sync | the same four tiers as SSO | connection per month |
| Audit Logs, stream to a SIEM system | 125 USD | connection per month |
| Audit Logs, event retention | 99 USD | million events per month |
| Radar, first 1000 checks | 0 USD | month |
| Radar, further checks | 100 USD | 50 thousand checks per month |
| Custom domain for AuthKit and Admin Portal | 99 USD | month |
The percentages in the tier table match the amounts: 125 times 0.8 gives 100, times 0.64 gives 80, times 0.52 gives 65. There is no typo or rounding here that would break the arithmetic.
The key sentence from the questions section on the same page reads as follows: every connection costs the same regardless of identity provider, directory service and the number of end users. A connection is the relationship with one group of end users, which in practice means one enterprise customer. A customer with five employees and a customer with five thousand pay identically.
Let us work the promised example. Ten enterprise customers with their own SSO means ten connections in the first tier, so 10 times 125, which gives 1250 USD per month and 15,000 USD per year. If each also wants directory synchronisation, another ten Directory Sync connections join in, a further 1250 USD per month. Together that is 2500 USD per month and 30,000 USD per year. Adding an audit log stream to one SIEM system at 125 USD and a custom domain at 99 USD closes the invoice at 2724 USD per month, that is 32,688 USD per year. The active users of those ten companies change nothing in this calculation until you cross one million per month.
Three things the page does not settle, and which have to be confirmed with the vendor before signing. First, the tiers do not say whether the discount covers all connections or only those above the threshold. At twenty connections the first reading gives 20 times 100, that is 2000 USD, and the second 15 times 125 plus 5 times 100, that is 2375 USD. The gap is 375 USD per month and cannot be derived from the page text. Second, the line about each additional million users does not say whether a partial million is prorated or rounded up to the full 2500 USD. Third, the annual plan named Annual Credits has no published price and no discount rate, only a button to book a call, so comparing a monthly invoice against an annual one is impossible without contacting sales.
Two things the page settles unambiguously in your favour. The staging environment is free and all products are available in it, so connections set up for testing generate no invoice. A payment card is needed only when moving to production. An active user is defined as one who performed any action in a given calendar month, for example a sign-up, a sign-in or a profile update.
WorkOS against Clerk, Auth0, Kinde and Better Auth
The comparison only makes sense when it lines up the right products. Raw SSO from WorkOS has no counterpart at the others, because there SSO is a plan feature rather than a separately billed service.
| Tool | Billing unit | Where session data lives | What it targets |
|---|---|---|---|
| WorkOS AuthKit | monthly active user, first million free | at the vendor | B2B products sold to companies |
| WorkOS Enterprise SSO | identity provider connection | with you, WorkOS returns the profile only | adding SAML to existing sign-in |
| Clerk | monthly active user | at the vendor | fast rollout with ready-made components |
| Auth0 | monthly active user | at the vendor | broad scope and a long deployment history |
| Kinde | active user, 10.5 thousand threshold on every plan | at the vendor | small teams and early products |
| Better Auth | no fee, library under an open licence | in your own database | full control and no lock-in |
The difference this table brings out comes down to asking what grows alongside the invoice. At Clerk, Auth0 and Kinde it is the number of people signing in. At WorkOS it is the number of companies you have sold to. A consumer product with two hundred thousand users and zero enterprise customers pays nothing at WorkOS and plenty at the competition. A B2B product with a thousand users spread across forty companies pays between 3200 and 4175 USD per month at WorkOS for the SSO connections alone, depending on how the tier discount is read, and at the competition whatever a thousand active users costs, which is incomparably less, assuming the plan covers SAML at all.
Better Auth stands apart in this line-up because it is not a service. You keep the user and session tables yourself, in Supabase or any other database, and pay for SAML and SCIM with your team's time instead of a subscription. That is the reverse side of the same choice: no invoice growing with customer count, in exchange for maintaining an integration with every further identity provider.
What WorkOS will not handle for you
The lock-in is total and cannot be softened. Only the client libraries are open under MIT. The service that parses SAML assertions, stores connection configurations and issues tokens runs at the vendor alone. There is no self-installable variant and no path for exporting configuration to another tool. Leaving WorkOS means setting up every connection with every customer from scratch, which at thirty customers is a quarter-long project, on your side and on the side of those customers' IT departments.
SAML complexity does not disappear, it shifts by one step. Admin Portal lets the customer's administrator enter their identity provider metadata themselves, which genuinely saves an email exchange. But mapping attributes onto roles in your application, handling signing certificate rotation, deciding whether provider-initiated sign-in is acceptable, and the rules for assigning new people to organisations all stay with you. Directory Sync brings data, not decisions: you get a list of groups and must work out yourself that the Finance-Admins group maps onto the invoice permission.
The invoice grows in steps rather than smoothly. Every signed enterprise customer with SSO is plus 125 USD per month from the day the connection goes up, regardless of what that customer pays you. For a customer paying 300 USD per month that is forty percent of the revenue from them handed to one vendor. For a customer paying 10,000 USD it is noise. That ratio has to be computed against your own price list before SSO lands in the offer as a free extra.
For a simple consumer product WorkOS is overkill. A million active users at no charge sounds like the best offer on the market and in raw numbers it is, but you pay for it with coupling to a closed service and an organisation model you will never use. If your product has no notion of the company a user belongs to, half of the WorkOS API is dead weight.
Common mistakes
Installing the workos package instead of @workos-inc/node. The former name belongs to a command line tool from a different repository and contains no API client.
Mixing the workos.sso and workos.userManagement namespaces in one sign-in path. They return different objects, Profile and User, with different fields, and they are billed separately. Pick one path and stay on it.
Checking permissions through permissions.includes(...) without a guard. The permissions field on UserInfo is optional and holds undefined when no roles are configured, so calling a method on it will crash the server component.
Estimating cost by user count. In Enterprise SSO mode what counts is the number of connections, and a customer with five thousand employees costs exactly the same as a customer with five.
Counting one customer as one connection when they use both SSO and Directory Sync. Those are two separately billed connections, so 250 USD per month rather than 125.
Drawing conclusions from the number of connections set up in the staging environment. Staging is free and does not reflect the production invoice.
Installing on Node older than 22.11.0. The engines field in the package is set hard, and with a strict package manager configuration the install will fail.
Relying on the contents of the PyPI wheel during a licence audit. The workos package for Python carries no licence file, only the License-Expression metadata field and a LICENSE file in the repository.
FAQ
Is AuthKit free?
Up to one million monthly active users, yes, according to the vendor's price list. An active user is one who performed any action in a given calendar month, for example a sign-up, a sign-in or a profile update. Above that threshold each further million costs 2500 USD per month, though the page does not specify whether a partial million is prorated.
What will ten enterprise customers with their own SSO cost me?
Ten connections in the first tier, so 10 times 125 USD, gives 1250 USD per month and 15,000 USD per year. If those same customers are to use directory synchronisation, that is another ten connections and a second 1250 USD per month. The headcount at those companies does not affect the amount.
Can WorkOS be run on my own server?
No. Only the client libraries for Node, Python, Ruby, Go, PHP and Elixir are published under MIT. The service processing sign-in is closed and hosted solely by the vendor, with no locally installable variant.
How does a connection differ from a user?
A connection is the relationship between WorkOS and one group of end users, which in practice means one enterprise customer with one identity provider. The price list states outright that every connection costs the same regardless of provider, directory type and the number of users on the other side.
Does WorkOS make sense for an ordinary consumer application?
Rarely. If the product has no notion of an organisation a user belongs to, you use a small slice of the capability and pay for it with permanent coupling to a closed service. For that profile a library keeping data on your side, or a service billed per user, works out simpler.
Does the MIT licence cover all of WorkOS?
No, it covers the client code only. The @workos-inc/node package at version 10.10.0 has MIT in the license field in the npm registry, a LICENSE file inside the package and the same file in the repository, so on that count it is clean. The server side of the service has no published code and no open licence.