Client Guide: Indexing Resources in LFX

May 28, 2026 · View on GitHub

This guide explains how to send messages to the LFX Indexer Service to index your resources in OpenSearch. It is a caller-facing walkthrough. The authoritative generic envelope, IndexingConfig, OpenSearch document fields, event semantics, and schema evolution policy live in docs/indexer-contract.md.

Table of Contents

Overview

The LFX Indexer Service processes messages from NATS and indexes resources into OpenSearch for search and discovery. Clients should provide complete indexing metadata via the indexing_config field to ensure proper access control and search behavior.

Message Format

All indexing messages follow the IndexerMessageEnvelope structure:

type IndexerMessageEnvelope struct {
    Action         string                 // "created", "updated", or "deleted"
    Headers        map[string]string      // Authentication headers
    Data           any                    // Resource data (map or string for deletes)
    Tags           []string               // Optional exact tags merged with indexing_config.tags
    IndexingConfig *IndexingConfig        // Required for create/update; omit for delete
}

Publishing Messages

Publish to NATS subjects:

  • Object-specific: lfx.index.<object_type> (e.g., lfx.index.project)

Using indexing_config

Create and update messages must include the indexing_config field to provide complete indexing metadata. Delete messages omit indexing_config — the indexer only needs the object ID to remove the document. indexing_config gives you full control over how your resources are indexed and ensures proper access control.

Benefits:

  • Full control over indexing behavior
  • Explicit access control configuration
  • Works for any object type
  • Consistent indexing across all clients

Example:

{
  "action": "created",
  "headers": {
    "authorization": "Bearer <token>"
  },
  "data": {
    "uid": "proj-123",
    "name": "My Project",
    "slug": "my-project",
    "description": "A sample project"
  },
  "indexing_config": {
    "object_id": "proj-123",
    "public": true,
    "access_check_object": "project:proj-123",
    "access_check_relation": "viewer",
    "history_check_object": "project:proj-123",
    "history_check_relation": "historian",
    "sort_name": "my project",
    "name_and_aliases": ["My Project", "my-project"],
    "parent_refs": ["org:org-456"],
    "tags": ["uid:proj-123", "slug:my-project", "featured", "active"],
    "fulltext": "My Project - A sample project for demonstration"
  }
}

The server will:

  • Use the provided config directly
  • Set server-side fields: latest, timestamps, and principals
  • Index the document with your exact specifications

Template Support in indexing_config

To reduce message payload size and eliminate redundancy, indexing_config supports template variables that reference fields from the data object using {{ field_name }} syntax.

Benefits:

  • Reduced Payload Size: Avoid repeating the same values multiple times
  • Single Source of Truth: Data field values are the canonical source
  • Less Error-Prone: No risk of mismatched values between data and config
  • Type Preservation: Templates can preserve original data types

Template Syntax:

Templates use double curly braces: {{ field_name }}

Features:

  1. Simple Templates: "{{ uid }}" - Entire field value (preserves type)
  2. Embedded Templates: "project:{{ uid }}" - Template within a string
  3. Nested Fields: "{{ parent.id }}" - Dot notation for nested objects
  4. Arrays: ["{{ uid }}", "{{ parent_id }}"] - Templates in arrays
  5. Multiple Templates: "{{ name }} - {{ description }}" - Multiple in one string
  6. Escaping: "\{{ not_a_template }}" - Use backslash to escape

Example with Templates:

{
  "action": "created",
  "headers": {
    "authorization": "Bearer eyJhbGc..."
  },
  "data": {
    "uid": "proj-123",
    "name": "My Project",
    "slug": "my-project",
    "parent_id": "org-456",
    "metadata": {
      "organization": {
        "id": "org-789"
      }
    }
  },
  "indexing_config": {
    "object_id": "{{ uid }}",
    "public": true,
    "access_check_object": "project:{{ uid }}",
    "access_check_relation": "viewer",
    "history_check_object": "project:{{ uid }}",
    "history_check_relation": "historian",
    "sort_name": "{{ name }}",
    "name_and_aliases": ["{{ name }}", "{{ slug }}"],
    "parent_refs": ["org:{{ parent_id }}", "org:{{ metadata.organization.id }}"],
    "tags": ["uid:{{ uid }}", "slug:{{ slug }}"],
    "fulltext": "{{ name }} - Project in organization {{ metadata.organization.id }}"
  }
}

Equivalent without templates (notice the redundancy):

{
  "action": "created",
  "headers": {
    "authorization": "Bearer eyJhbGc..."
  },
  "data": {
    "uid": "proj-123",
    "name": "My Project",
    "slug": "my-project",
    "parent_id": "org-456",
    "metadata": {
      "organization": {
        "id": "org-789"
      }
    }
  },
  "indexing_config": {
    "object_id": "proj-123",
    "public": true,
    "access_check_object": "project:proj-123",
    "access_check_relation": "viewer",
    "history_check_object": "project:proj-123",
    "history_check_relation": "historian",
    "sort_name": "My Project",
    "name_and_aliases": ["My Project", "my-project"],
    "parent_refs": ["org:org-456", "org:org-789"],
    "tags": ["uid:proj-123", "slug:my-project"],
    "fulltext": "My Project - Project in organization org-789"
  }
}

Template Rules:

  1. Field Resolution: Templates reference fields in the data object
  2. Nested Access: Use dot notation (e.g., {{ parent.id }}) for nested objects
  3. Type Preservation: Simple templates ("{{ field }}") preserve the original type
  4. String Conversion: Embedded templates convert values to strings
  5. Error Handling: Missing fields in required whole-value templates become empty and then fail required-field validation; missing fields in optional or embedded templates are skipped with a warning and blank substitution. Traversing through a non-object still fails processing.
  6. Escaping: Use \{{ }} to include literal curly braces in values
  7. Whitespace: Whitespace inside templates is trimmed (e.g., {{ uid }}uid)

Type Preservation Example:

{
  "data": {
    "uid": "proj-123",
    "count": 42,
    "active": true
  },
  "indexing_config": {
    "object_id": "{{ uid }}",           // Becomes: "proj-123" (string)
    "sort_name": "Count: {{ count }}",  // Becomes: "Count: 42" (string - embedded)
    ...
  }
}

Best Practices:

  • Use templates for repeated values (e.g., UIDs in multiple FGA fields)
  • Use nested field access to avoid flattening your data structure
  • Escape templates when you need literal {{ }} in your data
  • Ensure referenced fields exist in data to avoid processing errors

Field Reference

Required Fields (All Messages)

FieldTypeDescription
actionstringOperation type: "created", "updated", or "deleted"
headersobjectAuthentication headers (must include authorization for V2)
dataobject/stringResource data (object for create/update, string ID for delete)

Top-Level Fields

FieldTypeRequiredDescription
tagsarrayNoOptional exact tags merged with indexing_config.tags. Prefer indexing_config.tags for new publishers.
indexing_configobjectYes (create/update)Indexing metadata controlling access control, search, and sort behavior. Not required for delete.

IndexingConfig Fields

Required indexing_config Fields

FieldTypeDescriptionExample
object_idstringUnique identifier for the resource"proj-123"
access_check_objectstringFGA object for access checks"project:proj-123"
access_check_relationstringFGA relation for access checks"viewer"
history_check_objectstringFGA object for history checks"project:proj-123"
history_check_relationstringFGA relation for history checks"historian"

Optional indexing_config Fields

FieldTypeDescriptionExample
publicbooleanWhether resource is publicly accessibletrue
sort_namestringNormalized name for sorting"my project"
name_and_aliasesarrayNames and aliases for search["My Project", "MP"]
parent_refsarrayReferences to parent resources["org:org-456"]
tagsarrayExact-match tags for filtering["uid:proj-123", "status:active"]
fulltextstringFull-text search content"searchable content"
contactsarrayContact information for the resource (see Contact structure below)See example below

Contact Structure

Each contact in the contacts array supports the following fields:

FieldTypeDescriptionExample
lfx_principalstringLFX principal ID for the contact"user:user-123"
namestringDisplay name of the contact"John Doe"
emailsarrayEmail addresses for the contact["john@example.com"]
botbooleanWhether the contact is a bot accountfalse
profileobjectAdditional profile information{"title": "Developer"}

Example contacts array:

{
  "contacts": [
    {
      "lfx_principal": "user:user-123",
      "name": "John Doe",
      "emails": ["john@example.com", "jdoe@example.com"],
      "bot": false,
      "profile": {
        "title": "Project Lead",
        "organization": "ACME Corp"
      }
    },
    {
      "name": "Jane Smith",
      "emails": ["jane@example.com"]
    }
  ]
}

Note: Contacts support template expansion, allowing you to reference fields from the data object:

{
  "data": {
    "uid": "proj-123",
    "owner_id": "user-456",
    "owner_name": "Alice Johnson",
    "owner_email": "alice@example.com"
  },
  "indexing_config": {
    "object_id": "{{ uid }}",
    "contacts": [
      {
        "lfx_principal": "{{ owner_id }}",
        "name": "{{ owner_name }}",
        "emails": ["{{ owner_email }}"]
      }
    ]
  }
}

Server-Side Fields (Automatically Set)

These fields are always set by the server and should not be included in your message:

FieldDescriptionSet By
latestBoolean flag (always true)Server
created_atTimestamp for create actionsServer (from message timestamp)
updated_atTimestamp for update actionsServer (from message timestamp)
deleted_atTimestamp for delete actionsServer (from message timestamp)
created_byPrincipal(s) who created the resourceServer (from auth headers)
updated_byPrincipal(s) who updated the resourceServer (from auth headers)
deleted_byPrincipal(s) who deleted the resourceServer (from auth headers)
created_by_principalsPrincipal IDs onlyServer (extracted from tokens)
updated_by_principalsPrincipal IDs onlyServer (extracted from tokens)
deleted_by_principalsPrincipal IDs onlyServer (extracted from tokens)
created_by_emailsEmail addresses onlyServer (extracted from tokens)
updated_by_emailsEmail addresses onlyServer (extracted from tokens)
deleted_by_emailsEmail addresses onlyServer (extracted from tokens)

Examples

Example 1: Create Resource

NATS Subject: lfx.index.project

{
  "action": "created",
  "headers": {
    "authorization": "Bearer eyJhbGc..."
  },
  "data": {
    "uid": "proj-123",
    "name": "My Project",
    "slug": "my-project",
    "parent_id": "org-456"
  },
  "indexing_config": {
    "object_id": "proj-123",
    "public": true,
    "access_check_object": "project:proj-123",
    "access_check_relation": "viewer",
    "history_check_object": "project:proj-123",
    "history_check_relation": "historian",
    "sort_name": "my project",
    "name_and_aliases": ["My Project", "my-project"],
    "parent_refs": ["org:org-456"],
    "tags": ["uid:proj-123", "slug:my-project", "featured"],
    "fulltext": "My Project - A sample project"
  }
}

Example 2: Update Resource

NATS Subject: lfx.index.project

{
  "action": "updated",
  "headers": {
    "authorization": "Bearer eyJhbGc..."
  },
  "data": {
    "uid": "proj-123",
    "name": "Updated Project Name",
    "slug": "my-project"
  },
  "indexing_config": {
    "object_id": "proj-123",
    "public": false,
    "access_check_object": "project:proj-123",
    "access_check_relation": "viewer",
    "history_check_object": "project:proj-123",
    "history_check_relation": "historian",
    "sort_name": "updated project name",
    "name_and_aliases": ["Updated Project Name"]
  }
}

Example 3: Delete Resource

NATS Subject: lfx.index.project

{
  "action": "deleted",
  "headers": {
    "authorization": "Bearer eyJhbGc..."
  },
  "data": "proj-123"
}

Note: For delete operations, indexing_config is not needed. The server only requires the resource ID.

Best Practices

Access Control

  1. FGA Pattern: Use the pattern <type>:<id>#<relation> for FGA queries

    • Example: project:proj-123#viewer
  2. Access vs History: Distinguish between access and history permissions

    • access_check_*: Who can view the current state
    • history_check_*: Who can view the change history
  3. Public Flag: Set appropriately to enable/disable public access

    • true: Resource is publicly searchable
    • false: Resource requires authorization

Search Optimization

  1. sort_name: Normalize for consistent sorting

    • Lowercase
    • Remove special characters
    • Example: "My Project!" → "my project"
  2. name_and_aliases: Include all searchable variations

    • Full name
    • Short name
    • Common abbreviations
    • Slug/URL-friendly name
  3. parent_refs: Maintain resource hierarchy

    • Use format: <type>:<id>
    • Example: ["org:org-456", "team:team-789"]
  4. tags: Add categorical tags

    • Keep tags concise
    • Use consistent naming conventions
    • Example: ["uid:proj-123", "status:active", "featured"]
  5. fulltext: Construct comprehensive search text

    • Include name, description, and other searchable content
    • Keep it concise but complete

Authentication

  1. V2 Messages: Always include authorization header

    "headers": {
      "authorization": "Bearer <jwt-token>"
    }
    
  2. Token Requirements:

    • Must be a valid JWT token
    • Validated against Heimdall service
    • Used to extract principal information for audit fields

Error Handling

The indexer service will reply with:

  • "OK" on success
  • "ERROR: <details>" on failure

Common errors:

  • Missing required indexing_config fields
  • Invalid action type
  • Missing authorization header
  • Malformed FGA queries

Performance Considerations

  1. Message Size: indexing_config increases payload size

    • Average overhead: ~500-1000 bytes
    • Consider compression for high-volume scenarios
  2. Processing Speed: indexing_config is required and drives all indexing decisions

    • Reduced server CPU usage
  3. Batch Operations: Use NATS queue groups for load balancing

    • Service uses queue group: lfx.indexer.queue
    • Messages distributed across instances

Go Client Example

For Go services, use the public types package:

import (
    "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/constants"
    "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/types"
)

// Create message with indexing config
publicFlag := true
envelope := &types.IndexerMessageEnvelope{
    Action: constants.ActionCreated,
    Headers: map[string]string{
        constants.AuthorizationHeader: "Bearer " + token,
    },
    Data: map[string]any{
        "id":   "proj-123",
        "name": "My Project",
    },
    IndexingConfig: &types.IndexingConfig{
        ObjectID:             "proj-123",
        Public:               &publicFlag,
        AccessCheckObject:    "project:proj-123",
        AccessCheckRelation:  "viewer",
        HistoryCheckObject:   "project:proj-123",
        HistoryCheckRelation: "historian",
        SortName:             "my project",
        NameAndAliases:       []string{"My Project"},
        Tags:                 []string{"uid:proj-123", "featured"},
    },
}

// Marshal and publish to NATS
data, _ := json.Marshal(envelope)
nc.Publish("lfx.index.project", data)

Troubleshooting

Issue: Document missing server-side fields

Problem: latest, timestamp, or principal fields are missing in OpenSearch.

Solution: Confirm the write went through indexer-service and returned OK. Create/update documents get created_at or updated_at; delete documents get deleted_at. Principal fields require a valid lower-case authorization header on V2 messages or x-username / x-email on V1 messages.

Issue: "indexing_config is required" error

Problem: Sending create/update messages without an indexing_config block.

Solution: Add indexing_config to your message. The indexer is data-agnostic and requires clients to provide complete indexing metadata.

Issue: Access control not working

Problem: Users can't access indexed resources.

Solution: Verify FGA configuration:

  • Check access_check_object matches your FGA object pattern
  • Verify access_check_relation is configured in FGA
  • Ensure public flag is set correctly

Issue: Search not finding resources

Problem: Resources don't appear in search results.

Solution: Improve searchability fields:

  • Add comprehensive name_and_aliases
  • Set meaningful fulltext content
  • Add relevant tags
  • Verify public flag for public searches

Additional Resources