Awesomate docs v0.27.0

Guides

Sign people in

Email sign-in links for your app's people, no passwords. How the link, the session and sign-out work, in a browser and in a mobile app.

People sign in to your app with a link emailed to them. There are no passwords to store or reset.

Set up the app

An app needs a name, the addresses it runs on, and a sign-up mode. With Claude Code:

Make an Awesomate app called "Brightwater portal" that runs on https://portal.brightwater.example and http://localhost:5173, invite only.

  • Addresses (origins) are exact: scheme, host and port, no path. Add http://localhost:<port> while you build and capacitor://localhost for a Capacitor app.
  • Sign-up is invite (only people you add can sign in) or open (anyone who uses the link joins with the app's default role). Open sign-up lets strangers in, so use it only when that's the point.

You get a publishable key (pk_...). Put it in your page's code; it is not a secret.

Add people with Claude Code (awesomate_crm_app_users) or on the app's page in the hub (Website, Apps, then the app, People). Each has a role, and is linked to the contact with the same email address.

import { createAppClient } from '@awesomate/sdk';

const app = createAppClient({ publishableKey: 'pk_...' });

const r = await app.auth.signInWithLink(email, { redirectTo: `${location.origin}/signed-in` });
showMessage(r.message); // "If that address can sign in to Brightwater portal, a link is on its way."

The answer is the same whether or not the address can sign in, so nobody can use your page to find out who your customers are. The link works for 15 minutes, once, and only lands on one of the app's own addresses.

When they click it

In a browser, the client takes the link up by itself: when the page loads with one, and when one arrives in a tab that is already open. It removes the token from the address bar at once and tells your onChange listener:

app.auth.onChange((user) => {
  if (user) render(user.email, user.role);
  else render(null);
});

app.auth.onSignInError((err) => showMessage(err.message)); // expired, or already used

onChange hears about changes to who is signed in: a sign-in, a sign-out, and the person's details changing (a new role, a newly linked contact), never a routine token refresh. It is not called for someone who was already signed in when the page loaded, so read that once at start-up:

const user = await app.auth.user(); // null when nobody is signed in
render(user);

Sessions

The client keeps the session in localStorage and refreshes it before it runs out (access tokens last 10 minutes; each refresh issues a new refresh token and uses up the old one). Two tabs share one session. If the person is switched off, or their access changes, their next call refreshes once and then signs them out, which onChange hears as null.

await app.auth.signOut();

Mobile apps (Capacitor)

Keep the session in Capacitor's Preferences, and finish the sign-in from the deep link the email opens:

const mobile = createAppClient({
  publishableKey: 'pk_...',
  handleSignInLinks: false,
  storage: {
    getItem: async (key) => (await Preferences.get({ key })).value,
    setItem: (key, value) => Preferences.set({ key, value }),
    removeItem: (key) => Preferences.remove({ key }),
  },
});

const user = await mobile.auth.completeSignIn(deepLinkUrl);

To check whether a link is a sign-in link before handling it, signInTokenFrom(url) returns the token the link carries, or null:

import { signInTokenFrom } from '@awesomate/sdk';

if (signInTokenFrom(deepLinkUrl)) {
  // a sign-in link: finish signing in with completeSignIn
}

Your own server

If your app has a server of its own, send it the person's access token and verify it there against the hub's public keys:

const token = await app.auth.accessToken(); // send as Authorization: Bearer <token>

On the server, verifyAppToken() checks it against the hub's public keys: signed by the hub (ES256), for your app, and not expired. It answers the token's claims, or rejects with code unauthenticated:

import { verifyAppToken } from '@awesomate/sdk';

const claims = await verifyAppToken(header.replace(/^Bearer /, ''), { appId: 'app_your_app_id' });
console.log(claims.sub, claims.role, claims.contact);

The keys are at https://hub.awesomate.ai/api/sdk/v1/jwks.json, and verifyAppToken() keeps them for five minutes. To check a token yourself, check the issuer is https://hub.awesomate.ai and the audience awesomate-app:<your app id>. The token's sub is the person's id and its app claim your app's id. Treat the role inside it as a hint only: the hub reads the person's current role from the database on every call.

Who is signed in, fresh

auth.user() is the person as this client last saw them, with no call to the hub. auth.me() reads them fresh (their current role and linked contact) along with the app's own settings:

const { user, app: settings } = await app.auth.me();
console.log(user.role, settings.name, settings.file_uploads);

A person the account has disabled is signed out, and me() rejects with unauthenticated.