> ## Documentation Index
> Fetch the complete documentation index at: https://docs.livepeer.org/llms.txt
> Use this file to discover all available pages before exploring further.

# NaaP architecture

> NaaP micro-frontend architecture: the Next.js 15 shell, ShellContext interface, UMD plugin loading, and the request flow from browser to database.

export const CenteredContainer = ({children, maxWidth = "800px", padding = "0", preset = "default", width = "", minWidth = "", marginRight = "", marginBottom = "", textAlign = "", style = {}, className = "", ...rest}) => {
  const presets = {
    default: {},
    fitContent: {
      width: "fit-content",
      minWidth: "fit-content"
    },
    readable70: {
      width: "70%",
      minWidth: "fit-content"
    },
    readable80: {
      width: "80%",
      minWidth: "fit-content"
    },
    readable90: {
      width: "90%"
    },
    wide900: {
      maxWidth: "900px"
    }
  };
  const presetStyle = presets[preset] || presets.default;
  return <div className={className} style={{
    maxWidth: presetStyle.maxWidth || maxWidth,
    margin: "0 auto",
    padding: padding,
    ...presetStyle.width ? {
      width: presetStyle.width
    } : {},
    ...presetStyle.minWidth ? {
      minWidth: presetStyle.minWidth
    } : {},
    ...width ? {
      width
    } : {},
    ...minWidth ? {
      minWidth
    } : {},
    ...marginRight ? {
      marginRight
    } : {},
    ...marginBottom ? {
      marginBottom
    } : {},
    ...textAlign ? {
      textAlign
    } : {},
    ...style
  }} {...rest}>
      {children}
    </div>;
};

export const ScrollableDiagram = ({children, title = '', maxHeight = '500px', minWidth = '100%', showControls = false, className = '', style = {}, ...rest}) => {
  const buildDiagramKey = (currentTitle = '', currentClassName = '') => {
    const source = `${currentTitle}|${currentClassName}|scrollable-diagram`;
    let hash = 0;
    for (let index = 0; index < source.length; index += 1) {
      hash = hash * 31 + source.charCodeAt(index) >>> 0;
    }
    return `docs-diagram-${hash.toString(36)}`;
  };
  const diagramKey = buildDiagramKey(title, className);
  const zoomName = `${diagramKey}-zoom`;
  const zoomLevels = [{
    label: '75%',
    value: 0.75
  }, {
    label: '100%',
    value: 1
  }, {
    label: '125%',
    value: 1.25
  }, {
    label: '150%',
    value: 1.5
  }];
  const containerStyle = {
    overflow: 'auto',
    maxHeight,
    border: '1px solid var(--lp-color-border-default)',
    borderRadius: "8px",
    padding: "var(--lp-spacing-4)",
    background: 'var(--lp-color-bg-card)',
    position: 'relative'
  };
  return <div className={className} style={{
    position: 'relative',
    marginBottom: "var(--lp-spacing-4)",
    ...style
  }} {...rest}>
      {title && <p style={{
    textAlign: 'center',
    fontStyle: 'italic',
    color: 'var(--lp-color-text-secondary)',
    marginBottom: "var(--lp-spacing-2)",
    fontSize: '0.875rem'
  }}>
          {title}
        </p>}

      {showControls ? <style>{`
          [data-docs-diagram-key="${diagramKey}"] [data-docs-diagram-content] {
            transform: scale(1);
            transform-origin: top left;
            width: max-content;
          }
          ${zoomLevels.map(zoomLevel => `
          #${diagramKey}-${zoomLevel.label.replace('%', '')}:checked ~ [data-docs-diagram-shell] [data-docs-diagram-content] {
            transform: scale(${zoomLevel.value});
          }
          #${diagramKey}-${zoomLevel.label.replace('%', '')}:checked ~ [data-docs-diagram-controls] label[for="${diagramKey}-${zoomLevel.label.replace('%', '')}"] {
            background: var(--lp-color-accent);
            color: var(--lp-color-on-accent);
            border-color: var(--lp-color-accent);
          }`).join('\n')}
        `}</style> : null}

      {showControls ? zoomLevels.map(zoomLevel => {
    const inputId = `${diagramKey}-${zoomLevel.label.replace('%', '')}`;
    return <input key={inputId} id={inputId} type="radio" name={zoomName} defaultChecked={zoomLevel.value === 1} style={{
      position: 'absolute',
      opacity: 0,
      pointerEvents: 'none'
    }} />;
  }) : null}

      <div data-docs-diagram-key={diagramKey} data-docs-diagram-shell style={containerStyle}>
        <div data-docs-diagram-content style={{
    minWidth,
    transformOrigin: 'top left',
    width: 'max-content'
  }}>
          {children}
        </div>
      </div>

      {showControls ? <div data-docs-diagram-controls style={{
    display: 'flex',
    justifyContent: 'flex-end',
    alignItems: 'center',
    gap: "var(--lp-spacing-2)",
    marginTop: "var(--lp-spacing-2)",
    flexWrap: 'wrap'
  }}>
          <span style={{
    fontSize: "0.75rem",
    color: 'var(--lp-color-text-muted)',
    marginRight: 'auto'
  }}>
            Scroll to pan
          </span>
          {zoomLevels.map(zoomLevel => {
    const inputId = `${diagramKey}-${zoomLevel.label.replace('%', '')}`;
    return <label key={inputId} htmlFor={inputId} style={{
      background: 'transparent',
      color: 'var(--lp-color-text-secondary)',
      border: '1px solid var(--lp-color-border-default)',
      borderRadius: "4px",
      padding: '4px 10px',
      cursor: 'pointer',
      fontSize: "0.75rem",
      fontWeight: '600'
    }}>
                {zoomLevel.label}
              </label>;
  })}
        </div> : null}
    </div>;
};

export const CustomDivider = ({color = "var(--lp-color-border-default)", middleText = "", spacing = "default", style = {}, className = "", ...rest}) => {
  const spacingPresets = {
    default: {
      margin: "24px 0"
    },
    overlap: {
      margin: "-1rem 0 -1rem 0"
    },
    tight: {
      margin: "0 0 -1rem 0"
    },
    section: {
      margin: "0 0 -2rem 0"
    },
    sectionOverlap: {
      margin: "-1rem 0 -2rem 0"
    },
    deepOverlap: {
      margin: "-1rem 0 -1.5rem 0"
    }
  };
  const spacingStyle = spacingPresets[spacing] || spacingPresets.default;
  return <div role="separator" aria-orientation="horizontal" className={className} style={{
    display: "flex",
    alignItems: "center",
    ...spacingStyle,
    fontSize: style?.fontSize || "16px",
    height: "fit-content",
    ...style
  }} {...rest}>
      <span style={{
    marginRight: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
      </span>
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      {middleText && <>
          <Icon icon="circle" size={2} />
          <span style={{
    margin: "0 8px",
    fontWeight: "bold",
    color: color,
    opacity: 0.7
  }}>
            {middleText}
          </span>
          <Icon icon="circle" size={2} />
        </>}
      <div style={{
    flex: 1,
    height: "1px",
    background: "var(--lp-color-border-default)",
    opacity: 0.4
  }}></div>
      <span style={{
    marginLeft: "var(--lp-spacing-px-8)",
    opacity: 0.2
  }}>
        <span style={{
    display: "inline-block",
    transform: "scaleX(-1)"
  }}>
          <Icon icon="/snippets/assets/logos/Livepeer-Logo-Symbol-Theme.svg" />
        </span>
      </span>
    </div>;
};

export const TableCell = ({children, align = "left", header = false, style = {}, className = "", ...rest}) => {
  const Component = header ? "th" : "td";
  return <Component className={className} style={{
    padding: "0.75rem 1rem",
    textAlign: align,
    border: header ? "none" : "1px solid var(--lp-color-border-default)",
    ...style
  }} {...rest}>
      {children}
    </Component>;
};

export const TableRow = ({children, header = false, hover = false, style = {}, className = "", ...rest}) => {
  const rowId = `table-row-${Math.random().toString(36).substr(2, 9)}`;
  return <>
      {hover && <style>{`
          #${rowId}:hover {
            background-color: var(--lp-color-bg-card);
          }
        `}</style>}
      <tr id={rowId} className={className} style={{
    ...header && ({
      backgroundColor: "var(--lp-color-accent-strong)",
      color: "var(--lp-color-on-accent)",
      fontWeight: "bold"
    }),
    ...style
  }} {...rest}>
        {children}
      </tr>
    </>;
};

export const StyledTable = ({children, variant = "default", style = {}, className = "", ...rest}) => {
  const wrapperVariants = {
    default: {
      border: "1px solid var(--lp-color-border-default)",
      backgroundColor: "var(--lp-color-bg-card)",
      overflow: "hidden"
    },
    bordered: {
      border: "2px solid var(--lp-color-accent)",
      backgroundColor: "var(--lp-color-bg-page)",
      overflow: "hidden"
    },
    minimal: {
      border: "none",
      backgroundColor: "transparent",
      overflow: "visible"
    }
  };
  return <div data-docs-styled-table-shell className={className} style={{
    width: "100%",
    padding: 0,
    margin: 0,
    ...wrapperVariants[variant],
    ...style
  }} {...rest}>
      <table data-docs-styled-table style={{
    width: "100%",
    borderCollapse: "collapse",
    borderSpacing: 0,
    margin: 0,
    backgroundColor: "transparent"
  }}>
        {children}
      </table>
    </div>;
};

export const LinkArrow = ({href, label, description, newline = true, borderColor, className = '', style = {}, ...rest}) => {
  const linkArrowStyle = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    gap: "var(--lp-spacing-1)",
    width: 'fit-content',
    ...borderColor && ({
      borderColor
    })
  };
  return <span className={className} style={style} {...rest}>
      {newline && <br />}
      <span style={linkArrowStyle}>
        <a href={href} target="_blank" rel="noopener noreferrer">
          {label}
        </a>
        <Icon icon="arrow-up-right" size={14} color="var(--lp-color-accent)" />
      </span>
      {description && description}
      {description && <div style={{
    height: "var(--lp-spacing-3)"
  }} />}
    </span>;
};

<CenteredContainer preset="readable90">
  <Tip>NaaP is a micro-frontend architecture. The shell is a Next.js 15 host. Plugins compile to UMD bundles and load at runtime. Each plugin gets a ShellContext object with auth, navigation, theming, and database access.</Tip>
</CenteredContainer>

<CustomDivider />

## Micro-frontend design

NaaP is a micro-frontend architecture. The shell application is a Next.js 15 host. Plugins are compiled to UMD bundles and loaded at runtime via a plugin registry. Each plugin gets a `ShellContext` object on mount - this is the entire interface between a plugin and the platform.

The shell provides these services to every plugin via `ShellContext`:

```typescript icon="terminal" theme={"theme":{"light":"github-light","dark":"dark-plus"}}
interface ShellContext {
  auth: IAuthService;           // Authentication and authorisation
  navigate: NavigateFunction;   // Client-side navigation
  eventBus: IEventBus;          // Inter-plugin communication
  theme: IThemeService;         // Theme management
  notifications: INotificationService; // Toast notifications
  integrations: IIntegrationService;   // AI, storage, email
  logger: ILoggerService;       // Structured logging
  permissions: IPermissionService;     // Permission checking
  tenant?: ITenantService;      // Tenant context
  team?: ITeamContext;          // Team context
}
```

Plugins own a full vertical slice: frontend React components, backend API logic, and an isolated PostgreSQL schema within the shared database. In production (Vercel), plugin backends run as Next.js API route handlers at `/api/v1/[plugin-name]/*`. In local development, the shell proxies the same routes to standalone Express backends on ports 4001-4012.

The following diagram shows the complete request flow from browser to database.

<ScrollableDiagram title="NaaP request flow" maxHeight="480px">
  ```mermaid theme={"theme":{"light":"github-light","dark":"dark-plus"}}
  %%{init: {'theme': 'base', 'themeVariables': { 'primaryColor': '#1a1a1a', 'primaryTextColor': '#fff', 'primaryBorderColor': '#2d9a67', 'lineColor': '#2d9a67', 'secondaryColor': '#0d0d0d', 'tertiaryColor': '#1a1a1a', 'background': '#0d0d0d', 'fontFamily': 'system-ui' }}}%%
  flowchart TB
      Browser["Browser"]

      subgraph Shell["Shell - operator.livepeer.org (Next.js 15)"]
          Auth["Auth + RBAC"]
          Registry["Plugin Registry"]
          EventBus["Event Bus"]
          Loader["UMD Plugin Loader"]
      end

      subgraph Plugins["Plugins (UMD bundles)"]
          DevAPI["Developer API Manager"]
          Market["Plugin Marketplace"]
          Capacity["Capacity Planner"]
          Wallet["My Wallet"]
          Other["... community plugins"]
      end

      subgraph API["API Layer"]
          Routes["/api/v1/[plugin]/* (Vercel)\nExpress :4001-4012 (local dev)"]
      end

      subgraph Data["@naap/database"]
          PG["PostgreSQL\n(Neon in production)\nper-plugin schema isolation"]
      end

      Browser --> Shell
      Shell --> Loader
      Loader --> Plugins
      Plugins --> API
      API --> Data

      classDef default fill:#1a1a1a,color:#fff,stroke:#2d9a67,stroke-width:2px
  ```
</ScrollableDiagram>

<CustomDivider />

## Installed plugins

NaaP ships with 12 plugins covering developer, operator, monitoring, and governance use cases. The Plugin Marketplace plugin manages installation of additional community plugins.

<StyledTable variant="bordered">
  <thead>
    <TableRow header>
      <TableCell header>Plugin</TableCell>
      <TableCell header>Category</TableCell>
      <TableCell header>What it does</TableCell>
    </TableRow>
  </thead>

  <tbody>
    <TableRow>
      <TableCell>**Plugin Marketplace**</TableCell>
      <TableCell>Core</TableCell>
      <TableCell>Discover, install, and manage plugins within the shell</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>**Developer API Manager**</TableCell>
      <TableCell>Developer</TableCell>
      <TableCell>Create and manage API keys, browse AI model listings, configure Gateway connections, monitor usage quotas. Integrates with pymthouse as a billing provider via OAuth.</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>**Plugin Publisher**</TableCell>
      <TableCell>Developer</TableCell>
      <TableCell>Validate, publish, and manage plugins in the NaaP marketplace</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>**Capacity Planner**</TableCell>
      <TableCell>Monitoring</TableCell>
      <TableCell>Plan and monitor compute capacity across Livepeer node infrastructure</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>**My Dashboard**</TableCell>
      <TableCell>Analytics</TableCell>
      <TableCell>Embedded analytics dashboards sourced via the Dashboard Data Provider pattern</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>**My Wallet**</TableCell>
      <TableCell>Finance</TableCell>
      <TableCell>MetaMask wallet integration and LPT staking operations</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>**Daydream Video**</TableCell>
      <TableCell>Media</TableCell>
      <TableCell>Real-time AI video generation via the Livepeer AI subnet</TableCell>
    </TableRow>

    <TableRow>
      <TableCell>**Community Hub**</TableCell>
      <TableCell>Social</TableCell>
      <TableCell>Community forum and discussion for Livepeer Network participants</TableCell>
    </TableRow>
  </tbody>
</StyledTable>

<CustomDivider />

<CustomDivider />

The [building a plugin](/v2/developers/build/plugins-and-extensions/building-a-plugin) page covers the CLI, scaffold commands, and SDK hooks for creating your own plugin.
