Skip to content

Event Subscriptions ​

Plugin Play allows your plugin to subscribe to platform events and react to changes in real-time. This is one of the most powerful features for building integrations.

Overview ​

When you subscribe to an event, Plugin Play:

  1. Registers a webhook endpoint for that event
  2. Adds the subscription to your plugin manifest
  3. Handles authentication and request validation
  4. Calls your handler when the event occurs

Subscribing to Events ​

Basic Event Subscription ​

Use registerToHook to subscribe to events:

typescript
import { registerToHook } from '@qelos/plugin-play';

registerToHook({
  source: 'auth',
  kind: 'users',
  eventName: 'user-created'
}, async (request, reply) => {
  const { user } = request.body;
  
  console.log('New user created:', user);
  
  // Your logic here
  await sendWelcomeEmail(user.email);
  
  return { received: true };
});

Event with Source Filter ​

Subscribe to events from a specific source:

typescript
registerToHook({
  source: 'auth-service',
  eventName: 'user.created'
}, async (request, reply) => {
  // Only receives events from auth-service
  const { user } = request.body;
  return { received: true };
});

Event with Kind Filter ​

Subscribe to events of a specific kind:

typescript
registerToHook({
  kind: 'user',
  eventName: 'created'
}, async (request, reply) => {
  // Receives all 'created' events for 'user' kind
  const { user } = request.body;
  return { received: true };
});

Wildcard Subscriptions ​

Use wildcards to subscribe to multiple events:

typescript
// Subscribe to all user events
registerToHook({
  kind: 'user',
  eventName: '*'
}, async (request, reply) => {
  const { eventName, data } = request.body;
  console.log(`User event: ${eventName}`, data);
  return { received: true };
});

// Subscribe to all events from a source
registerToHook({
  source: 'auth-service',
  eventName: '*'
}, async (request, reply) => {
  const { eventName, data } = request.body;
  console.log(`Auth service event: ${eventName}`, data);
  return { received: true };
});

// Subscribe to all events
registerToHook({
  source: '*',
  kind: '*',
  eventName: '*'
}, async (request, reply) => {
  console.log('Received event:', request.body);
  return { received: true };
});

Event Handler ​

Request Object ​

The request object contains:

typescript
interface EventRequest {
  body: {
    eventName: string;
    source: string;
    kind: string;
    tenant: string;
    workspace?: string;
    user?: string;
    timestamp: string;
    data: any;
    metadata?: any;
  };
  headers: {
    authorization: string;
    'x-tenant': string;
    'x-workspace'?: string;
    'x-user'?: string;
  };
  tenant: string;  // Automatically parsed
  workspace?: string;
  user?: string;
}

Handler Example ​

typescript
registerToHook({
  eventName: 'order.created'
}, async (request, reply) => {
  const { 
    eventName, 
    data, 
    tenant, 
    workspace, 
    user 
  } = request.body;
  
  console.log('Event:', eventName);
  console.log('Tenant:', tenant);
  console.log('Workspace:', workspace);
  console.log('User:', user);
  console.log('Order data:', data);
  
  // Process the order
  await processOrder(data);
  
  // Return success
  return { 
    received: true,
    processed: true,
    orderId: data._id
  };
});

Platform Event Types ​

Qelos dispatches various platform events that your plugin can subscribe to:

Authentication Events ​

typescript
// User registered (signup)
registerToHook({
  source: 'auth',
  kind: 'signup',
  eventName: 'user-registered'
}, async (request, reply) => {
  const { user } = request.body;
  await onUserRegistered(user);
  return { received: true };
});

// User created
registerToHook({
  source: 'auth',
  kind: 'users',
  eventName: 'user-created'
}, async (request, reply) => {
  const { user } = request.body;
  await onUserCreated(user);
  return { received: true };
});

// User updated
registerToHook({
  source: 'auth',
  kind: 'users',
  eventName: 'user-updated'
}, async (request, reply) => {
  const { user, changes } = request.body;
  await onUserUpdated(user, changes);
  return { received: true };
});

// User removed
registerToHook({
  source: 'auth',
  kind: 'users',
  eventName: 'user-removed'
}, async (request, reply) => {
  const { userId } = request.body;
  await onUserRemoved(userId);
  return { received: true };
});

Social Login Failures ​

typescript
// Failed LinkedIn login
registerToHook({
  source: 'auth',
  kind: 'failed-social-login',
  eventName: 'failed-linkedin-login'
}, async (request, reply) => {
  const { error, user } = request.body;
  await logFailedLogin('linkedin', error);
  return { received: true };
});

// Subscribe to all failed social logins
registerToHook({
  source: 'auth',
  kind: 'failed-social-login',
  eventName: '*'
}, async (request, reply) => {
  const { eventName, error } = request.body;
  console.log(`Failed social login: ${eventName}`, error);
  return { received: true };
});

Asset Events ​

typescript
// Asset uploaded
registerToHook({
  source: 'assets',
  kind: 'asset-operation',
  eventName: 'asset-uploaded'
}, async (request, reply) => {
  const { asset } = request.body;
  await processUploadedAsset(asset);
  return { received: true };
});

// Storage connection errors
registerToHook({
  source: 'assets',
  kind: 'storage-connection-error',
  eventName: '*'
}, async (request, reply) => {
  const { eventName, error } = request.body;
  console.error(`Storage error: ${eventName}`, error);
  await notifyAdmin(eventName, error);
  return { received: true };
});

Complete Event List ​

SourceKindEvent NameDescription
authsignupuser-registeredNew user signed up
authusersuser-createdUser created by admin
authusersuser-updatedUser information updated
authusersuser-removedUser deleted
authfailed-social-loginfailed-linkedin-loginLinkedIn login failed
authfailed-social-loginfailed-facebook-loginFacebook login failed
authfailed-social-loginfailed-google-loginGoogle login failed
authfailed-social-loginfailed-github-loginGithub login failed
assetsasset-operationasset-uploadedFile/asset uploaded
assetsstorage-connection-errors3-connection-errorS3 connection failed
assetsstorage-connection-errorgcs-connection-errorGoogle Cloud Storage failed
assetsstorage-connection-errorftp-connection-errorFTP connection failed
assetsstorage-connection-errorcloudinary-connection-errorCloudinary connection failed
authinvitesinvite-respondedUser responded to an invite
authinvitesinvite-createdNew invite created
authworkspacesworkspace-createdNew workspace created
authworkspacesworkspace-deletedWorkspace deleted
aithreadscreateAI thread created
aithreadsdeleteAI thread deleted

Blueprint Events ​

Blueprint events are dynamic and correspond to the identifier of a blueprint.

SourceKindEvent NameDescription
blueprintscreateA new entity has been created
blueprintsupdateAn entity has been updated
blueprintsdeleteAn entity has been deleted

Custom Plugin Events ​

Your plugin can dispatch custom events that other plugins can subscribe to. Custom plugin events are automatically prefixed with plugin: in the source field.

Dispatching Custom Events ​

typescript
import { getSdk } from '@qelos/plugin-play';

// Dispatch a custom event
await getSdk().events.dispatch({
  user: '111',
  source: 'my-plugin',
  kind: 'my-type',
  eventName: 'thing-changed',
  metadata: {
    customData: 1
  }
});

Subscribing to Custom Plugin Events ​

When subscribing to custom plugin events, use the plugin: prefix in the source:

typescript
import { registerToHook } from '@qelos/plugin-play';

registerToHook({
  source: 'plugin:my-plugin',
  kind: '*',
  eventName: '*'
}, async (request, reply) => {
  const { user, source, kind, eventName, metadata } = request.body;
  
  if (user === '111' && metadata.customData === 1 && eventName === 'thing-changed') {
    console.log('Custom event received!');
  }
  
  return { received: true };
});

Custom Event Example ​

Here's a complete example of a plugin that dispatches and listens to custom events:

typescript
import { registerToHook, getSdk } from '@qelos/plugin-play';

// Subscribe to custom events from another plugin
registerToHook({
  source: 'plugin:order-service',
  kind: 'order',
  eventName: 'order-completed'
}, async (request, reply) => {
  const { order } = request.body;
  
  // Process the completed order
  await sendInvoice(order);
  
  // Dispatch our own event
  await getSdk().events.dispatch({
    source: 'invoice-service',
    kind: 'invoice',
    eventName: 'invoice-sent',
    metadata: {
      orderId: order._id,
      invoiceId: 'INV-123'
    }
  });
  
  return { received: true };
});

Error Handling ​

Handling Errors ​

typescript
registerToHook({
  eventName: 'order.created'
}, async (request, reply) => {
  try {
    const { data } = request.body;
    await processOrder(data);
    return { received: true, success: true };
  } catch (error) {
    console.error('Error processing order:', error);
    
    // Return error response
    reply.code(500);
    return { 
      received: true, 
      success: false,
      error: error.message 
    };
  }
});

Using ResponseError ​

typescript
import { registerToHook, ResponseError } from '@qelos/plugin-play';

registerToHook({
  eventName: 'payment.process'
}, async (request, reply) => {
  const { amount, currency } = request.body.data;
  
  if (amount <= 0) {
    throw new ResponseError('Invalid amount', 400);
  }
  
  if (!['USD', 'EUR', 'GBP'].includes(currency)) {
    throw new ResponseError('Unsupported currency', 400);
  }
  
  await processPayment(amount, currency);
  return { received: true, success: true };
});

Async Processing ​

Background Jobs ​

For long-running tasks, acknowledge the event immediately and process asynchronously:

typescript
registerToHook({
  eventName: 'report.generate'
}, async (request, reply) => {
  const { reportId, params } = request.body.data;
  
  // Acknowledge immediately
  reply.send({ received: true, processing: true });
  
  // Process in background
  setImmediate(async () => {
    try {
      await generateReport(reportId, params);
      console.log('Report generated:', reportId);
    } catch (error) {
      console.error('Report generation failed:', error);
    }
  });
});

Using Queue Systems ​

For production, use a queue system:

typescript
import { Queue } from 'bull';

const orderQueue = new Queue('orders', process.env.REDIS_URL);

registerToHook({
  eventName: 'order.created'
}, async (request, reply) => {
  const { data } = request.body;
  
  // Add to queue
  await orderQueue.add('process', data);
  
  return { 
    received: true, 
    queued: true 
  };
});

// Process queue
orderQueue.process('process', async (job) => {
  await processOrder(job.data);
});

SDK Integration ​

Using SDK in Event Handlers ​

Access the Qelos SDK to make API calls:

typescript
import { registerToHook, getSdkForTenant } from '@qelos/plugin-play';

registerToHook({
  eventName: 'user.created'
}, async (request, reply) => {
  const { user, tenant } = request.body;
  
  // Get SDK for this tenant
  const sdk = await getSdkForTenant(tenant);
  
  // Create a welcome block
  await sdk.blocks.create({
    name: `Welcome ${user.fullName}`,
    content: `Welcome to our platform, ${user.firstName}!`,
    contentType: 'html'
  });
  
  return { received: true };
});

Testing Event Handlers ​

Manual Testing ​

Test your event handler with curl:

bash
curl -X POST http://localhost:3000/api/hooks/<hook-path> \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer <access-token>" \
  -d '{
    "eventName": "user.created",
    "source": "auth-service",
    "kind": "user",
    "tenant": "tenant-id",
    "timestamp": "2025-01-01T00:00:00Z",
    "data": {
      "user": {
        "_id": "user-123",
        "email": "test@example.com",
        "fullName": "Test User"
      }
    }
  }'

Unit Testing ​

typescript
import { test } from 'node:test';
import { getApp } from '@qelos/plugin-play';

test('user.created event handler', async (t) => {
  const app = getApp();
  
  const response = await app.inject({
    method: 'POST',
    url: '/api/hooks/user-created',
    headers: {
      'content-type': 'application/json',
      'authorization': 'Bearer test-token'
    },
    payload: {
      eventName: 'user.created',
      data: {
        user: {
          _id: 'user-123',
          email: 'test@example.com'
        }
      }
    }
  });
  
  assert.strictEqual(response.statusCode, 200);
  const body = JSON.parse(response.body);
  assert.strictEqual(body.received, true);
});

Best Practices ​

  1. Acknowledge Quickly: Return a response quickly to avoid timeouts
  2. Process Asynchronously: Use background jobs for long-running tasks
  3. Handle Errors Gracefully: Always catch and log errors
  4. Validate Data: Validate event data before processing
  5. Use Idempotency: Handle duplicate events gracefully
  6. Log Events: Log all events for debugging and auditing
  7. Monitor Performance: Track event processing times
  8. Use Specific Subscriptions: Subscribe to specific events rather than wildcards when possible

Complete Example ​

typescript
import { 
  registerToHook, 
  getSdkForTenant,
  ResponseError 
} from '@qelos/plugin-play';

// Subscribe to order creation
registerToHook({
  eventName: 'order.created'
}, async (request, reply) => {
  const { data, tenant } = request.body;
  const { order } = data;
  
  try {
    // Validate order
    if (!order.items || order.items.length === 0) {
      throw new ResponseError('Order has no items', 400);
    }
    
    // Get SDK for tenant
    const sdk = await getSdkForTenant(tenant);
    
    // Create order record in blueprint
    const orderEntity = await sdk.blueprints
      .entitiesOf('order')
      .create({
        identifier: order._id,
        metadata: {
          total: order.total,
          status: 'pending',
          items: order.items
        }
      });
    
    // Send confirmation email
    await sendOrderConfirmation(order);
    
    // Log success
    console.log('Order processed:', order._id);
    
    return {
      received: true,
      success: true,
      orderId: orderEntity.identifier
    };
    
  } catch (error) {
    console.error('Error processing order:', error);
    
    if (error instanceof ResponseError) {
      throw error;
    }
    
    reply.code(500);
    return {
      received: true,
      success: false,
      error: error.message
    };
  }
});

Next Steps ​

Build SaaS Products Without Limits.