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:
- Machine User - Service account for authentication
- Callback Endpoint - HTTPS endpoint to receive trigger notifications
- 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:
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:
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:
{
"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:
// 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:
{
"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:
{
"status": "success",
"message": "Processing completed",
"processedAt": "2025-12-01T10:00:15Z",
"results": {
"itemsProcessed": 42
}
}Error Response:
{
"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
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
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
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:
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:
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:
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):
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
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
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
- Trigger System - Comprehensive trigger documentation
- Machine Users - Service account authentication
- REST API - REST endpoint documentation
- API Reference - Complete API reference