A high-performance webhook receiver service for Jira integrations with configurable event processing pipelines and MongoDB sink support, written in Rust.
- Jira webhook integration with HMAC-SHA256 signature validation
- Support for 15+ Jira event types (issues, projects, versions, issue links)
- Secure secret management (environment variables, files, or plain text)
- Event normalization and pipeline processing
- CEL (Common Expression Language) filters for event filtering
- Handlebars template-based event mapping
- MongoDB sink for persisted event storage
- Extensible processor and sink architecture
- Health check endpoints
- Structured logging with tracing
- Docker & Docker Compose support with distroless images
- Quick Start - Get started quickly with Docker or from source
- DOCKER.md - Comprehensive Docker deployment guide
- API Endpoints - Available HTTP endpoints
- Pipeline Processing - Configure filters, mappers, and sinks
- Configuration - Detailed configuration options
- Rust 1.70+
- Cargo
- MongoDB (if using database sink)
- Docker 20.10+
- Docker Compose 2.0+ (optional, for full stack)
Build and run with Docker:
# Build the image
docker build -t connectcare:latest .
# Run with environment variables
docker run -d \
-p 3000:3000 \
-e JIRA_WEBHOOK_SECRET="your-secret-here" \
-e MONGO_URL="your-mongodb-connection-string" \
-e LOG_LEVEL=info \
-v $(pwd)/config/config.json:/app/config/config.json:ro \
connectcare:latestOr use Docker Compose (includes MongoDB):
# Set your secret
export JIRA_WEBHOOK_SECRET="your-secret-here"
# Start all services
docker-compose up -d
# View logs
docker-compose logs -f connectcare
# Stop services
docker-compose downcargo build --release- Copy the example configuration:
cp config/config.example.json config/config.json- Set your Jira webhook secret:
export JIRA_WEBHOOK_SECRET="your-secret-here"- Set MongoDB connection URL:
export MONGO_URL="mongodb://localhost:27017/connectcare/events"- Update
config/config.jsonas needed.
# Set configuration path (optional, defaults to config/config.json)
export CONFIGURATION_PATH=config/config.json
# Set log level (optional)
export LOG_LEVEL=info
# Run the service
cargo run --releaseThe service will start on port 3000 by default (configurable via HTTP_PORT environment variable).
GET /-/healthz- Health check endpointGET /-/ready- Readiness check endpoint
POST /jira/webhook- Receives Jira webhook events (path configurable)
jira:issue_created(Write)jira:issue_updated(Write)jira:issue_deleted(Delete)
issuelink_created(Write)issuelink_deleted(Delete)
project_created(Write)project_updated(Write)project_deleted(Delete)project_soft_deleted(Delete)project_restored_deleted(Write)
jira:version_created(Write)jira:version_updated(Write)jira:version_released(Write)jira:version_unreleased(Write)jira:version_deleted(Delete)
Secrets can be loaded from three sources:
- Environment Variable:
{
"secret": {
"fromEnv": "JIRA_WEBHOOK_SECRET"
}
}- File:
{
"secret": {
"fromFile": "/path/to/secret.txt"
}
}- Plain Text (not recommended for production):
{
"secret": "my-secret"
}{
"integrations": [
{
"source": {
"type": "jira",
"webhookPath": "/jira/webhook",
"authentication": {
"secret": {
"fromEnv": "JIRA_WEBHOOK_SECRET"
},
"headerName": "X-Hub-Signature"
}
},
"pipelines": [
{
"processors": [
{
"type": "filter",
"celExpression": "eventType == 'jira:version_updated'"
},
{
"type": "mapper",
"outputEvent": {
"deploymentName": "{{ version.name }}",
"projectKey": "{{ version.projectId }}",
"status": "{{ version.released }}",
"timestamp": "{{ timestamp }}"
}
}
],
"sinks": [
{
"type": "database",
"provider": "MONGO"
}
]
}
]
}
]
}Events flow through configurable pipelines with processors and sinks:
Webhook → Event Extraction → Processors (Filter, Map) → Sinks (Database)
Uses CEL (Common Expression Language) to filter events. Only events matching the expression pass through.
Available variables:
eventType- The event type (e.g., "jira:issue_created")body- The entire event body as JSON- All top-level fields from the body
Examples:
{
"type": "filter",
"celExpression": "eventType == 'jira:version_updated'"
}{
"type": "filter",
"celExpression": "eventType == 'jira:issue_created' && issue.fields.priority == 'High'"
}Uses Handlebars templates to transform event data into a new structure.
Basic Example:
{
"type": "mapper",
"outputEvent": {
"deploymentName": "{{ version.name }}",
"projectKey": "{{ version.projectId }}",
"status": "{{ version.released }}",
"timestamp": "{{ timestamp }}",
"metadata": {
"source": "jira",
"type": "version_update"
}
}
}Type Preservation:
The mapper automatically preserves JSON types (objects, arrays, numbers, booleans) when using simple variable references:
{
"type": "mapper",
"outputEvent": {
"priority": "{{ issue.fields.priority }}",
"labels": "{{ issue.fields.labels }}",
"assignee": "{{ issue.fields.assignee }}"
}
}If priority is an object in the source, it will remain an object. If labels is an array, it stays an array.
Static/Plain Values:
You can assign static values directly without any template interpolation:
{
"type": "mapper",
"outputEvent": {
"source": "jira",
"version": "1.0",
"priority": 1,
"enabled": true,
"metadata": null,
"tags": ["production", "automated"],
"config": {
"timeout": 30,
"retry": true
},
"issueKey": "{{ issue.key }}"
}
}Static values are passed through as-is with their original types:
- Strings:
"source": "jira"→"jira" - Numbers:
"priority": 1→1 - Booleans:
"enabled": true→true - Null:
"metadata": null→null - Arrays:
"tags": [1, 2]→[1, 2] - Objects:
"config": {...}→{...}
This is useful for:
- Adding metadata or configuration to events
- Setting default values
- Mixing static and dynamic fields in the same mapping
Pass-Through Mapping:
Use {{ @this }} to pass the entire event unchanged:
{
"type": "mapper",
"outputEvent": {
"event": "{{ @this }}"
}
}Type Casting:
You can explicitly cast values between string and number types using the castTo property:
{
"type": "mapper",
"outputEvent": {
"key": "{{ issue.key }}",
"issueId": {
"value": "{{ issue.id }}",
"castTo": "number"
},
"priorityLabel": {
"value": "{{ issue.fields.priority }}",
"castTo": "string"
}
}
}Supported cast types:
string- Converts numbers, booleans to stringnumber- Parses strings as integers or floats (e.g., "123" → 123, "45.67" → 45.67)
This is useful when:
- Jira sends numeric IDs as strings but you want them as numbers in the database
- You need to convert numeric values to strings for specific processing requirements
Writes processed events to MongoDB with upsert support.
Configuration:
{
"type": "database",
"provider": "MONGO"
}Document structure:
_id- Event ID (SHA256 hash of primary keys)_eventType- Original event type- All fields from the mapped event body
Operations:
Writeoperations usereplace_onewith upsertDeleteoperations remove the document by_id
You can configure multiple pipelines per integration to process events differently:
{
"pipelines": [
{
"processors": [
{
"type": "filter",
"celExpression": "eventType == 'jira:issue_created'"
}
],
"sinks": [
{
"type": "database",
"provider": "MONGO"
}
]
},
{
"processors": [
{
"type": "filter",
"celExpression": "eventType == 'jira:version_updated'"
},
{
"type": "mapper",
"outputEvent": {
"version": "{{ version.name }}",
"released": "{{ version.released }}"
}
}
],
"sinks": [
{
"type": "database",
"provider": "MONGO"
}
]
}
]
}make help # Show all available commands
make test # Run unit tests
make e2e # Run E2E tests
make build # Build release binary
make docker # Build Docker image
make all # Run formatting, linting, tests, and buildUnit Tests:
cargo test
# or
make testEnd-to-End Tests:
E2E tests run the full stack (ConnectCare + MongoDB) with Docker Compose and test real webhook scenarios.
# Run E2E tests
./run_e2e_tests.sh
# or
make e2e
# Start E2E environment for manual testing
make e2e-start
# View logs
make e2e-logs
# Stop E2E environment
make e2e-stopWhat E2E tests cover:
- Health check endpoints
- Valid webhook requests with HMAC signatures
- Invalid signatures and missing headers
- Malformed JSON payloads
- Event filtering with CEL expressions
- Event mapping with Handlebars
- MongoDB persistence and data validation
- Delete operations
See tests/e2e/README.md for detailed documentation.
LOG_LEVEL=debug cargo run- HMAC-SHA256 signature validation with constant-time comparison
- Signature format:
sha256=<hex_signature> - Configurable signature header name (default:
X-Hub-Signature)
HTTP Request → HMAC Validation → Event Extraction → Pipeline Processing → Sinks
↓
Filter (CEL)
↓
Mapper (Handlebars)
↓
Sink (MongoDB)
Each event is normalized to:
{
id: String, // SHA256 hash of primary keys
body: Value, // Full JSON payload (or mapped output)
event_type: String, // e.g., "jira:issue_updated"
pk_fields: Vec<...>, // Primary key fields
operation: Write|Delete
}The architecture is designed for extensibility:
- Processors: Implement the
Processortrait to add new processing logic - Sinks: Implement the
Sinktrait to add new destination types - Sources: Add new webhook sources by implementing event extraction
Future sink types could include: HTTP endpoints, Kafka, SQS, etc.
- Multi-stage build for minimal image size
- Distroless base image (
gcr.io/distroless/cc-debian12) for security - Optimized layer caching for faster rebuilds
- No shell or package manager in final image (security hardening)
The docker-compose.yml provides a complete stack:
- connectcare service on port 3000
- mongodb service on port 27017
- Automatic network configuration
- Volume persistence for MongoDB data
- Health checks and restart policies
# Build the image
docker build -t connectcare:latest .
# Build with specific tag
docker build -t myregistry/connectcare:v1.0.0 .# Run with Docker
docker run -d \
--name connectcare \
-p 3000:3000 \
-e JIRA_WEBHOOK_SECRET="your-secret" \
-e LOG_LEVEL=info \
-v $(pwd)/config/config.json:/app/config/config.json:ro \
connectcare:latest
# Run with Docker Compose (includes MongoDB)
docker-compose up -d
# View logs
docker-compose logs -f
# Check health
curl http://localhost:3000/-/healthzHTTP_PORT- Server port (default:3000)LOG_LEVEL- Logging level (default:info)CONFIGURATION_PATH- Config file path (default:/app/config/config.json)JIRA_WEBHOOK_SECRET- Jira webhook secret (if using env-based secrets)MONGO_URL- MongoDB connection string (optional, can also be configured per-sink in config file)
Mount your configuration file:
-v /path/to/your/config.json:/app/config/config.json:ro