Skip to content

@msflib/core

Framework-agnostic core package for msflib. Provides global application configuration, a safe browser storage wrapper, and a multi-tenant runtime used by all other msflib modules.


Table of Contents


Installation

pnpm add @msflib/core

Quick Start

Call configureApplication() once before mounting any msflib providers — typically in your app root or an initialisation file.

import { configureApplication } from '@msflib/core';

configureApplication({
  baseURL: 'https://api.example.com/api/v1',
  accessTokenKey: 'access_token',
});

Next.js (App Router)

// src/application.client.ts
'use client';
import { configureApplication } from '@msflib/core';

configureApplication({
  baseURL: process.env.NEXT_PUBLIC_API_URL!,
  accessTokenKey: 'access_token',
});
// src/app/providers.tsx
'use client';
import '@/application.client'; // must be imported before any msflib provider
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { AuthProvider } from '@msflib/react-auth';
import { NotificationProvider } from '@msflib/react-notification';

const qc = new QueryClient();

export default function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={qc}>
      <AuthProvider>
        <NotificationProvider>{children}</NotificationProvider>
      </AuthProvider>
    </QueryClientProvider>
  );
}

Important: configureApplication() is a one-time call. Subsequent calls are silently ignored to prevent accidental reconfiguration.


API Reference

configureApplication

Initialises global application configuration. Must be called once before any module is used.

configureApplication(config: {
  baseURL: string;
  accessTokenKey: string;
  workspace?: string | null;
  endpoints?: Record<string, Record<string, string>>;
}): void
Field Type Required Description
baseURL string ✅ Base URL for all API requests
accessTokenKey string ✅ localStorage key used to read the access token
workspace string \| null — Initial workspace slug for multi-tenant apps
endpoints object — Per-module endpoint path overrides
configureApplication({
  baseURL: 'https://api.example.com/api/v1',
  accessTokenKey: 'access_token',
  workspace: 'acme-corp', // optional: initial workspace
  endpoints: {
    notification: {
      notifications: '/alerts', // override default path
    },
  },
});

getApplicationConfig

Returns the full configuration object. Throws if called before configureApplication().

getApplicationConfig(): ApplicationConfig
import { getApplicationConfig } from '@msflib/core';

const cfg = getApplicationConfig();
console.log(cfg.baseURL); // 'https://api.example.com/api/v1'

getApplicationEndpoints

Returns the endpoint overrides for a specific module, or undefined if none were configured.

getApplicationEndpoints(moduleName: string): Record<string, string> | undefined
const endpoints = getApplicationEndpoints('notification');
// { notifications: '/alerts' } or undefined

storage

A safe localStorage wrapper that is a no-op on the server (SSR-safe). Use this wherever you need to read or write tokens and preferences.

import { storage } from '@msflib/core';

storage.setItem('access_token', 'eyJ...');
const token = storage.getItem('access_token'); // string | null
storage.removeItem('access_token');
Method Signature Description
getItem (key: string) => string \| null Reads a value from localStorage
setItem (key: string, value: string) => void Writes a value to localStorage
removeItem (key: string) => void Removes a key from localStorage

On the server or in environments without window, all methods are safe no-ops — getItem returns null.


Workspace Runtime

The workspace runtime is a lightweight pub/sub state manager for the active workspace. It is fully framework-agnostic — no React required. React modules integrate via the useActiveWorkspace hook from @msflib/react-shared.


setActiveWorkspace

Updates the current active workspace. Notifies all subscribers. Slug is trimmed and normalised — empty or whitespace-only strings are treated as null.

setActiveWorkspace(workspace?: string | null): void
import { setActiveWorkspace } from '@msflib/core';

setActiveWorkspace('acme-corp'); // switch to workspace
setActiveWorkspace(null); // clear workspace (single-workspace mode)
setActiveWorkspace(''); // treated as null

Calling setActiveWorkspace with the same value that is already set is a no-op — no listeners are notified.


getActiveWorkspace

Returns the current active workspace slug, or null if no workspace is set.

getActiveWorkspace(): string | null
const workspace = getActiveWorkspace(); // 'acme-corp' | null

subscribeActiveWorkspace

Registers a listener that is called whenever the active workspace changes. Returns an unsubscribe function.

subscribeActiveWorkspace(listener: () => void): () => void
const unsubscribe = subscribeActiveWorkspace(() => {
  console.log('Workspace changed to:', getActiveWorkspace());
});

// Later, clean up:
unsubscribe();

This is the subscription API used internally by useActiveWorkspace in @msflib/react-shared to drive React re-renders on workspace change.


resolveWorkspaceScopedEndpoint

Registers an endpoint as workspace-scoped (or explicitly unscoped) and returns its normalized path. Each module's API layer calls this once per endpoint, typically via configuredApiClient(), rather than being called directly by consumers.

resolveWorkspaceScopedEndpoint(
  endpoint: string,
  options?: { workspaceScoped?: boolean; matchPath?: string },
): string
Argument Description
endpoint The API endpoint path.
options.workspaceScoped false registers the endpoint as explicitly unscoped (e.g. auth). Defaults to scoped.
options.matchPath Override which path gets registered, when it differs from endpoint (e.g. a narrower opt-out under a broader scoped prefix).

isWorkspaceScopedEndpoint

Checks whether a given path is currently registered as workspace-scoped, respecting any explicit unscoped overrides.

isWorkspaceScopedEndpoint(endpoint: string): boolean
import { addWorkspaceScopedEndpoint, addWorkspaceUnscopedEndpoint, isWorkspaceScopedEndpoint } from '@msflib/core';

addWorkspaceScopedEndpoint('/notifications');
isWorkspaceScopedEndpoint('/notifications'); // → true

addWorkspaceUnscopedEndpoint('/notifications/public');
isWorkspaceScopedEndpoint('/notifications/public'); // → false (explicit opt-out)

The registry only tracks which endpoints are scoped — it does not rewrite URLs itself. The actual workspace prefix or header is applied by the apiClientDecorator you pass to configureApplication (e.g. workspaceHookDecorator from @msflib/react-workspace), which consults this registry at request time.


Multi-Tenant Guide

This package has built-in support for multi-tenant SaaS apps where different workspaces are served from different URL namespaces (e.g. /acme/notifications vs /globex/notifications).

How it works

  1. Each API module registers its endpoints via resolveWorkspaceScopedEndpoint() (through configuredApiClient()); the configured apiClientDecorator applies the workspace prefix/header at request time, so switching is instant.
  2. The active workspace is stored as module-level state with a pub/sub listener set.
  3. React modules subscribe to workspace changes via useActiveWorkspace() from @msflib/react-shared.
  4. TanStack Query caches are scoped per workspace using workspace-first query keys.

Static workspace (known at login time)

configureApplication({
  baseURL: 'https://api.example.com/api/v1',
  accessTokenKey: 'access_token',
  workspace: 'acme-corp',
});

Dynamic workspace switching (org switcher, workspace switcher)

// Somewhere in your app (sidebar, dropdown, etc.)
import { setActiveWorkspace } from '@msflib/core';

function OrgSwitcher({ orgs }) {
  return (
    <select onChange={(e) => setActiveWorkspace(e.target.value)}>
      {orgs.map((org) => (
        <option key={org.slug} value={org.slug}>{org.name}</option>
      ))}
    </select>
  );
}

This single call cascades through modules that subscribe to it via useActiveWorkspace():

  • All API requests immediately use the new workspace path
  • Query keys that include the workspace change, triggering a fresh fetch
  • No page reload required

Not every provider subscribes, though — @msflib/react-auth's AuthProvider uses a fixed query key and does not call useActiveWorkspace(), so it does not auto-refetch on a workspace switch. See React Auth's Multi-Tenancy Support for the current contract and how to invalidate it explicitly.


Single-Tenant Mode

Just omit workspace from configuration. Everything works as normal.

configureApplication({
  baseURL: 'https://api.example.com/api/v1',
  accessTokenKey: 'access_token',
  // No workspace field
});

Usage in Module Packages

All msflib modules use this package internally. You typically do not call these APIs directly from a module — they are consumed automatically.

// Inside a module API file
import { configuredApiClient } from '@msflib/core';

export const createNotificationApi = (isWorkspaceScoped = true) => {
  const { apiClient, loginClient } = configuredApiClient({ isWorkspaceScoped });

  return {
    list: () => apiClient<Notification[]>('GET', '/notifications'),

    // Auth endpoints opt out of workspace scoping per-call
    login: (payload: LoginPayload) =>
      loginClient('/login', payload, { isWorkspaceScoped: false }),
  };
};

Exports

import {
  configureApplication,
  getApplicationConfig,
  getApplicationEndpoints,
  resetApplicationConfig,
  storage,
  createStorage,
  configuredApiClient,
  setActiveWorkspace,
  getActiveWorkspace,
  subscribeActiveWorkspace,
  resolveWorkspaceScopedEndpoint,
  isWorkspaceScopedEndpoint,
} from '@msflib/core';

Available APIs

  • configureApplication(config) Set the global Application configuration. Must be called once at app startup before using any Application.

  • getApplicationConfig() Retrieve the resolved global configuration anywhere inside Application or consumer apps.

  • storage Default safe browser storage wrapper (uses localStorage with SSR guards).

  • createStorage(options?) Factory to create a custom storage adapter (e.g. sessionStorage, memory storage, or custom persistence layer).

  • configuredApiClient(options?) Factory used internally by module API layers to build a workspace-aware HTTP client.