Documentation

Building Custom Connectors

Create custom connectors to integrate any identity source with TigerIdentity using our powerful SDK.

Overview

The TigerIdentity Connector SDK enables you to build custom connectors for any identity source. Whether you need to integrate with a proprietary system, a legacy database, or a SaaS application without a pre-built connector, the SDK provides all the tools you need.

TypeScript SDK

Fully typed SDK with IntelliSense support.

Local Testing

Test connectors locally before deployment.

Easy Publishing

Publish to the connector registry with one command.

Getting Started

Step 1: Install the SDK

Install the TigerIdentity Connector SDK via npm:

# Create a new connector project
mkdir my-custom-connector
cd my-custom-connector

# Initialize npm project
npm init -y

# Install the SDK
npm install @tigeridentity/connector-sdk

# Install development dependencies
npm install -D typescript @types/node tsx

Step 2: Configure TypeScript

Create a tsconfig.json file:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "lib": ["ES2022"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Implementing the Connector Interface

Create your connector by implementing the Connector interface:

// src/index.ts
import {
  Connector,
  ConnectorConfig,
  ConnectorMetadata,
  SyncResult,
  HealthCheckResult,
  WebhookEvent,
  Identity,
  Group,
} from '@tigeridentity/connector-sdk';

/**
 * Custom connector for internal HR system
 */
export class HRSystemConnector implements Connector {
  private config: ConnectorConfig;
  private apiClient: any; // Your API client

  constructor(config: ConnectorConfig) {
    this.config = config;
    this.initializeClient();
  }

  /**
   * Initialize API client with configuration
   */
  private initializeClient(): void {
    const { apiUrl, apiKey } = this.config;

    // Initialize your API client here
    this.apiClient = {
      baseURL: apiUrl,
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
    };
  }

  /**
   * Return connector metadata
   */
  getMetadata(): ConnectorMetadata {
    return {
      name: 'hr-system',
      displayName: 'Internal HR System',
      description: 'Sync employees and departments from internal HR system',
      version: '1.0.0',
      author: 'Your Company',

      // Configuration schema
      configSchema: {
        type: 'object',
        required: ['apiUrl', 'apiKey'],
        properties: {
          apiUrl: {
            type: 'string',
            title: 'API URL',
            description: 'Base URL of the HR system API',
          },
          apiKey: {
            type: 'string',
            title: 'API Key',
            description: 'API key for authentication',
            secret: true,
          },
          syncDepartments: {
            type: 'boolean',
            title: 'Sync Departments',
            description: 'Include department information in sync',
            default: true,
          },
        },
      },

      // Supported features
      capabilities: {
        sync: true,
        webhook: true,
        incremental: true,
        realtime: false,
      },

      // Resource types
      resourceTypes: ['users', 'groups'],
    };
  }

  /**
   * Perform health check
   */
  async healthCheck(): Promise<HealthCheckResult> {
    try {
      // Test API connectivity
      const response = await fetch(`${this.config.apiUrl}/health`, {
        headers: this.apiClient.headers,
      });

      if (!response.ok) {
        return {
          healthy: false,
          message: `API returned status ${response.status}`,
        };
      }

      return {
        healthy: true,
        message: 'Connection successful',
      };
    } catch (error) {
      return {
        healthy: false,
        message: `Connection failed: ${error.message}`,
        error: error,
      };
    }
  }

  /**
   * Sync identities from the source system
   */
  async onSync(lastSyncTime?: Date): Promise<SyncResult> {
    const startTime = Date.now();
    const identities: Identity[] = [];
    const groups: Group[] = [];
    const errors: any[] = [];

    try {
      // Fetch employees
      const employees = await this.fetchEmployees(lastSyncTime);

      // Transform to Identity objects
      for (const employee of employees) {
        try {
          const identity = this.transformEmployee(employee);
          identities.push(identity);
        } catch (error) {
          errors.push({
            id: employee.id,
            error: error.message,
          });
        }
      }

      // Fetch departments if enabled
      if (this.config.syncDepartments) {
        const departments = await this.fetchDepartments();

        for (const dept of departments) {
          try {
            const group = this.transformDepartment(dept);
            groups.push(group);
          } catch (error) {
            errors.push({
              id: dept.id,
              error: error.message,
            });
          }
        }
      }

      return {
        success: true,
        identities,
        groups,
        syncTime: new Date(),
        duration: Date.now() - startTime,
        stats: {
          identitiesProcessed: identities.length,
          groupsProcessed: groups.length,
          errors: errors.length,
        },
        errors,
      };
    } catch (error) {
      return {
        success: false,
        identities: [],
        groups: [],
        syncTime: new Date(),
        duration: Date.now() - startTime,
        error: error.message,
      };
    }
  }

  /**
   * Handle webhook events
   */
  async onWebhook(event: WebhookEvent): Promise<void> {
    const { type, payload } = event;

    switch (type) {
      case 'employee.created':
      case 'employee.updated':
        const identity = this.transformEmployee(payload);
        await this.emitIdentity(identity);
        break;

      case 'employee.terminated':
        await this.emitIdentityDeletion(payload.id);
        break;

      case 'department.created':
      case 'department.updated':
        const group = this.transformDepartment(payload);
        await this.emitGroup(group);
        break;

      default:
        console.log(`Unhandled webhook event type: ${type}`);
    }
  }

  /**
   * Fetch employees from HR system
   */
  private async fetchEmployees(since?: Date): Promise<any[]> {
    const params = new URLSearchParams();
    if (since) {
      params.append('updatedSince', since.toISOString());
    }

    const response = await fetch(
      `${this.config.apiUrl}/employees?${params}`,
      {
        headers: this.apiClient.headers,
      }
    );

    if (!response.ok) {
      throw new Error(`Failed to fetch employees: ${response.statusText}`);
    }

    const data = await response.json();
    return data.employees || [];
  }

  /**
   * Fetch departments from HR system
   */
  private async fetchDepartments(): Promise<any[]> {
    const response = await fetch(`${this.config.apiUrl}/departments`, {
      headers: this.apiClient.headers,
    });

    if (!response.ok) {
      throw new Error(`Failed to fetch departments: ${response.statusText}`);
    }

    const data = await response.json();
    return data.departments || [];
  }

  /**
   * Transform employee to Identity
   */
  private transformEmployee(employee: any): Identity {
    return {
      id: employee.id,
      email: employee.email,
      username: employee.email.split('@')[0],
      firstName: employee.firstName,
      lastName: employee.lastName,
      displayName: `${employee.firstName} ${employee.lastName}`,
      department: employee.department?.name,
      title: employee.jobTitle,
      manager: employee.managerId,
      status: employee.status === 'active' ? 'active' : 'inactive',

      // Custom attributes
      attributes: {
        employeeId: employee.employeeNumber,
        hireDate: employee.hireDate,
        location: employee.office,
        employeeType: employee.type,
      },

      // Group memberships
      groups: [
        employee.department?.id,
        ...employee.teams?.map((t: any) => t.id) || [],
      ].filter(Boolean),

      source: 'hr-system',
      sourceId: employee.id,
      lastSyncTime: new Date(),
    };
  }

  /**
   * Transform department to Group
   */
  private transformDepartment(dept: any): Group {
    return {
      id: dept.id,
      name: dept.name,
      description: dept.description,

      // Members
      members: dept.employees?.map((e: any) => e.id) || [],

      // Parent group
      parentGroup: dept.parentDepartmentId,

      // Custom attributes
      attributes: {
        costCenter: dept.costCenter,
        location: dept.location,
        manager: dept.managerId,
      },

      source: 'hr-system',
      sourceId: dept.id,
      lastSyncTime: new Date(),
    };
  }

  /**
   * Emit identity update
   */
  private async emitIdentity(identity: Identity): Promise<void> {
    // SDK handles sending to TigerIdentity
    console.log('Emitting identity:', identity.id);
  }

  /**
   * Emit identity deletion
   */
  private async emitIdentityDeletion(id: string): Promise<void> {
    console.log('Emitting identity deletion:', id);
  }

  /**
   * Emit group update
   */
  private async emitGroup(group: Group): Promise<void> {
    console.log('Emitting group:', group.id);
  }
}

// Export the connector
export default HRSystemConnector;

Testing Locally

Test your connector locally before deploying to production.

Create Test Configuration

# test-config.yaml
name: hr-system-test
type: hr-system
enabled: true

config:
  apiUrl: https://hr-system.example.com/api/v1
  apiKey: test-api-key-12345
  syncDepartments: true

Run Local Tests

# Build the connector
npm run build

# Test health check
tiger connector dev test-config.yaml --health-check

# Test sync
tiger connector dev test-config.yaml --sync

# Test with webhook event
tiger connector dev test-config.yaml --webhook \
  --event employee.created \
  --payload '{"id":"123","email":"[email protected]",...}'

# Run in watch mode (auto-reload on changes)
tiger connector dev test-config.yaml --watch

Create Unit Tests

// src/__tests__/connector.test.ts
import { HRSystemConnector } from '../index';

describe('HRSystemConnector', () => {
  let connector: HRSystemConnector;

  beforeEach(() => {
    connector = new HRSystemConnector({
      apiUrl: 'https://hr-system.test',
      apiKey: 'test-key',
      syncDepartments: true,
    });
  });

  test('metadata is defined', () => {
    const metadata = connector.getMetadata();
    expect(metadata.name).toBe('hr-system');
    expect(metadata.version).toBe('1.0.0');
  });

  test('health check succeeds with valid config', async () => {
    // Mock fetch
    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ status: 'ok' }),
      })
    ) as jest.Mock;

    const result = await connector.healthCheck();
    expect(result.healthy).toBe(true);
  });

  test('sync returns identities', async () => {
    // Mock API responses
    const mockEmployees = [
      {
        id: '1',
        email: '[email protected]',
        firstName: 'John',
        lastName: 'Doe',
        status: 'active',
      },
    ];

    global.fetch = jest.fn(() =>
      Promise.resolve({
        ok: true,
        json: () => Promise.resolve({ employees: mockEmployees }),
      })
    ) as jest.Mock;

    const result = await connector.onSync();
    expect(result.success).toBe(true);
    expect(result.identities).toHaveLength(1);
    expect(result.identities[0].email).toBe('[email protected]');
  });
});

Publishing to Connector Registry

Once your connector is tested and ready, publish it to the TigerIdentity connector registry.

Step 1: Create connector.json

{
  "name": "@your-org/tigeridentity-connector-hr-system",
  "version": "1.0.0",
  "description": "TigerIdentity connector for internal HR system",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",

  "tigeridentity": {
    "connectorType": "hr-system",
    "displayName": "Internal HR System",
    "category": "hr",
    "icon": "https://example.com/icon.png",
    "documentation": "https://docs.example.com/connector"
  },

  "keywords": [
    "tigeridentity",
    "connector",
    "hr",
    "identity"
  ],

  "author": "Your Company",
  "license": "MIT",

  "peerDependencies": {
    "@tigeridentity/connector-sdk": "^1.0.0"
  }
}

Step 2: Validate and Publish

# Validate connector package
tiger connector validate

# Build for production
npm run build

# Publish to connector registry
tiger connector publish

# Output:
# ✓ Connector validated successfully
# ✓ Package built
# ✓ Published to registry: [email protected]
#
# Your connector is now available at:
# https://connectors.tigeridentity.com/hr-system

Step 3: Install and Use

Others can now install and use your connector:

# Install the connector
tiger connector install hr-system

# Create configuration
cat > hr-connector.yaml << EOF
name: hr-production
type: hr-system
enabled: true

config:
  apiUrl: https://hr.example.com/api
  apiKey: ${HR_API_KEY}
  syncDepartments: true
EOF

# Deploy the connector
tiger connector create -f hr-connector.yaml

Complete Working Example

Here's a complete example project structure for a custom connector:

my-custom-connector/
├── src/
│   ├── index.ts              # Main connector implementation
│   ├── types.ts              # Type definitions
│   ├── client.ts             # API client
│   ├── transforms.ts         # Data transformations
│   └── __tests__/
│       ├── connector.test.ts # Connector tests
│       └── transforms.test.ts # Transform tests
├── dist/                     # Build output
├── package.json              # Package configuration
├── tsconfig.json             # TypeScript configuration
├── connector.yaml            # Local test config
├── README.md                 # Documentation
└── LICENSE                   # License file

Best Practices

  • Error handling: Always handle API errors gracefully and provide meaningful error messages
  • Rate limiting: Respect source system rate limits and implement backoff strategies
  • Incremental sync: Support incremental syncs to minimize API calls and improve performance
  • Logging: Use structured logging to help with debugging and monitoring
  • Testing: Write comprehensive tests including unit tests and integration tests
  • Documentation: Provide clear documentation on configuration, setup, and troubleshooting

Related Documentation

Ready to Build Your Connector?

Join our developer community and start building custom connectors today.