Skip to content

Backend Registration

Backend services integrate with the Manager's trigger system by registering callback URLs to receive trigger execution notifications.

Overview

Backend registration enables:

  • Trigger Callbacks - Receive POST requests when triggers execute
  • Application Context - Get tenant and application information with each trigger
  • Service Discovery - Manager maintains registry of active backends
  • Automatic Routing - Callbacks delivered to correct backend services
  • Multi-Tenant Support - Handle triggers for multiple tenants

Prerequisites

Before registering a backend, you need:

  1. Machine User - Service account for authentication
  2. Callback Endpoint - HTTPS endpoint to receive trigger notifications
  3. Network Access - Manager must be able to reach your callback URL

See Machine Users for creating service accounts.

Registration Process

1. Create Machine User

First, create a machine user for your backend service:

graphql
mutation {
  createMachineUserWithCredentials(
    tenantId: "tenant-uuid"
    input: {
      name: "Payment Processing Service"
      username: "payment-service"
      hashedKey: "__GENERATE_TOKEN__"
      enabled: true
    }
  ) {
    generatedToken
    machineUser {
      id
      username
    }
  }
}

TIP

Save the generatedToken immediately - it's only shown once.

2. Register Backend

Use the machine user token to register your backend:

bash
curl -X POST https://manager.example.com/api/machine/register-backend \
  -H "Authorization: Bearer <machine-user-token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Payment Processing Service",
    "callbackUrl": "https://payment.example.com/triggers/callback",
    "description": "Handles payment-related trigger executions"
  }'

Response:

json
{
  "id": "backend-uuid",
  "name": "Payment Processing Service",
  "callbackUrl": "https://payment.example.com/triggers/callback",
  "machineUserId": "machine-user-uuid",
  "createdAt": "2025-12-01T10:00:00Z"
}

3. Implement Callback Handler

Create an endpoint at your callback URL to receive trigger notifications:

javascript
// Node.js/Express example
app.post("/triggers/callback", async (req, res) => {
  const {
    triggerId,
    triggerName,
    cronExpression,
    runKey,
    executionTime,
    application,
    tenant,
    project,
  } = req.body;

  console.log(`Trigger ${triggerName} fired for tenant ${tenant.slug}`);

  try {
    // Execute your business logic
    await processPayments(tenant.id, application.id);

    // Respond with success
    res.json({
      status: "success",
      message: "Payment processing completed",
      processedAt: new Date().toISOString(),
    });
  } catch (error) {
    console.error("Trigger execution failed:", error);

    // Respond with error
    res.status(500).json({
      status: "error",
      message: error.message,
    });
  }
});

Callback Payload

When a trigger executes, the Manager sends this payload to your callback URL:

json
{
  "triggerId": "trigger-uuid",
  "triggerName": "Hourly Payment Processing",
  "cronExpression": "0 * * * *",
  "runKey": "hourly-payment",
  "executionTime": "2025-12-01T10:00:00Z",
  "application": {
    "id": "app-uuid",
    "name": "Payment App",
    "slug": "payment-app"
  },
  "tenant": {
    "id": "tenant-uuid",
    "name": "Customer A",
    "slug": "customer-a"
  },
  "project": {
    "id": "project-uuid",
    "name": "E-commerce Platform",
    "slug": "ecommerce"
  }
}

Payload Fields

  • triggerId - UUID of the trigger that fired
  • triggerName - Human-readable trigger name
  • cronExpression - Cron expression defining the schedule
  • runKey - Unique identifier for backend routing
  • executionTime - ISO 8601 timestamp of execution

Expected Response

Your callback should respond with a JSON payload:

Success Response:

json
{
  "status": "success",
  "message": "Processing completed",
  "processedAt": "2025-12-01T10:00:15Z",
  "results": {
    "itemsProcessed": 42
  }
}

Error Response:

json
{
  "status": "error",
  "message": "Database connection failed",
  "errorCode": "DB_ERROR"
}

Response Time

Respond quickly to callback requests (< 5 seconds recommended). Process heavy workloads asynchronously.

Implementation Examples

Node.js with Express

javascript
const express = require("express");
const app = express();

app.use(express.json());

// Trigger callback handler
app.post("/triggers/callback", async (req, res) => {
  const { triggerId, triggerName, tenant, application } = req.body;

  // Log trigger execution
  console.log(`[${new Date().toISOString()}] Trigger: ${triggerName}`);
  console.log(`  Tenant: ${tenant.name} (${tenant.id})`);
  console.log(`  Application: ${application.name} (${application.id})`);

  try {
    // Process trigger
    const result = await handleTrigger(triggerId, tenant.id, application.id);

    res.json({
      status: "success",
      message: "Trigger processed successfully",
      result,
    });
  } catch (error) {
    console.error("Trigger processing error:", error);
    res.status(500).json({
      status: "error",
      message: error.message,
    });
  }
});

async function handleTrigger(triggerId, tenantId, applicationId) {
  // Your business logic here
  return { processed: true };
}

app.listen(3000, () => {
  console.log("Backend listening on port 3000");
});

Python with Flask

python
from flask import Flask, request, jsonify
import logging

app = Flask(__name__)
logging.basicConfig(level=logging.INFO)

@app.route('/triggers/callback', methods=['POST'])
def trigger_callback():
    data = request.json
    trigger_id = data['triggerId']
    trigger_name = data['triggerName']
    tenant = data['tenant']
    application = data['application']

    logging.info(f"Trigger: {trigger_name}")
    logging.info(f"  Tenant: {tenant['name']} ({tenant['id']})")
    logging.info(f"  Application: {application['name']} ({application['id']})")

    try:
        # Process trigger
        result = handle_trigger(trigger_id, tenant['id'], application['id'])

        return jsonify({
            'status': 'success',
            'message': 'Trigger processed successfully',
            'result': result
        })
    except Exception as e:
        logging.error(f'Trigger processing error: {e}')
        return jsonify({
            'status': 'error',
            'message': str(e)
        }), 500

def handle_trigger(trigger_id, tenant_id, application_id):
    # Your business logic here
    return {'processed': True}

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000)

Go with Gin

go
package main

import (
    "log"
    "net/http"
    "github.com/gin-gonic/gin"
)

type TriggerPayload struct {
    TriggerID     string      `json:"triggerId"`
    TriggerName   string      `json:"triggerName"`
    CronExpression string     `json:"cronExpression"`
    RunKey        string      `json:"runKey"`
    ExecutionTime string      `json:"executionTime"`
    Application   Application `json:"application"`
    Tenant        Tenant      `json:"tenant"`
    Project       Project     `json:"project"`
}

type Application struct {
    ID   string `json:"id"`
    Name string `json:"name"`
    Slug string `json:"slug"`
}

type Tenant struct {
    ID   string `json:"id"`
    Name string `json:"name"`
    Slug string `json:"slug"`
}

type Project struct {
    ID   string `json:"id"`
    Name string `json:"name"`
    Slug string `json:"slug"`
}

func triggerCallback(c *gin.Context) {
    var payload TriggerPayload

    if err := c.BindJSON(&payload); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "status":  "error",
            "message": "Invalid payload",
        })
        return
    }

    log.Printf("Trigger: %s", payload.TriggerName)
    log.Printf("  Tenant: %s (%s)", payload.Tenant.Name, payload.Tenant.ID)
    log.Printf("  Application: %s (%s)", payload.Application.Name, payload.Application.ID)

    // Process trigger
    if err := handleTrigger(payload); err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "status":  "error",
            "message": err.Error(),
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "status":  "success",
        "message": "Trigger processed successfully",
    })
}

func handleTrigger(payload TriggerPayload) error {
    // Your business logic here
    return nil
}

func main() {
    r := gin.Default()
    r.POST("/triggers/callback", triggerCallback)
    r.Run(":3000")
}

Best Practices

Idempotency

Implement idempotent trigger handlers to handle duplicate deliveries:

javascript
const processedTriggers = new Set();

app.post("/triggers/callback", async (req, res) => {
  const { triggerId, executionTime } = req.body;

  // Create unique key for this execution
  const executionKey = `${triggerId}-${executionTime}`;

  // Check if already processed
  if (processedTriggers.has(executionKey)) {
    return res.json({
      status: "success",
      message: "Already processed",
    });
  }

  try {
    await processTrigger(req.body);
    processedTriggers.add(executionKey);

    res.json({ status: "success" });
  } catch (error) {
    res.status(500).json({ status: "error", message: error.message });
  }
});

Asynchronous Processing

Respond quickly and process heavy workloads asynchronously:

javascript
const queue = require("./queue");

app.post("/triggers/callback", async (req, res) => {
  // Immediately queue for processing
  await queue.add("trigger-execution", req.body);

  // Respond quickly
  res.json({
    status: "success",
    message: "Queued for processing",
  });
});

// Process queue asynchronously
queue.process("trigger-execution", async (job) => {
  const { triggerId, tenant, application } = job.data;
  await heavyProcessing(tenant.id, application.id);
});

Error Handling

Implement comprehensive error handling:

javascript
app.post('/triggers/callback', async (req, res) => {
  try {
    // Validate payload
    if (!req.body.triggerId || !req.body.tenant) {
      throw new Error('Invalid payload structure');
    }

    // Process trigger
    const result = await processTrigger(req.body);

    res.json({ status: 'success', result });

  } catch (error) {
    // Log error
    console.error('Trigger processing failed:', error);

    // Respond with appropriate status
    const status Code = error.code === 'VALIDATION_ERROR' ? 400 : 500;

    res.status(statusCode).json({
      status: 'error',
      message: error.message,
      errorCode: error.code
    });
  }
});

Security

Verify callbacks come from Manager (optional but recommended):

javascript
const crypto = require("crypto");

function verifyCallbackSignature(req) {
  const signature = req.headers["x-productify-signature"];
  const payload = JSON.stringify(req.body);

  const expectedSignature = crypto
    .createHmac("sha256", process.env.WEBHOOK_SECRET)
    .update(payload)
    .digest("hex");

  return signature === expectedSignature;
}

app.post("/triggers/callback", async (req, res) => {
  // Verify signature
  if (!verifyCallbackSignature(req)) {
    return res.status(401).json({
      status: "error",
      message: "Invalid signature",
    });
  }

  // Process trigger
  // ...
});

Monitoring & Logging

Logging Best Practices

javascript
const winston = require("winston");

const logger = winston.createLogger({
  level: "info",
  format: winston.format.json(),
  transports: [new winston.transports.File({ filename: "triggers.log" })],
});

app.post("/triggers/callback", async (req, res) => {
  const { triggerId, triggerName, tenant } = req.body;

  logger.info("Trigger received", {
    triggerId,
    triggerName,
    tenantId: tenant.id,
    tenantName: tenant.name,
    timestamp: new Date().toISOString(),
  });

  try {
    await processTrigger(req.body);

    logger.info("Trigger processed successfully", {
      triggerId,
      duration: Date.now() - startTime,
    });

    res.json({ status: "success" });
  } catch (error) {
    logger.error("Trigger processing failed", {
      triggerId,
      error: error.message,
      stack: error.stack,
    });

    res.status(500).json({ status: "error", message: error.message });
  }
});

Metrics Collection

javascript
const prometheus = require("prom-client");

const triggerCounter = new prometheus.Counter({
  name: "triggers_processed_total",
  help: "Total triggers processed",
  labelNames: ["status", "trigger_name"],
});

const triggerDuration = new prometheus.Histogram({
  name: "trigger_processing_duration_seconds",
  help: "Trigger processing duration",
});

app.post("/triggers/callback", async (req, res) => {
  const end = triggerDuration.startTimer();

  try {
    await processTrigger(req.body);

    triggerCounter.inc({
      status: "success",
      trigger_name: req.body.triggerName,
    });
    res.json({ status: "success" });
  } catch (error) {
    triggerCounter.inc({ status: "error", trigger_name: req.body.triggerName });
    res.status(500).json({ status: "error", message: error.message });
  } finally {
    end();
  }
});

Troubleshooting

Callbacks Not Received

Check:

  • Backend is registered via /api/machine/register-backend
  • Callback URL is correct and accessible from Manager
  • Firewall allows inbound connections on callback port
  • HTTPS certificate is valid (if using HTTPS)
  • Backend service is running and healthy

Registration Fails

Verify:

  • Machine user token is valid and not expired
  • Token has correct format (Bearer <token>)
  • Callback URL is a valid HTTPS URL
  • Network connectivity from your service to Manager

Trigger Executes But Callback Fails

Debug:

  • Check backend logs for errors
  • Verify payload structure matches expected format
  • Ensure quick response time (< 5 seconds)
  • Test callback endpoint independently
  • Check for rate limiting or resource exhaustion

See Also