Skip to content

feat: add createWorkOS factory for type-safe public/confidential clients [v8]#1440

Merged
nicknisi merged 4 commits intoversion-8from
nicknisi/workos-factory
Jan 8, 2026
Merged

feat: add createWorkOS factory for type-safe public/confidential clients [v8]#1440
nicknisi merged 4 commits intoversion-8from
nicknisi/workos-factory

Conversation

@nicknisi
Copy link
Member

@nicknisi nicknisi commented Jan 8, 2026

Summary

Adds a createWorkOS() factory function that provides compile-time type safety for public vs confidential clients.

When the SDK is instantiated with only a clientId (no API key), most methods throw ApiKeyRequiredException at runtime. However, TypeScript can't warn you at compile time because the WorkOS class type is static—all methods appear available regardless of how it was constructed.

The factory solves this by using function overloads that return different types based on input:

  • createWorkOS({ clientId }) → returns PublicWorkOS (narrow type exposing only PKCE-compatible methods)
  • createWorkOS({ apiKey, ... }) → returns full WorkOS type

Example

import { createWorkOS } from '@workos-inc/node';

// Public client (Electron, mobile, CLI) - only PKCE methods available
const publicClient = createWorkOS({ clientId: 'client_123' });

// ✅ These work - available on PublicWorkOS
const { url, codeVerifier } = await publicClient.userManagement.getAuthorizationUrlWithPKCE({
  provider: 'authkit',
  redirectUri: 'myapp://callback',
});

const auth = await publicClient.userManagement.authenticateWithCodeAndVerifier({
  code: authCode,
  codeVerifier,
});

// ❌ TypeScript error - not available on PublicWorkOS
publicClient.userManagement.listUsers();
publicClient.organizations.list();

// Confidential client (server) - full access
const serverClient = createWorkOS({
  apiKey: process.env.WORKOS_API_KEY!,
  clientId: 'client_123',
});

// ✅ All methods available
await serverClient.userManagement.listUsers();
await serverClient.organizations.list();

Design

  • No runtime changes - The factory returns a standard WorkOS instance; type narrowing is purely compile-time
  • Uses Pick<> for maintainability - Public method names are listed once as a string union; types are derived automatically
  • Ignores env vars - Factory uses only explicit input for predictable types. Users who want env var convenience can use new WorkOS() or pass process.env.WORKOS_API_KEY explicitly
  • Backward compatible - new WorkOS() continues to work unchanged

Adds a factory function with overloaded signatures that returns different
types based on input credentials:
- `createWorkOS({ clientId })` returns `PublicWorkOS` (PKCE methods only)
- `createWorkOS({ apiKey })` returns full `WorkOS` type

This provides compile-time safety for public clients (Electron, mobile, CLI)
that cannot use API-key-required methods.
@nicknisi nicknisi changed the title feat: add createWorkOS factory for type-safe public/confidential clients feat: add createWorkOS factory for type-safe public/confidential clients [v8] Jan 8, 2026
These are server-side concerns (receiving callbacks from WorkOS),
not relevant to public client use cases (PKCE auth flows).
@nicknisi nicknisi marked this pull request as ready for review January 8, 2026 17:24
@nicknisi nicknisi requested a review from a team as a code owner January 8, 2026 17:24
@nicknisi nicknisi requested review from amygdalama and removed request for a team January 8, 2026 17:24
@imkesin imkesin self-requested a review January 8, 2026 17:38
Comment on lines +10 to +15
beforeEach(() => {
jest.resetModules();
process.env = { ...OLD_ENV };
delete process.env.WORKOS_API_KEY;
delete process.env.WORKOS_CLIENT_ID;
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we want to be sure that createWorkOS doesn't rely on these, there could be some approach with a Proxy to throw an error when we try to get certain variables.

(Obviously would be more work + gross)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah IMO it's not worth it. This give you the compile-time safety but if you have the env var set it'll still work but at that point you're ignoring TS errors or using JS and not worried about the factory function's return type, anyway.

@nicknisi nicknisi force-pushed the nicknisi/workos-factory branch from db7be12 to b3222f4 Compare January 8, 2026 19:57
- Remove string apiKey overload for cleaner, unambiguous signature
- Add @ts-expect-error tests proving PublicWorkOS type narrowing works
- Add test verifying factory types based on explicit options, not env vars
- Document apiKey?: never discriminant pattern
@nicknisi nicknisi merged commit e6004c3 into version-8 Jan 8, 2026
8 checks passed
@nicknisi nicknisi deleted the nicknisi/workos-factory branch January 8, 2026 20:03
nicknisi added a commit that referenced this pull request Jan 9, 2026
…nts [v8] (#1440)

## Summary

Adds a `createWorkOS()` factory function that provides **compile-time
type safety** for public vs confidential clients.

When the SDK is instantiated with only a `clientId` (no API key), most
methods throw `ApiKeyRequiredException` at runtime. However, TypeScript
can't warn you at compile time because the `WorkOS` class type is
static—all methods appear available regardless of how it was
constructed.

The factory solves this by using function overloads that return
different types based on input:

- `createWorkOS({ clientId })` → returns `PublicWorkOS` (narrow type
exposing only PKCE-compatible methods)
- `createWorkOS({ apiKey, ... })` → returns full `WorkOS` type

## Example

```typescript
import { createWorkOS } from '@workos-inc/node';

// Public client (Electron, mobile, CLI) - only PKCE methods available
const publicClient = createWorkOS({ clientId: 'client_123' });

// ✅ These work - available on PublicWorkOS
const { url, codeVerifier } = await publicClient.userManagement.getAuthorizationUrlWithPKCE({
  provider: 'authkit',
  redirectUri: 'myapp://callback',
});

const auth = await publicClient.userManagement.authenticateWithCodeAndVerifier({
  code: authCode,
  codeVerifier,
});

// ❌ TypeScript error - not available on PublicWorkOS
publicClient.userManagement.listUsers();
publicClient.organizations.list();

// Confidential client (server) - full access
const serverClient = createWorkOS({
  apiKey: process.env.WORKOS_API_KEY!,
  clientId: 'client_123',
});

// ✅ All methods available
await serverClient.userManagement.listUsers();
await serverClient.organizations.list();
```

## Design

- **No runtime changes** - The factory returns a standard `WorkOS`
instance; type narrowing is purely compile-time
- **Uses `Pick<>` for maintainability** - Public method names are listed
once as a string union; types are derived automatically
- **Ignores env vars** - Factory uses only explicit input for
predictable types. Users who want env var convenience can use `new
WorkOS()` or pass `process.env.WORKOS_API_KEY` explicitly
- **Backward compatible** - `new WorkOS()` continues to work unchanged
nicknisi added a commit that referenced this pull request Jan 12, 2026
…nts [v8] (#1440)

## Summary

Adds a `createWorkOS()` factory function that provides **compile-time
type safety** for public vs confidential clients.

When the SDK is instantiated with only a `clientId` (no API key), most
methods throw `ApiKeyRequiredException` at runtime. However, TypeScript
can't warn you at compile time because the `WorkOS` class type is
static—all methods appear available regardless of how it was
constructed.

The factory solves this by using function overloads that return
different types based on input:

- `createWorkOS({ clientId })` → returns `PublicWorkOS` (narrow type
exposing only PKCE-compatible methods)
- `createWorkOS({ apiKey, ... })` → returns full `WorkOS` type

## Example

```typescript
import { createWorkOS } from '@workos-inc/node';

// Public client (Electron, mobile, CLI) - only PKCE methods available
const publicClient = createWorkOS({ clientId: 'client_123' });

// ✅ These work - available on PublicWorkOS
const { url, codeVerifier } = await publicClient.userManagement.getAuthorizationUrlWithPKCE({
  provider: 'authkit',
  redirectUri: 'myapp://callback',
});

const auth = await publicClient.userManagement.authenticateWithCodeAndVerifier({
  code: authCode,
  codeVerifier,
});

// ❌ TypeScript error - not available on PublicWorkOS
publicClient.userManagement.listUsers();
publicClient.organizations.list();

// Confidential client (server) - full access
const serverClient = createWorkOS({
  apiKey: process.env.WORKOS_API_KEY!,
  clientId: 'client_123',
});

// ✅ All methods available
await serverClient.userManagement.listUsers();
await serverClient.organizations.list();
```

## Design

- **No runtime changes** - The factory returns a standard `WorkOS`
instance; type narrowing is purely compile-time
- **Uses `Pick<>` for maintainability** - Public method names are listed
once as a string union; types are derived automatically
- **Ignores env vars** - Factory uses only explicit input for
predictable types. Users who want env var convenience can use `new
WorkOS()` or pass `process.env.WORKOS_API_KEY` explicitly
- **Backward compatible** - `new WorkOS()` continues to work unchanged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants