Skip to content

Class based clients and tree-shakeable functions

New feature

Passlock now supports both class-based clients and standalone functions across the browser and server libraries.

Both styles are functionally equivalent. The difference is how you want to structure your integration:

  • use a Passlock class when you want to configure the client once
  • use standalone functions when you want tree-shakeable imports
  • use safe entry points when expected Passlock errors should return as typed result envelopes
  • use unsafe entry points when expected Passlock errors should throw and be handled with try / catch

For the full side-by-side reference, see Choose your code style.

Different teams want different integration shapes.

Some applications prefer a configured client object. That keeps tenancy and API configuration in one place, makes dependency injection straightforward, and reads naturally in service classes or route handlers.

Other applications care more about importing only the functions they use. That is especially useful in browser code, where a registration-only page should not have to pull in every other client method just because a class exists.

Passlock now supports both approaches directly, without forcing either code style on every project.

Class clients are the simplest option when you call Passlock from several places that share the same config.

import { Passlock } from "@passlock/browser";
const passlock = new Passlock({ tenancyId: "myTenancyId" });
const result = await passlock.registerPasskey({
username: "jdoe@example.com",
});
if (result.success) {
console.log(result.value.code);
} else {
console.log(result.error.message);
}

The same pattern works in server code:

import { Passlock } from "@passlock/server";
const passlock = new Passlock({
tenancyId: "myTenancyId",
apiKey: "myApiKey",
});
const result = await passlock.exchangeCode({ code });
if (result.success) {
console.log(result.value.id);
}

The class methods supply the constructor config for every operation. That reduces repetition and keeps application-level setup separate from per-call data.

Standalone functions are better when tree-shaking is important or when you prefer explicit call sites.

import { registerPasskey } from "@passlock/browser";
const result = await registerPasskey(
{ username: "jdoe@example.com" },
{ tenancyId: "myTenancyId" }
);
if (result.success) {
console.log(result.value.code);
}

The function import gives bundlers a smaller surface to analyze. It also makes each operation’s config dependency visible at the call site.

Server functions follow the same shape:

import { exchangeCode } from "@passlock/server";
const result = await exchangeCode(
{ code },
{ tenancyId: "myTenancyId", apiKey: "myApiKey" }
);
if (result.success) {
console.log(result.value.id);
}

The default package entry points are safe:

import { Passlock, registerPasskey } from "@passlock/browser";
import { Passlock as ServerPasslock, exchangeCode } from "@passlock/server";

Safe functions and methods return a result envelope for expected Passlock errors. You branch on result.success or result.failure, and TypeScript narrows the success and error paths.

That shape is useful when errors are part of normal product flow: unsupported passkeys, duplicate credentials, invalid one-time codes, expired challenges, or failed verification.

import { isPasskeyUnsupportedError, registerPasskey } from "@passlock/browser";
const result = await registerPasskey(
{ username: "jdoe@example.com" },
{ tenancyId: "myTenancyId" }
);
if (result.failure && isPasskeyUnsupportedError(result.error)) {
console.log("This device does not support passkeys");
}

Unexpected runtime failures can still throw. Safe entry points are about typed handling for expected Passlock outcomes.

Unsafe entry points live under /unsafe:

import { Passlock, registerPasskey } from "@passlock/browser/unsafe";
import { Passlock as ServerPasslock, exchangeCode } from "@passlock/server/unsafe";

Unsafe functions and methods resolve with the success payload directly. Expected Passlock errors reject the promise, so you handle them with try / catch and narrow with the exported type guards.

import {
isDuplicatePasskeyError,
registerPasskey,
} from "@passlock/browser/unsafe";
try {
const result = await registerPasskey(
{ username: "jdoe@example.com" },
{ tenancyId: "myTenancyId" }
);
console.log(result.code);
} catch (error) {
if (isDuplicatePasskeyError(error)) {
console.log("This passkey is already registered");
}
}

This style is useful when the rest of your application already uses thrown errors and centralized exception handling.

There is no special capability hidden behind one style. Pick the one that best fits the code you are writing.

ApproachBest fit
Class safeShared config and explicit result handling
Class unsafeShared config and thrown-error application style
Function safeTree-shakeable imports and explicit result handling
Function unsafeTree-shakeable imports and thrown-error application style

Our default recommendation is simple: start with the safe Passlock class for application code, then move hot browser paths to standalone safe functions when bundle size matters. Use unsafe entry points when thrown errors fit your existing conventions better.