Features

Authentication

Sign-in lives in the shell. Connect the identity-service or Supabase, protect routes, and decide who can join - public, a domain allow list, or invitation only.

Sign-in lives in the shell

Your microfrontend does not ship a login page. The shell owns /login and /login/callback, stores the session, refreshes tokens, and shares the signed-in user with iframes through the SDK.

The default backend is the identity-service — OAuth with GitHub, Google, or Microsoft, JWT sessions, and JWKS so other services can verify tokens. If Supabase Auth is already in your stack, point backend.type at it instead.

1. Connect a backend

Set backend in shellui.config.ts and enable the login methods you want on the page.

2. Protect routes

Mark navigation with requiresAuth. Signed-out visitors go to login, then back to the page they asked for.

3. Login from the iframe

OAuth must run in the top window. From an embedded app, call shellui.login() so the shell can start the redirect.

shellui.config.ts
import type { ShellUIConfig } from "@shellui/core";

const config: ShellUIConfig = {
  backend: {
    type: "shellui",
    url: "http://localhost:8000",
    companyId: 1,
    login: {
      methods: ["oauth"],
      oauthProviders: ["github", "google"],
    },
  },
  navigation: [
    {
      label: "Billing",
      path: "billing",
      url: "https://app.example.com/billing",
      requiresAuth: true,
    },
  ],
};

export default config;

Who can join

After a successful OAuth login, the company decides whether that person actually gets tokens. Access is per company: the same account can be enabled in one tenant and waiting in another. Configure the mode in admin, or patch it on the identity API.

Public

Anyone who authenticates joins with access enabled and receives tokens. The default when you are still opening the doors.

Domain allow list

Emails on allowed_email_domains join enabled. Other domains create a disabled membership and owners get notified.

Invitation only

New users land as disabled members. Tokens stay blocked until an owner or staff enables them for that company.

access.ts
await fetch(`${backendUrl}/api/v1/companies/${companyId}/`, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    access_mode: "public",
  }),
});
access.ts
await fetch(`${backendUrl}/api/v1/companies/${companyId}/`, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    access_mode: "domain",
    allowed_email_domains: ["acme.com"],
  }),
});
access.ts
await fetch(`${backendUrl}/api/v1/companies/${companyId}/`, {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    access_mode: "invite",
  }),
});

Groups, logs, and tokens

The identity-service keeps a company directory next to the session. Staff and company owners manage groups, read login events, and mint access tokens for people and for services that call your APIs without a browser.

Group names travel on the JWT as user.groups. Login events record success or failure, provider, and privacy-oriented client fields. Personal access tokens are named JWTs you can revoke; mark them read-only when a service only needs to fetch data.

groups.ts
import { useAuth } from "@shellui/core";

const { user } = useAuth();
const groups = user?.groups ?? [];

await fetch(`${backendUrl}/api/v1/groups`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ name: "billing" }),
});
login-events.ts
const events = await fetch(
  `${backendUrl}/api/v1/login-events?outcome=success`,
  {
    headers: {
      Authorization: `Bearer ${accessToken}`,
    },
  },
).then((response) => response.json());
access-token.ts
const { access_token } = await fetch(
  `${backendUrl}/api/v1/personal-access-tokens`,
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${sessionToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      name: "billing-service",
      read_only: true,
    }),
  },
).then((response) => response.json());

Next, see how Administration manages users, company access, and operational data.