Security

Replyke Documentation

Overview

Replyke is designed to simplify the development and implementation of social interaction features by offloading much of the backend work. This allows developers to focus on the frontend experience and reduces complexity. Unlike traditional architectures where all interactions pass through a central server, Replyke communicates directly with the client. You can regard Replyke as an oppnionated server-as-a-service.

This approach reduces complexity for developers but introduces unique challenges in ensuring that data remains valid across various custom use cases—especially when dealing with free-form metadata and content validation.

Replyke addresses many common validation needs out of the box, such as enforcing data ownership, managing user authorization, and implementing logical constraints like limiting users to a single vote per entity/comment. However, for application-specific validation—particularly for custom metadata or complex entity rules—developers need a way to extend Replyke’s built-in capabilities, so they can verify their own unique data structures.

To solve this, Replyke leverages a webhook-based system. Developers can define custom validation logic by exposing a webhook on their server, which will be triggered during certain events that migh require extra validation. Those events are: Entity creation or update, and User creation or update. This ensures that all relevant data is validated on the developer’s server before the operation is finalized in Replyke. The webhook response determines whether the operation proceeds or is rejected by Replyke, allowing for fine-grained control over data integrity.

By default, no webhooks are configured for new projects, meaning that no further validation takes place beyond Replyke’s built-in validation. While this might be sufficient for development, exposing a webhook is highly recommended for applications in production that require enhanced security and data validation to maintain the integrity of their data.

Webhooks for Validation

In Replyke’s dashboard developers can set up their webhook under settings. A single webhook is enough, and each payload includes a “type” field to help developers know which event the payload is associated with, and how to handle it. A single shared secret from the Replyke dashboard is sufficient for all webhooks. However, it is recommended to periodically rotate the secret for enhanced security.

For reference, the setup for validation webhooks is similar to the notifications webhook integration described in App Notifications Webhook Integration, with the added requirement of sending a signed response.

Falure to sd a respons would lead to Replyke rejecting the operation entirely.

Validation Webhook Endpoints

  1. User Created This webhook validates user details before a user is created. The payload includes the following fields:

    FieldDescription
    projectIdAlways included
    dataObject containing the user details:
    - foreignId: If integrating Replyke with an external user system, a foreign ID will be provided.
    - role: Always “visitor” for new users
    - email: Optional, user’s email address
    - name: Optional, user’s name
    - username: Optional, username
    - avatar: Optional, URL to the avatar image
    - bio: Optional, user biography
    - location: Optional, user’s location
    - birthdate: Optional, user’s birthdate
    - metadata: Optional, free-form metadata
    - secureMetadata: Optional, sensitive data
  2. User Updated This webhook validates user details before a user is updated. The payload structure is similar to the one used for user creation but includes only the updated fields within the data object.

  3. Entity Created This webhook validates entity details before an entity is created. The payload includes the following fields:

    FieldDescription
    projectIdAlways included
    dataObject containing entity details:
    - foreignId: If entity data is attached to a resource from an external data set, a foreign ID will be provided.
    - sourceId: Entities could be grouped by source ID to separate entity lists.
    - userId: Optional, creator’s ID
    - title: Optional, title of the entity
    - content: Optional, content of the entity
    - attachments: Optional, file attachments
    - mentions: Optional, array of user mentions
    - keywords: Optional, keywords for the entity
    - location: Optional, geographic location
    - metadata: Optional, free-form metadata
    initiatorIdOptional, ID of the user initiating the action. Some apps don’t associate entties with the users who created them, which requires userId to remain null. For suce projects, this field is useful in the validation stage.
  4. Entity Updated This webhook validates entity details before an entity is updated. The payload structure is similar to the one used for entity creation but includes only the updated fields within the data object.

HMAC Signature and Security

To ensure secure communication between Replyke and your server, each webhook request includes an HMAC signature. This signature verifies the authenticity of the request and prevents tampering. The HMAC signature is calculated using the shared secret and the payload.

Supporting Functions

Here are supporting functions for HMAC validation and response signing:

import crypto from "crypto";
import { Request, Response } from "express";
 
/**
 * Validate HMAC signature of an incoming request.
 */
export function validateIncomingHmac(req: Request, secret: string): void {
  const signature = req.headers["x-signature"] as string;
  const timestamp = req.headers["x-timestamp"] as string;
 
  if (!signature || !timestamp) {
    throw new Error("Missing HMAC signature or timestamp in headers");
  }
 
  // Reject requests older than 5 minutes to prevent replay attacks
  if (Date.now() - parseInt(timestamp, 10) > 5 * 60 * 1000) {
    throw new Error("Request timestamp expired");
  }
 
  // Compute the expected signature
  const payload = JSON.stringify(req.body);
  const expectedSignature = crypto
    .createHmac("sha256", secret)
    .update(`${timestamp}.${payload}`)
    .digest("hex");
 
  if (signature !== expectedSignature) {
    throw new Error("Invalid HMAC signature");
  }
}
 
/**
 * Generate an HMAC signature for the response payload.
 */
export function signResponsePayload(payload: any, secret: string): string {
  return crypto
    .createHmac("sha256", secret)
    .update(JSON.stringify(payload))
    .digest("hex");
}
 
/**
 * Handles unexpected errors, logs them, signs the error response, and sends it.
 */
export function handleError(
  res: Response,
  error: any,
  sharedSecret: string
): void {
  console.error("Error processing request:", error.message || error);
 
  const responsePayload = { valid: false, message: "Server error" };
  const responseSignature = signResponsePayload(responsePayload, sharedSecret);
 
  res
    .status(500)
    .header("X-Response-Signature", responseSignature)
    .send(responsePayload);
}

Example Implementation

import { Request as ExReq, Response as ExRes } from "express";
import {
  validateIncomingHmac,
  signResponsePayload,
  handleError,
} from "./utility-functions";
 
export default async (req: ExReq, res: ExRes) => {
  const sharedSecret = process.env.REPLYKE_WEBHOOKS_SECRET;
 
  try {
    const { projectId, type, data } = req.body;
 
    // Step 1: Validate the incoming HMAC signature
    validateIncomingHmac(req, sharedSecret!);
 
    // Step 2: Add your event-specific validation logic here
    switch (type) {
      case "user.created":
        console.log("Project ID:", projectId, "User data:", data);
        break;
 
      case "user.updated":
        console.log("Project ID:", projectId, "User update data:", data);
        break;
 
      case "entity.created":
        console.log("Project ID:", projectId, "Entity data:", data);
        break;
 
      case "entity.updated":
        console.log("Project ID:", projectId, "Entity update data:", data);
        break;
 
      case "notification.created":
        console.log("Project ID:", projectId, "Notification data:", data);
        break;
    }
 
    // Step 3: Send a valid response if the data is valid, or false if it is not valid. Include your secret
    const responsePayload = { valid: true };
    const responseSignature = signResponsePayload(
      responsePayload,
      sharedSecret!
    );
 
    res
      .status(200)
      .header("X-Response-Signature", responseSignature)
      .send(responsePayload);
  } catch (err) {
    handleError(res, err, sharedSecret!);
  }
};

This example demonstrate complete webhook implementations and supporting functions, ensuring secure and accurate handling of data. Developers using other languages should replicate the HMAC signature logic to maintain compatibility.