Skip to content

Your own backend (HTTP) ​

@priemskiyyy/anchor-http makes your own backend the session provider, through your own client. You give the adapter the functions that read the session, mint each token and end the session, and your client keeps everything it already owns: the address, cookies, authentication, retries and response validation. The adapter maps their answers onto the session contract, binds every token to the session that asked for it, and observes nothing.

Install ​

sh
pnpm add @priemskiyyy/anchor @priemskiyyy/anchor-http

Create an Anchor ​

ts
import { Anchor } from "@priemskiyyy/anchor";
import { http } from "@priemskiyyy/anchor-http";

export const anchor = new Anchor({
  adapter: http({
    authority: "https://api.example.com",
    read: async ({ signal }) => {
      const response = await fetch("/api/session", {
        credentials: "include",
        signal,
      });

      if (response.status === 401) {
        return null;
      }

      if (!response.ok) {
        throw new Error(`The session endpoint answered ${response.status}.`);
      }

      const { user, session } = SessionSchema.parse(await response.json());

      return { subject: user.id, user, context: session.id };
    },
    end: async ({ session, signal }) => {
      const response = await fetch("/api/logout", {
        method: "POST",
        credentials: "include",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ session: session.context }),
        signal,
      });

      if (!response.ok) {
        throw new Error(`The logout endpoint answered ${response.status}.`);
      }
    },
    tokens: {
      realtime: async ({ signal }) => {
        const { token, userId } = TokenSchema.parse(
          await api.get("/api/realtime-token", { signal }),
        );

        return { value: token, subject: userId };
      },
    },
  }),
});

await anchor.start();

After a sign-in or sign-out flow of your own, call anchor.sync().

Options ​

OptionTypeDefaultWhat it does
authoritystringrequiredNames your backend, the authority of every principal, such as its origin.
read({ signal }) => HttpSessionAnswer | nullrequiredReads the current session, as described below.
end({ session, signal }) => voidnoneEnds exactly the session named, on your server.
tokensRecord<string, (request) => HttpToken>noneThe token profiles, each the function that mints it.

Each function may return its answer or a promise of it. Pass signal on: it aborts at Anchor's timeout and on disposal.

Capabilities ​

Capabilityhttp
ObservationNone: call anchor.sync() after a sign-in or sign-out flow.
Token profilesAny: each is a function of yours, bound to the session by its subject.
Forced refreshPassed on as forceRefresh.
EndLocal and remote, once your end resolves.
Navigation after endNever.
RuntimesAnywhere your client runs: browsers, React Native, Node and edge runtimes.

How a session is mapped ​

read answers null only on your server's positive word that there is no session, such as a 401. When it cannot tell, a network failure or a 5xx, it throws: the first state is then unavailable and a later read keeps the session, never signing anyone out on a guess. A session is:

FieldTypeDefaultMeaning
subjectstringrequiredYour server's stable id of the user.
userTUserrequiredPlain data the session carries; its type is the session's user type.
tenantstring | nullnullYour server's tenant partition, when it has them.
contextstring | nullnullA deterministic fingerprint of what else changes permissions: the server's session id, the active organization.
guestbooleanfalseAn anonymous session.
impersonatedbooleanfalseA session someone else acts in.
action{ kind } | nullnullA step the user must finish, such as accept-terms: the state is action-required.

The principal's authority is the one you named, its subject and tenant are yours, and a change of subject, tenant, context, guest or impersonated is a new session with a new key. Validate the response before you answer: Anchor refuses an answer outside the contract, such as one without a string subject, with INVALID_ANSWER, but it cannot tell a well-formed wrong answer from a right one.

Tokens ​

session.getToken(profile) calls that profile's function with { profile, forceRefresh, session, signal }, where session is the identity of the session that asked. It answers { value, subject, tenant?, context?, kind?, scheme? }, kind defaulting to access-token and scheme to null. subject is who your server minted the token for: a cookie authenticates whoever is signed in now, not the session that asked, so a token minted for another subject rejects with SESSION_CHANGED and never reaches the old session's work. Answer tenant and context too, as read answers them, when your server knows what it minted for: a token minted after an organization switch or a new login in another tab then rejects with SESSION_CHANGED as well, where the subject alone cannot tell. A function that throws rejects with TOKEN_FAILED and its error as the cause. forceRefresh is yours to honor: the adapter invents no refresh semantics.

Ending the session ​

session.end() calls end({ session, signal }) with the identity of the session to end, so your server can end exactly that one, and answers { local: "completed", remote: "completed", navigation: null } once it resolves. An end that throws rejects with END_FAILED, and the session stays signed in until a read says otherwise. Without end, session.end() rejects with UNSUPPORTED.

Behavior ​

  • The factory is cold: creating the adapter calls none of your functions.
  • native is null: there is no SDK behind the adapter.
  • The adapter holds no state and installs no listener; dispose() has nothing to remove.
  • A token is minted once per call: there is no cache, retry or queue in the adapter. Anchor shares one request between concurrent callers of the same profile and the same forceRefresh.
  • A cookie session behind this adapter is shared by the browser's tabs, which do not tell each other: the crossTab() hint makes a sign-out in one read again in the others.

What Anchor does not normalize ​

Everything your client and your server own stays with them: the transport and its address, cookies and how requests authenticate, retries, response validation, refresh semantics, and every sign-in and sign-out flow. The adapter only maps the answers of your three kinds of function onto the contract.

The adapter runs against an in-process fake backend client, and no request leaves this repository; see the verification matrix. A server that reads its own sessions per request writes a server adapter of its own. The package README is the reference.

Released under the MIT License.