NubleStation

Identity gives every user in your org one account that works on every app. They sign in once at identity.{org}.local; your app just asks “who is this, and what’s their role here?”. This page gets you from zero to a gated app — for the why behind it, see Sessions & SSO.

What works today

CapabilityStatus
Server-side sessions (revocable, 8h TTL)✅ Live
SSO across all *.{org}.local apps✅ Live
Per-app roles, default-deny✅ Live
User self-registration at identity.{org}.local✅ Live — but new users have zero app access until an admin grants a role in the Console
Mobile Safari support (Bearer fallback)✅ Live — automatic in SDK ≥ 0.1.1
OIDC / external identity providers⏳ Not yet — shared-cookie SSO only

Get up and running

1. Install the SDK

npm install @nublestation/identity
# or
pnpm add @nublestation/identity

2. Create the client

Create it once at module level and reuse it everywhere:

import { createIdentityClient } from "@nublestation/identity";

export const auth = createIdentityClient({
  url:         import.meta.env.VITE_NUBLESTATION_URL,          // Gateway — http://api.{org}.local
  identityUrl: import.meta.env.VITE_NUBLESTATION_IDENTITY_URL, // Identity pages — http://identity.{org}.local
  app:         import.meta.env.VITE_NUBLESTATION_APP,          // your app's slug from the Console
});

Set the three env vars in your .env (Vite bakes them in at build time):

VariableExample
VITE_NUBLESTATION_URLhttp://api.clinic.local
VITE_NUBLESTATION_IDENTITY_URLhttp://identity.clinic.local
VITE_NUBLESTATION_APPtasks

Two URLs because programmatic calls (/v1/auth/*) go through the Gateway, while sign-in is a full-page navigation to the Identity pages.

3. Check the session

getSession() returns one of three states — handle all of them:

const session = await auth.getSession();

switch (session.status) {
  case "authenticated":
    // session.user.role is set — render the app
    console.log(`Hi ${session.user.email}, role: ${session.user.role}`);
    break;
  case "forbidden":
    // Signed in, but no role on THIS app — show "ask an admin", don't loop to login
    break;
  case "unauthenticated":
    auth.login(); // redirect to the SSO sign-in, returns to the current URL after
    break;
}

4. Wire sign-in and sign-out

auth.login();        // navigate to identity.{org}.local/authorize for this app
await auth.logout(); // revoke the session server-side, then go to the Identity login page

That’s it — your app is gated. Using React? Copy the canonical useIdentity hook and AuthGate component from the SDK reference.

Everyday usage

Guard a route imperatively

requireUser() resolves with the user when signed in and allowed on this app; otherwise it redirects to sign-in for you:

const user = await auth.requireUser();
// code below only runs for an authorized user

// Custom handling for signed-in-but-no-access:
await auth.requireUser({ onForbidden: (u) => showNoAccessScreen(u) });

Check a role

await auth.hasAccess();         // any role on this app
await auth.hasAccess("admin");  // require this exact role

Build a “share with…” picker

listAppUsers() returns everyone with access to this app, minus the caller — exactly the set you can share with:

const users = await auth.listAppUsers();
// [{ id, email, displayName, avatarUrl, role }, ...]

Handle the forbidden state

A user can be signed in yet denied: access is default-deny per app. Show them “ask an admin to grant you access” — an admin then grants a role in the Console under App → Users. Don’t redirect them to login; they’re already logged in.

Reference

SDK methods

MethodReturnsUse for
getUser()IdentityUser | null”Who is signed in” UI, ignoring app access
getSession(){ status, user? }authenticated / forbidden / unauthenticatedGating your app (primary call)
isAuthenticated()booleanQuick “any session?” check
hasAccess(role?)booleanSigned in + has access (optionally an exact role)
requireUser(opts?)IdentityUser (or redirects)Imperative route guard
listAppUsers()AppUser[]Share pickers, member lists
loginUrl(redirectUri?)stringSign-in URL for an href
login(redirectUri?)void (navigates)Sign-in button
logout(redirectTo?)Promise<void> (navigates)Sign-out button

HTTP endpoints

All JSON calls go through the Gateway at api.{org}.local. The session rides the cookie (or the Bearer fallback) — no API key needed on /v1/auth/*.

MethodPathStatusesDescription
GET/v1/auth/me200, 401Current user, no app context (role is null)
GET/v1/auth/me?app={slug}200, 401, 403, 404Current user + role on that app; 403 forbidden if no role, 404 unknown_app if the slug doesn’t exist
GET/v1/auth/app-users?app={slug}200, 401, 403, 404Users with access to the app, minus the caller
POST/v1/auth/logout200Revoke the session, clear the cookie

User-facing pages (/login, /register, /authorize, /account) are served directly at identity.{org}.local — your app never renders them.

Limits & gotchas

  • No localhost auth. The session cookie is scoped to .{org}.local and is never sent from Vite’s dev server. Develop UI with mocked state; test the real flow on your deployed app at {app}.{org}.local.
  • Mobile Safari drops the cross-subdomain cookie (.local isn’t a registrable TLD for it). SDK ≥ 0.1.1 handles this automatically via a Bearer-token fallback — nothing to do on your side. Details in Sessions & SSO.
  • Granting access is manual. Self-registration works, but a new user sees forbidden on every app until an admin grants a role in the Console. There is no self-serve access request yet.
  • 404 unknown_app from any call means your app slug doesn’t match the Console — check VITE_NUBLESTATION_APP.

Go deeper