Insolitum Developers
Guides

Authentication

How authentication works between Universe Shell and your module. postMessage protocol, ShellAuthProvider, and session management.

Authentication

Your module runs inside an iframe embedded in Universe Shell. Authentication happens automatically through a postMessage protocol — no OAuth setup, no API keys to manage.

How It Works

┌─────────────────────────────────────────────┐
│ Universe Shell (parent window)               │
│                                               │
│  User is logged in via Supabase Auth          │
│  Has access_token + refresh_token             │
│                                               │
│  ┌─────────────────────────────────────────┐ │
│  │ Your Module (iframe)                     │ │
│  │                                           │ │
│  │  1. Mount → REQUEST_AUTH_SESSION ───────→ │ │
│  │                                           │ │
│  │  2. ←──── AUTH_SESSION {tokens}           │ │
│  │                                           │ │
│  │  3. supabase.auth.setSession(tokens)      │ │
│  │                                           │ │
│  │  4. ✓ user + organizationId available     │ │
│  └─────────────────────────────────────────┘ │
└─────────────────────────────────────────────┘

ShellAuthProvider

The ShellAuthProvider component handles the entire authentication flow. It's included in the module-starter template at src/lib/shell-auth.tsx.

src/lib/shell-auth.tsx
'use client';
 
import { createContext, useContext, useEffect, useState } from 'react';
import { createClient } from '@supabase/supabase-js';
 
const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL!,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
);
 
export function ShellAuthProvider({ children }) {
  const [user, setUser] = useState(null);
  const [isLoading, setIsLoading] = useState(true);
  const [organizationId, setOrganizationId] = useState(null);
 
  useEffect(() => {
    // Read org_id from URL (set by Shell iframe)
    const orgId = new URLSearchParams(window.location.search).get('org_id');
    if (orgId) setOrganizationId(orgId);
 
    // Listen for auth from Shell
    const handleMessage = async (event) => {
      if (event.data?.type === 'AUTH_SESSION') {
        const { access_token, refresh_token } = event.data.payload;
        await supabase.auth.setSession({ access_token, refresh_token });
        // ... set user state
      }
    };
 
    window.addEventListener('message', handleMessage);
 
    // Request session from Shell
    if (window.parent !== window) {
      window.parent.postMessage({ type: 'REQUEST_AUTH_SESSION' }, '*');
    }
 
    // Timeout fallback (5s)
    const timeout = setTimeout(() => setIsLoading(false), 5000);
 
    return () => {
      window.removeEventListener('message', handleMessage);
      clearTimeout(timeout);
    };
  }, []);
 
  // ... render provider
}

useShellAuth Hook

Access authentication state anywhere in your module:

import { useShellAuth } from '@/lib/shell-auth';
 
export default function MyPage() {
  const {
    user,            // Supabase User object (email, id, metadata)
    supabase,        // Authenticated Supabase client
    organizationId,  // Current tenant UUID
    isAuthenticated, // boolean
    isLoading,       // true during auth handshake
  } = useShellAuth();
 
  if (isLoading) return <LoadingSpinner />;
  if (!isAuthenticated) return <p>Not authenticated</p>;
 
  return <p>Hello, {user.email}!</p>;
}

Context Type

interface ShellAuthContextType {
  user: User | null;
  supabase: SupabaseClient;
  organizationId: string | null;
  isAuthenticated: boolean;
  isLoading: boolean;
}

Organization ID

The organizationId identifies which tenant (company) the user belongs to. It's passed via the iframe URL parameter ?org_id=<uuid>.

Always use organizationId when querying data. This ensures tenant isolation — users can only see data belonging to their organization.

Security Considerations

Origin Validation

The current implementation uses '*' as the target origin in postMessage. For production modules, validate the origin:

const handleMessage = async (event: MessageEvent) => {
  // Only accept messages from trusted Shell domains
  const trustedOrigins = [
    'https://universe.insolitum.ai',
    'https://app.insolitum.ai',
    'http://localhost:3000', // dev
  ];
 
  if (!trustedOrigins.includes(event.origin)) return;
 
  if (event.data?.type === 'AUTH_SESSION') {
    // ... handle auth
  }
};

Token Refresh

Supabase tokens have a limited lifetime. The ShellAuthProvider handles initial session setup, but for long-running sessions, the Supabase client auto-refreshes tokens in the background.

Never Expose Tokens

  • Don't store tokens in localStorage (the Shell manages the source of truth)
  • Don't log tokens to the console in production
  • Don't send tokens to third-party services

On this page