Anythink SDK: sign-in and sessions

Use the Anythink SDK to sign users in, keep a session alive, and call the REST API from a React web app, with silent token refresh built in.

Last updated

The Anythink SDK (@anythink-cloud/sdk) handles sign-in and sessions for a React app that talks to your project's REST API. It signs users in with email and password, stores the session, refreshes the access token before it matters, and attaches the token to your API calls.

The SDK is auth-only. It does not read or write your entities, and it has no realtime, GraphQL or functions layer. For data you call the REST API with the token the SDK manages.

Piece What it does
AuthClient Signs in, registers, refreshes, signs out and changes passwords. Holds the session.
AuthProvider and useAuth A React provider and hook that expose the session and user, restore it on page load and redirect to your login page when there isn't one.
AuthenticatedBaseService An Axios wrapper that adds the bearer token to every request and retries once after a 401.
createAuthStore The session store the client uses. You rarely call it yourself.

How sessions work#

Signing in exchanges an email and password for a session: an access token, a refresh token and the access token's lifetime.

Token Lifetime Used for
Access token 30 minutes Sent as Authorization: Bearer … on every API call.
Refresh token 30 days Exchanged for a new pair when the access token expires.

Refresh tokens are single-use. Each refresh returns a new access token and a new refresh token and revokes the old refresh token. Signing out revokes the refresh token on the server too.

The SDK keeps the session in a store and persists it in the browser (see Choose where the session is stored). Its lifecycle is:

  1. signIn posts the credentials, stores the session, then loads the user from GET /org/{project_id}/users/me.
  2. AuthenticatedBaseService attaches the access token to each request.
  3. When a request returns 401, the service calls refreshSession, then replays the request once with the new token. Concurrent refreshes share one network call, so a burst of failing requests makes one refresh, not many.
  4. If the refresh fails, the session is cleared and the original 401 is returned to your code.
  5. On page load, AuthProvider restores the stored session. It refreshes an expired one, and sends the user to your login page if there is no valid session.

Set up your project#

Before the SDK can sign anyone in, your project needs to allow your app's address and, if users register themselves, allow registration. You also need two values: your project ID and the API URL.

In the Anythink dashboard

  1. Copy your project ID from the dashboard address. It's the number after /org/, for example 12345678 in /org/12345678/….
  2. Go to Settings › Organisation and add your app's address under Allowed Application URLs, for example https://app.example.com or *.example.com. Changes take up to two minutes to apply. Anythink domains and localhost with a port always work, so you can skip this step while developing locally.
  3. To let users create their own accounts, go to Settings › Users, tick Allow registrations and choose a Default role for new users. Leave Require email confirmation for new users ticked if you want users to confirm their address before they can sign in.
  4. Make sure the default role has the permissions your app needs. See Roles and permissions.

The API URL is https://api.my.anythink.cloud.

With the CLI

Note: These project settings are managed in the dashboard. Use the steps above.

With an AI assistant (MCP)

Note: These project settings are managed in the dashboard. Your assistant can walk you through the steps above.

Install and create the client#

bash
npm install @anythink-cloud/sdk

React 18 or 19 is a peer dependency. Create one AuthClient for the whole app and reuse it. Each client keeps its own session store, so two clients in one page don't share state.

typescript
import { AuthClient } from "@anythink-cloud/sdk";

let client: AuthClient | null = null;

export const getAuthClient = (): AuthClient => {
  if (!client) {
    client = new AuthClient({
      instanceUrl: "https://api.my.anythink.cloud",
      orgId: 12345678,
      storageType: "localStorage",
      cookieStorage: { name: "orders_app_session" },
    });
  }
  return client;
};

orgId is your project ID. cookieStorage.name is the key the session is saved under, whichever storage you pick. Give each app its own name so apps on the same domain don't overwrite each other.

Add the provider#

Wrap your app in AuthProvider.

tsx
import { AuthProvider } from "@anythink-cloud/sdk";
import { getAuthClient } from "./authClient";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <AuthProvider
      authClient={getAuthClient()}
      loginUrl="/auth/login"
      authPrefix="/auth"
      callbacks={{
        onSignIn: (session) => console.log("signed in", session.expires_at),
        onSignOut: () => console.log("signed out"),
        onTokenRefreshFailed: () => console.log("session expired"),
      }}
    >
      {children}
    </AuthProvider>
  );
}
Prop Required What it does
authClient Yes The AuthClient instance.
loginUrl Yes Where to send the browser when there's no valid session.
authPrefix Yes A path prefix for your sign-in pages, such as /auth. Pages whose path starts with it skip the session check and the redirect, so the login page doesn't redirect to itself. Set it to match the path of loginUrl.
callbacks No onSignIn, onSignOut and onTokenRefreshFailed. Each can be async.

onSignOut runs every time the provider signs the user out, including when it finds no session on load and redirects to loginUrl.

Sign users in#

useAuth returns the session, the user and the actions. Failed calls don't throw: signIn, register, refreshSession, signOut and changePassword resolve with an error field, so check it rather than wrapping the call in try/catch.

tsx
import { useState } from "react";
import { useAuth } from "@anythink-cloud/sdk";

export function LoginForm() {
  const auth = useAuth();
  const [error, setError] = useState<string | null>(null);

  async function onSubmit(email: string, password: string) {
    setError(null);
    const result = await auth.signIn(email, password);
    if (result.error || !result.data.session) {
      setError(result.error?.message ?? "Sign-in failed");
      return;
    }
    window.location.href = "/";
  }

  // render your form and call onSubmit
}

On failure, error.message is the message from the API. The ones you'll handle most often:

Message Meaning
Invalid email or password The credentials don't match a user in this project.
User email address is not confirmed The project requires email confirmation and the user hasn't confirmed yet.
User is not active An administrator deactivated the user.

Register a user#

typescript
const { error } = await auth.register("Ada", "Lovelace", "ada@example.com", "Sup3r-secret!");

register creates the user but doesn't sign them in. If the project requires email confirmation, Anythink emails a confirmation link, and the user can sign in once they've confirmed. Otherwise call signIn straight away.

Passwords need at least eight characters, with an upper-case letter, a lower-case letter, a digit and a special character. First and last names are limited to 50 characters. Registration fails with the API's message if the project doesn't allow registrations or has no default role set.

Sign in with a social provider#

Social sign-in doesn't go through signIn. Run the flow described in Social sign-in with Apple, Google, GitHub and LinkedIn, then hand the tokens it returns to the SDK:

typescript
await getAuthClient().setSession({
  access_token,
  refresh_token,
  expires_in,
});

setSession stores the session and loads the user, and from then on refresh and API calls work exactly as they do after signIn.

Sign out and change the password#

typescript
await auth.signOut();
await auth.changePassword("current-password", "N3w-secret-pass!");

signOut clears the local session first, so it succeeds even if the server call fails, and then revokes the refresh token on the server. changePassword calls /org/{project_id}/users/me/password with the current access token.

Read the session and the user#

tsx
const { session, user, isAuthenticated, isLoading, error } = useAuth();
Value Type Notes
session Session | null access_token, refresh_token, expires_in and expires_at (Unix seconds).
user User | null id, gid, first_name, last_name, email and created_at.
isAuthenticated boolean true when there's a session whose expires_at hasn't passed.
isLoading boolean true while a sign-in or refresh is in flight.
error Error | null The last auth error.

Outside React, the same data is on the client: getSession(), getUser(), getAccessToken(), getRefreshToken() and isAuthenticated(). fetchUserInfo() reloads the user from the API.

Call the REST API#

Extend AuthenticatedBaseService for each group of calls. It gives you an Axios client that sends the access token, refreshes on 401 and replays the request.

typescript
import { AuthenticatedBaseService } from "@anythink-cloud/sdk";
import { getAuthClient } from "./authClient";

const API_URL = "https://api.my.anythink.cloud";
const ORG_ID = 12345678;

export type Order = { id: number; status: string; total: number };

class OrderService extends AuthenticatedBaseService {
  constructor() {
    super(getAuthClient(), API_URL);
  }

  async list(): Promise<Order[]> {
    const res = await this.get<{ items: Order[] }>(
      `/org/${ORG_ID}/entities/orders/items`
    );
    return res.items;
  }
}

export const orders = new OrderService();

The helpers get, post, put, patch, delete and postFormData return the response body. getClient() returns the underlying Axios instance if you need it. The calls are subject to the signed-in user's role permissions and row-level security, the same as any other API request.

Choose where the session is stored#

storageType decides where the session persists.

storageType Where Behaviour
"cookie" (default) A browser cookie Shared by every tab on the site. The cookie expires when the current access token does, 30 minutes after the last sign-in or refresh. A user who returns after that signs in again.
"localStorage" window.localStorage Shared by every tab and kept across browser restarts. On return, the provider restores the session and refreshes it with the 30-day refresh token.
"sessionStorage" window.sessionStorage One session per tab, cleared when the tab closes. Use it when each tab may hold a different user, or when the browser blocks third-party cookies, such as in an embedded app.

With "cookie" you can also set cookieStorage.domain, path (default /), secure (default on over HTTPS) and sameSite (default lax).

Only the session and the user are persisted. Loading and error state live in memory.

Note: Storage is read in the browser. On the server there is no session, so check it in client components or after hydration.

Point the client at other endpoints#

By default the client calls these paths, with your project ID in place of {project_id}:

Action Request
Sign in POST /org/{project_id}/auth/v1/token with email and password
Refresh POST /org/{project_id}/auth/v1/refresh with token
Sign out POST /org/{project_id}/auth/v1/logout with token
Register POST /org/{project_id}/auth/v1/register
Load the user GET /org/{project_id}/users/me
Change password PUT or POST /org/{project_id}/users/me/password

tokenEndpoint, refreshEndpoint, logoutEndpoint, registerEndpoint and changePasswordEndpoint in the config replace the sign-in, refresh, sign-out, register and change-password paths. Use them if you proxy sign-in through your own server. The responses must keep the same shape: access_token, refresh_token and expires_in.

Without the SDK#

The SDK is a thin layer over these endpoints, so any client can use them directly. Sign in with POST /org/{project_id}/auth/v1/token, refresh with POST /org/{project_id}/auth/v1/refresh, and send the access token as Authorization: Bearer …. For server-to-server calls use an API key instead of a user session.

Limits and trade-offs#

Limit Detail
Auth only The SDK signs users in and manages sessions. Data calls are plain REST requests.
React The package needs React 18 or 19 as a peer dependency.
Email and password built in Social sign-in hands its tokens to setSession. The SDK has no helper for starting a social flow.
One user per client Each client holds one session. To hold several users in one page, create one client per user with its own cookieStorage.name.
Token lifetimes are fixed Access tokens last 30 minutes and refresh tokens 30 days. They aren't configurable per project.
Errors are returned Auth methods return an error field instead of throwing. Requests made through AuthenticatedBaseService throw Axios errors as usual.

Next steps#