Home / books / building-with-larc / appendix-a-message-topics

Message Topics Reference

This appendix provides a comprehensive reference for LARC message topic conventions. Topics are the fundamental addressing mechanism in LARC applications—they determine how messages are routed, who receives them, and how components interact. Understanding topic patterns is essential for building scalable, maintainable LARC applications.

Topic Format and Structure

Standard Format

LARC topics follow a hierarchical dotted notation:

resource.action.qualifier
Components:
  • Resource: The domain entity or system (users, posts, nav, ui, auth)
  • Action: The operation or event type (list, item, get, save, updated)
  • Qualifier: Additional context (state, request, reply, event)
This three-segment format provides clarity while remaining flexible enough for various use cases. More segments can be added when needed for specificity.

Topic Examples

| Topic | Resource | Action | Qualifier | Purpose | |-------|----------|--------|-----------|---------| | users.list.state | users | list | state | Current user list (retained) | | users.item.get | users | item | get | Fetch single user (request) | | posts.item.updated | posts | item | updated | Post was modified (event) | | nav.goto | nav | goto | — | Navigate to route (command) | | ui.modal.opened | ui | modal.opened | — | Modal opened (event) | | auth.session.state | auth | session | state | Current session (retained) |

Naming Rules

Case and Separators:
  • Use lowercase letters only
  • Separate segments with dots (.)
  • Use hyphens (-) within a segment if needed: auth.two-factor.verify
Character Set:
  • Alphanumeric characters: a-z, 0-9
  • Dots for segment separation: .
  • Hyphens for compound words: -
  • No underscores, spaces, or special characters
Length:
  • Keep topics concise but descriptive
  • Typical range: 15-40 characters
  • Avoid abbreviations that sacrifice clarity

Topic Patterns and Matching

Exact Match

The simplest pattern matches a specific topic exactly:

client.subscribe('users.item.updated', (msg) => {
  console.log('User updated:', msg.data);
});

Receives only messages published to users.item.updated.

Single-Segment Wildcard

The asterisk (*) matches exactly one segment:

| Pattern | Matches | Does Not Match | |---------|---------|----------------| | users.* | users.list, users.item | users.list.state, users.item.updated | | *.updated | users.updated, posts.updated | users.item.updated | | users.*.state | users.list.state, users.item.state | users.state, users.list.item.state |

// Subscribe to all user-related events
client.subscribe('users.*', (msg) => {
  console.log('User event:', msg.topic);
});

Global Wildcard

The special pattern * matches all topics:

// Monitor all messages (use sparingly)
client.subscribe('*', (msg) => {
  console.log('[ALL]', msg.topic, msg.data);
});
Warning: Global wildcard subscriptions match every message in the system. Use them only for debugging, logging, or analytics. They can significantly impact performance in high-throughput applications.

Pattern Matching Examples

// Match all list operations
client.subscribe('*.list.*', (msg) => {
  // Matches: users.list.state, posts.list.get, comments.list.get
});

// Match all state topics
client.subscribe('*.*.state', (msg) => {
  // Matches: users.list.state, app.theme.state, nav.route.state
});

// Match all operations on items
client.subscribe('*.item.*', (msg) => {
  // Matches: users.item.get, posts.item.save, users.item.updated
});

Reserved Topic Namespaces

Certain topic namespaces are reserved for LARC internal use. Applications must not publish to these topics directly.

pan:* Namespace (System Internal)

The pan:* namespace is reserved for PAN bus internals:

| Topic | Purpose | Usage | |-------|---------|-------| | pan:sys.ready | Bus ready signal | Listen only | | pan:sys.stats | Bus statistics | Request only | | pan:sys.error | System errors | Listen only | | pan:sys.clear-retained | Clear retained messages | Request only | | pan:publish | Internal publish event | Internal only | | pan:subscribe | Internal subscribe event | Internal only | | pan:unsubscribe | Internal unsubscribe event | Internal only | | pan:deliver | Internal message delivery | Internal only | | pan:hello | Client registration | Internal only | | pan:$reply:* | Auto-generated reply topics | Internal only |

Never:
  • Publish to pan:* topics directly
  • Subscribe to internal topics like pan:publish or pan:subscribe
  • Manually create pan:$reply:* topics (these are auto-generated by request())
Exception: You may subscribe to system notification topics like pan:sys.ready and pan:sys.error.

sys:* Namespace (System Reserved)

The sys:* namespace is reserved for future system-level functionality:

sys:error               # Future: System-wide errors
sys:perf                # Future: Performance monitoring
sys:debug               # Future: Debug information
sys:config              # Future: Configuration changes

Do not use sys:* topics in application code.

Application Namespaces

Your application should establish its own top-level namespaces:

| Namespace | Purpose | Examples | |-----------|---------|----------| | app.* | Application-level concerns | app.config.state, app.theme.state | | auth.* | Authentication/authorization | auth.login, auth.session.state | | session.* | Session management | session.started, session.expired | | nav.* | Navigation | nav.goto, nav.route.state | | ui.* | UI components | ui.modal.opened, ui.toast.show | | analytics.* | Analytics tracking | analytics.event, analytics.page-view |

CRUD Topic Patterns

CRUD operations (Create, Read, Update, Delete) follow consistent topic patterns across all resources.

List Operations

#### List State (Retained)

Topic: ${resource}.list.state Purpose: Retained snapshot of current list data. New subscribers receive the most recent list immediately. Message Format:
{
  topic: 'users.list.state',
  data: {
    items: [/* array of items */],
    total: 150,              // Total count (for pagination)
    page: 1,                 // Current page
    filter: {},              // Active filters
    sort: 'name-asc'         // Active sort
  },
  retain: true
}
Usage:
// Publish list state
client.publish({
  topic: 'users.list.state',
  data: {
    items: users,
    total: users.length,
    page: 1
  },
  retain: true
});

// Subscribe to list state
client.subscribe('users.list.state', (msg) => {
  renderList(msg.data.items);
}, { retained: true });

#### List Get (Request)

Topic: ${resource}.list.get Purpose: Request to fetch list data with optional parameters. Request Format:
{
  topic: 'users.list.get',
  data: {
    page: 1,
    limit: 20,
    filter: { active: true },
    sort: 'name-asc'
  }
}
Response Format:
{
  ok: true,
  items: [/* array */],
  total: 150,
  page: 1
}
Usage:
// Request list
const response = await client.request('users.list.get', {
  page: 1,
  limit: 20,
  filter: { active: true }
});

if (response.data.ok) {
  renderList(response.data.items);
}

Item Operations

#### Item Get (Request)

Topic: ${resource}.item.get Purpose: Fetch a single item by ID. Request Format:
{
  topic: 'users.item.get',
  data: { id: 123 }
}
Response Format:
// Success
{ ok: true, item: { id: 123, name: 'Alice', email: '...' } }

// Not found
{ ok: false, error: 'Not found', code: 'NOT_FOUND' }

#### Item Save (Request)

Topic: ${resource}.item.save Purpose: Create or update an item. If id is present, updates existing item. If id is omitted, creates new item. Request Format:
{
  topic: 'users.item.save',
  data: {
    item: {
      id: 123,              // Omit for create
      name: 'Alice',
      email: 'alice@example.com'
    }
  }
}
Response Format:
// Success
{ ok: true, item: { id: 123, name: 'Alice', email: '...' } }

// Validation error
{ ok: false, error: 'Invalid email', code: 'VALIDATION_ERROR' }

#### Item Delete (Request)

Topic: ${resource}.item.delete Purpose: Delete an item by ID. Request Format:
{
  topic: 'users.item.delete',
  data: { id: 123 }
}
Response Format:
// Success
{ ok: true, id: 123 }

// Not found
{ ok: false, error: 'Not found', code: 'NOT_FOUND' }

#### Item Select (Event)

Topic: ${resource}.item.select Purpose: User selected/focused an item (no reply expected). Message Format:
{
  topic: 'users.item.select',
  data: { id: 123 }
}
Usage:
// Publish selection
client.publish({
  topic: 'users.item.select',
  data: { id: userId }
});

// Handle selection
client.subscribe('users.item.select', (msg) => {
  highlightItem(msg.data.id);
  loadDetails(msg.data.id);
});

Item Events

Item events notify about completed operations:

| Topic | Trigger | Data | |-------|---------|------| | \${resource}.item.created | Item created | { item: {...} } | | \${resource}.item.updated | Item updated | { item: {...} } | | \${resource}.item.deleted | Item deleted | { id: 123 } |

Example:
// After save operation completes
client.publish({
  topic: 'users.item.updated',
  data: { item: savedUser }
});

Per-Item State

For tracking state of individual items:

Topic: ${resource}.item.state.${id} Purpose: Retained state for a specific item (e.g., online status, typing indicator). Example:
// Publish item state
client.publish({
  topic: `users.item.state.${userId}`,
  data: {
    id: userId,
    online: true,
    typing: false,
    lastSeen: Date.now()
  },
  retain: true
});

// Subscribe to specific item
client.subscribe(`users.item.state.${userId}`, (msg) => {
  updatePresence(msg.data);
}, { retained: true });

// Subscribe to all item states
client.subscribe('users.item.state.*', (msg) => {
  updatePresence(msg.data);
});

State Management Topics

State topics use the .state qualifier and are always retained.

Global State

Pattern: ${domain}.state Examples:
'app.config.state'          # Application configuration
'app.theme.state'           # Current theme
'app.language.state'        # Current language
'ui.sidebar.state'          # Sidebar open/closed
'ui.loading.state'          # Loading indicator
Usage:
// Publish state
client.publish({
  topic: 'app.theme.state',
  data: { mode: 'dark', accent: '#007bff' },
  retain: true
});

// Subscribe (receives current state immediately)
client.subscribe('app.theme.state', (msg) => {
  applyTheme(msg.data);
}, { retained: true });

Scoped State

Pattern: ${domain}.${scope}.state Examples:
'users.list.state'          # User list
'auth.session.state'        # Current session
'nav.route.state'           # Current route
'search.query.state'        # Search query
'filters.active.state'      # Active filters

Events vs Commands

Distinguish between events (past tense) and commands (imperative).

Events

Events describe something that already happened. They use past tense.

Characteristics:
  • Past tense verbs: created, updated, deleted, opened, closed
  • Fire-and-forget (no reply expected)
  • Multiple subscribers allowed
  • Informational
Examples:

| Topic | Description | |-------|-------------| | users.item.created | A user was created | | users.item.updated | A user was updated | | ui.modal.opened | A modal was opened | | ui.modal.closed | A modal was closed | | session.started | Session started | | session.expired | Session expired | | auth.login.success | Login succeeded | | auth.login.failed | Login failed | | nav.navigated | Navigation completed |

Usage:
// Publish event
client.publish({
  topic: 'users.item.created',
  data: { item: newUser }
});

// Multiple handlers can react
client.subscribe('users.item.created', logAnalytics);
client.subscribe('users.item.created', sendWelcomeEmail);
client.subscribe('users.item.created', updateDashboard);

Commands

Commands request something to happen. They use imperative/verb form.

Characteristics:
  • Imperative verbs: save, delete, open, close, goto
  • May expect reply (request/reply pattern)
  • Usually single handler
  • May fail
Examples:

| Topic | Description | |-------|-------------| | users.item.save | Save a user | | users.item.delete | Delete a user | | ui.modal.open | Open a modal | | ui.modal.close | Close a modal | | nav.goto | Navigate to route | | nav.back | Go back in history | | auth.login | Perform login | | auth.logout | Perform logout |

Usage:
// Fire-and-forget command
client.publish({
  topic: 'nav.goto',
  data: { route: '/users/123' }
});

// Request command (expect reply)
const response = await client.request('users.item.save', {
  item: { name: 'Alice' }
});

Domain-Specific Patterns

Authentication

// Commands
'auth.login'                # Login request
'auth.logout'               # Logout request
'auth.refresh'              # Refresh token
'auth.verify'               # Verify credentials

// Events
'auth.login.success'        # Login succeeded
'auth.login.failed'         # Login failed
'auth.logout'               # User logged out
'auth.token.expired'        # Token expired

// State
'auth.session.state'        # Current session (retained)
'auth.user.state'           # Current user info (retained)

Navigation

// Commands
'nav.goto'                  # Navigate to route
'nav.back'                  # Go back
'nav.forward'               # Go forward
'nav.replace'               # Replace current route

// Events
'nav.navigated'             # Navigation completed
'nav.error'                 # Navigation error

// State
'nav.route.state'           # Current route (retained)
'nav.history.state'         # History stack (retained)

UI Components

// Modal
'ui.modal.open'             # Command: open modal
'ui.modal.close'            # Command: close modal
'ui.modal.opened'           # Event: modal opened
'ui.modal.closed'           # Event: modal closed
'ui.modal.state'            # State: current modal (retained)

// Sidebar
'ui.sidebar.toggle'         # Command: toggle sidebar
'ui.sidebar.open'           # Command: open sidebar
'ui.sidebar.close'          # Command: close sidebar
'ui.sidebar.state'          # State: open/closed (retained)

// Toast
'ui.toast.show'             # Command: show toast
'ui.toast.hide'             # Command: hide toast

// Loading
'ui.loading.start'          # Command: start loading
'ui.loading.stop'           # Command: stop loading
'ui.loading.state'          # State: loading status (retained)

Forms

// Validation
'form.validate'             # Command: validate form
'form.validated'            # Event: validation complete
'form.validation.state'     # State: validation errors (retained)

// Submission
'form.submit'               # Command: submit form
'form.submitted'            # Event: form submitted
'form.submit.success'       # Event: submission succeeded
'form.submit.failed'        # Event: submission failed

// Field changes
'form.field.changed'        # Event: field value changed
'form.field.focused'        # Event: field focused
'form.field.blurred'        # Event: field blurred

Data Synchronization

// Sync commands
'sync.start'                # Start sync
'sync.stop'                 # Stop sync
'sync.refresh'              # Force refresh

// Sync events
'sync.started'              # Sync started
'sync.completed'            # Sync completed
'sync.failed'               # Sync failed
'sync.conflict'             # Sync conflict detected

// Sync state
'sync.status.state'         # Current sync status (retained)
'sync.last-update.state'    # Last update time (retained)

Best Practices

Topic Naming

DO:
  • Use lowercase letters
  • Use dots to separate segments
  • Use descriptive names
  • Be consistent across resources
  • Use .state for retained topics
  • Use past tense for events
  • Use imperative for commands
DON'T:
  • Use underscores or camelCase
  • Use abbreviations that sacrifice clarity
  • Mix naming conventions
  • Use verbs for events (users.update [X] -> users.updated [check])
  • Overuse wildcards (* matches everything)

Topic Catalog

For larger applications, maintain a centralized topic catalog:

// topics.js
export const TOPICS = {
  USERS: {
    LIST: {
      STATE: 'users.list.state',
      GET: 'users.list.get'
    },
    ITEM: {
      GET: 'users.item.get',
      SAVE: 'users.item.save',
      DELETE: 'users.item.delete',
      SELECT: 'users.item.select',
      UPDATED: 'users.item.updated',
      DELETED: 'users.item.deleted',
      STATE: (id) => `users.item.state.${id}`
    }
  },

  NAV: {
    GOTO: 'nav.goto',
    BACK: 'nav.back',
    ROUTE_STATE: 'nav.route.state'
  },

  AUTH: {
    LOGIN: 'auth.login',
    LOGOUT: 'auth.logout',
    SESSION_STATE: 'auth.session.state'
  }
};

// Usage
client.publish({
  topic: TOPICS.USERS.ITEM.UPDATED,
  data: { item: user }
});

Performance Considerations

Wildcard Usage:
  • Avoid global wildcard (*) in production code
  • Prefer specific patterns (users. over )
  • Each wildcard increases matching overhead
Topic Depth:
  • Keep topics shallow (3-4 segments ideal)
  • Deeper hierarchies increase matching cost
  • Balance specificity with performance
State Retention:
  • Use retention sparingly (only for actual state)
  • Don't retain high-volume event streams
  • Clear retained messages when no longer needed

Summary

Key Principles:
  • Standard Format: resource.action.qualifier
  • State Suffix: Always use .state for retained topics
  • Events vs Commands: Past tense for events, imperative for commands
  • Consistency: Use same patterns across all resources
  • Reserved Namespaces: Never use pan: or sys:
  • Wildcards: Use judiciously; prefer specific patterns
  • Documentation: Maintain topic catalog for large apps
  • Quick Reference:

    | Pattern | Example | Use Case | |---------|---------|----------| | \${resource}.list.state | users.list.state | List data (retained) | | \${resource}.list.get | users.list.get | Request list | | \${resource}.item.get | users.item.get | Request single item | | \${resource}.item.save | users.item.save | Save item | | \${resource}.item.delete | users.item.delete | Delete item | | \${resource}.item.updated | users.item.updated | Item updated event | | \${domain}.state | app.theme.state | Global state (retained) | | \${domain}.\${action} | nav.goto | Command |

    For complete API documentation, see the main API Reference.