Mattermost

CommunicationSelf-HostedPlugin DevelopmentAPIBot DevelopmentOpen Source

Communication Tool

Mattermost

Overview

Mattermost is a self-hostable open-source messaging platform. With a Slack-like UI/UX while being completely operable in your own environment, it's ideal for enterprises prioritizing data sovereignty and security. It offers rich plugin systems, bot development capabilities, and RESTful APIs for advanced customization.

Details

Mattermost is an open-source enterprise messaging platform released in 2016. Designed as a Slack alternative, it's experiencing growing adoption especially among enterprises and organizations that prioritize security and privacy. With rich integrations with DevOps tools including GitLab, it offers features specialized for development teams.

In 2024-2025, significant feature enhancements were made including Playbooks v2 introduction, AI/LLM integration with Agents plugin, migration from Docker Content Trust to Sigstore Cosign, and addition of custom profile attribute fields. The plugin system built with Go and React provides powerful extensibility to modify server, web, and desktop app behavior.

Mattermost's REST API complies with OpenAPI specifications, making bot development and custom integration construction easy. Self-hosted environments support unlimited users with advanced enterprise security and management features. You can install pre-built plugins from Mattermost Marketplace or develop your own plugins.

Pros and Cons

Pros

  • Complete Self-Hosting: Own environment operation, data sovereignty assurance
  • Open Source: Source code availability, high customization freedom
  • Rich Plugin System: Powerful extensions with Go + React
  • DevOps Integration: Rich connectivity with GitHub, GitLab, Jenkins, etc.
  • Enterprise Security: LDAP/SAML, multi-factor authentication, audit logs
  • Unlimited Users: No restrictions in self-hosted environments
  • Bot & API Features: RESTful API, Interactive Bot, Webhook support
  • AI Integration: Multiple LLM support through Agents plugin

Cons

  • Operational Costs: Need for server construction, maintenance, and management
  • Learning Curve: Technical requirements for setup and customization
  • Cloud Version Limitations: Custom plugin restrictions in Mattermost Cloud
  • UI/UX: Usability challenges compared to Slack
  • Mobile Apps: Native app feature limitations
  • Marketplace: Plugin ecosystem scale

Key Links

Code Examples

Basic Mattermost Plugin Development Structure

// server/main.go - Plugin main entry point
package main

import (
    "fmt"
    "encoding/json"
    "github.com/mattermost/mattermost/server/public/plugin"
    "github.com/mattermost/mattermost/server/public/model"
)

type Plugin struct {
    plugin.MattermostPlugin
    configuration *configuration
}

type configuration struct {
    Enabled bool `json:"enabled"`
    APIKey  string `json:"api_key"`
}

// OnActivate called when plugin is activated
func (p *Plugin) OnActivate() error {
    p.API.LogInfo("Plugin activated")
    
    // Register slash command
    if err := p.API.RegisterCommand(&model.Command{
        Trigger:          "hello",
        DisplayName:      "Hello World",
        Description:      "Execute Hello World command",
        AutoComplete:     true,
        AutoCompleteDesc: "Send Hello World message",
    }); err != nil {
        return fmt.Errorf("command registration error: %w", err)
    }
    
    return nil
}

// ExecuteCommand called when slash command is executed
func (p *Plugin) ExecuteCommand(c *plugin.Context, args *model.CommandArgs) (*model.CommandResponse, *model.AppError) {
    switch args.Command {
    case "/hello":
        return &model.CommandResponse{
            ResponseType: model.CommandResponseTypeEphemeral,
            Text:         "Hello, World! Sent from plugin.",
        }, nil
    default:
        return &model.CommandResponse{
            ResponseType: model.CommandResponseTypeEphemeral,
            Text:         "Unknown command.",
        }, nil
    }
}

// MessageWillBePosted called before message is posted
func (p *Plugin) MessageWillBePosted(c *plugin.Context, post *model.Post) (*model.Post, string) {
    // Process messages containing specific keywords
    if contains(post.Message, "emergency") {
        // Notify administrators for emergency messages
        p.sendAdminNotification(post)
    }
    
    return post, ""
}

func (p *Plugin) sendAdminNotification(originalPost *model.Post) {
    adminPost := &model.Post{
        UserId:    p.botUserID,
        ChannelId: p.adminChannelID,
        Message:   fmt.Sprintf("🚨 Emergency message posted: %s", originalPost.Message),
    }
    
    if _, err := p.API.CreatePost(adminPost); err != nil {
        p.API.LogError("Failed to send admin notification", "error", err.Error())
    }
}

func main() {
    plugin.ClientMain(&Plugin{})
}

Webhook Integration Implementation

// webhook.go - Webhook processing implementation
package main

import (
    "encoding/json"
    "net/http"
    "github.com/mattermost/mattermost/server/public/plugin"
    "github.com/mattermost/mattermost/server/public/model"
)

type WebhookPayload struct {
    EventType string      `json:"event_type"`
    Data      interface{} `json:"data"`
}

// InitAPI HTTP route initialization
func (p *Plugin) InitAPI() *mux.Router {
    r := mux.NewRouter()
    r.HandleFunc("/webhook", p.handleWebhook).Methods("POST")
    r.HandleFunc("/health", p.handleHealth).Methods("GET")
    return r
}

func (p *Plugin) handleWebhook(w http.ResponseWriter, r *http.Request) {
    var payload WebhookPayload
    
    if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
        http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
        return
    }
    
    switch payload.EventType {
    case "deployment.success":
        p.handleDeploymentSuccess(payload.Data)
    case "build.failed":
        p.handleBuildFailed(payload.Data)
    case "security.alert":
        p.handleSecurityAlert(payload.Data)
    default:
        p.API.LogInfo("Unhandled webhook event", "event_type", payload.EventType)
    }
    
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status": "success"}`))
}

func (p *Plugin) handleDeploymentSuccess(data interface{}) {
    deploymentData := data.(map[string]interface{})
    
    post := &model.Post{
        UserId:    p.botUserID,
        ChannelId: p.deploymentChannelID,
        Message:   fmt.Sprintf("✅ Deployment successful: %s", deploymentData["service"]),
        Props: map[string]interface{}{
            "attachments": []map[string]interface{}{
                {
                    "color":  "#28a745",
                    "title":  "Deployment Details",
                    "fields": []map[string]interface{}{
                        {"title": "Service", "value": deploymentData["service"], "short": true},
                        {"title": "Version", "value": deploymentData["version"], "short": true},
                        {"title": "Environment", "value": deploymentData["environment"], "short": true},
                    },
                },
            },
        },
    }
    
    if _, err := p.API.CreatePost(post); err != nil {
        p.API.LogError("Post creation error", "error", err.Error())
    }
}

Interactive Dialog Implementation

// dialog.go - Interactive dialog implementation
func (p *Plugin) openTaskCreationDialog(userID, triggerId string) {
    dialog := model.OpenDialogRequest{
        TriggerId: triggerId,
        URL:       fmt.Sprintf("%s/plugins/%s/dialog", p.GetSiteURL(), manifest.Id),
        Dialog: model.Dialog{
            CallbackId:       "create_task_dialog",
            Title:            "Create New Task",
            IconURL:          p.GetBundlePath() + "/assets/icon.png",
            SubmitLabel:      "Create",
            NotifyOnCancel:   true,
            Elements: []model.DialogElement{
                {
                    DisplayName: "Task Name",
                    Name:        "task_title",
                    Type:        "text",
                    SubType:     "text",
                    MaxLength:   100,
                    Placeholder: "Enter task name",
                },
                {
                    DisplayName: "Description",
                    Name:        "task_description",
                    Type:        "textarea",
                    MaxLength:   500,
                    Placeholder: "Enter task details",
                    Optional:    true,
                },
                {
                    DisplayName: "Priority",
                    Name:        "priority",
                    Type:        "select",
                    Options: []*model.PostActionOptions{
                        {Text: "Low", Value: "low"},
                        {Text: "Medium", Value: "medium"},
                        {Text: "High", Value: "high"},
                        {Text: "Urgent", Value: "urgent"},
                    },
                },
                {
                    DisplayName: "Due Date",
                    Name:        "due_date",
                    Type:        "text",
                    SubType:     "date",
                    Optional:    true,
                },
            },
        },
    }
    
    if err := p.API.OpenInteractiveDialog(dialog); err != nil {
        p.API.LogError("Dialog open error", "error", err.Error())
    }
}

// Dialog submission handling
func (p *Plugin) handleDialogSubmission(w http.ResponseWriter, r *http.Request) {
    var request model.SubmitDialogRequest
    if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    if request.CallbackId == "create_task_dialog" {
        task := Task{
            Title:       request.Submission["task_title"].(string),
            Description: request.Submission["task_description"].(string),
            Priority:    request.Submission["priority"].(string),
            DueDate:     request.Submission["due_date"].(string),
            CreatedBy:   request.UserId,
            CreatedAt:   time.Now(),
        }
        
        if err := p.createTask(task); err != nil {
            response := model.SubmitDialogResponse{
                Error: "Failed to create task",
            }
            w.Header().Set("Content-Type", "application/json")
            json.NewEncoder(w).Encode(response)
            return
        }
        
        // Success response
        w.WriteHeader(http.StatusOK)
    }
}

Bot API Message Processing

// Node.js Mattermost Bot implementation
const axios = require('axios');

class MattermostBot {
  constructor(serverUrl, botToken) {
    this.serverUrl = serverUrl;
    this.botToken = botToken;
    this.headers = {
      'Authorization': `Bearer ${botToken}`,
      'Content-Type': 'application/json'
    };
  }

  // Send message
  async sendMessage(channelId, message, props = {}) {
    try {
      const response = await axios.post(
        `${this.serverUrl}/api/v4/posts`,
        {
          channel_id: channelId,
          message: message,
          props: props
        },
        { headers: this.headers }
      );
      
      return response.data;
    } catch (error) {
      console.error('Message send error:', error.response.data);
      throw error;
    }
  }

  // File upload
  async uploadFile(channelId, filePath, filename) {
    const FormData = require('form-data');
    const fs = require('fs');
    
    const form = new FormData();
    form.append('files', fs.createReadStream(filePath), filename);
    form.append('channel_id', channelId);
    
    try {
      const response = await axios.post(
        `${this.serverUrl}/api/v4/files`,
        form,
        {
          headers: {
            ...this.headers,
            ...form.getHeaders()
          }
        }
      );
      
      return response.data;
    } catch (error) {
      console.error('File upload error:', error.response.data);
      throw error;
    }
  }

  // Get channel information
  async getChannel(channelId) {
    try {
      const response = await axios.get(
        `${this.serverUrl}/api/v4/channels/${channelId}`,
        { headers: this.headers }
      );
      
      return response.data;
    } catch (error) {
      console.error('Channel info error:', error.response.data);
      throw error;
    }
  }

  // WebSocket connection for real-time event monitoring
  connectWebSocket() {
    const WebSocket = require('ws');
    const ws = new WebSocket(`${this.serverUrl.replace('http', 'ws')}/api/v4/websocket`, {
      headers: { 'Authorization': `Bearer ${this.botToken}` }
    });

    ws.on('open', () => {
      console.log('WebSocket connection established');
      
      // Send authentication message
      ws.send(JSON.stringify({
        seq: 1,
        action: 'authentication_challenge',
        data: { token: this.botToken }
      }));
    });

    ws.on('message', (data) => {
      const event = JSON.parse(data);
      this.handleWebSocketEvent(event);
    });

    return ws;
  }

  handleWebSocketEvent(event) {
    switch (event.event) {
      case 'posted':
        this.handleNewPost(event.data.post);
        break;
      case 'user_added':
        this.handleUserAdded(event.data);
        break;
      case 'channel_created':
        this.handleChannelCreated(event.data.channel);
        break;
      default:
        console.log('Unhandled event:', event.event);
    }
  }

  async handleNewPost(postData) {
    const post = JSON.parse(postData);
    
    // Ignore bot's own posts
    if (post.user_id === this.botUserId) return;
    
    // Mention detection
    if (post.message.includes(`@${this.botUsername}`)) {
      await this.processCommand(post);
    }
    
    // Keyword monitoring
    if (post.message.toLowerCase().includes('help')) {
      await this.sendHelpMessage(post.channel_id);
    }
  }

  async processCommand(post) {
    const message = post.message.replace(`@${this.botUsername}`, '').trim();
    const command = message.split(' ')[0].toLowerCase();
    
    switch (command) {
      case 'weather':
        await this.handleWeatherCommand(post);
        break;
      case 'schedule':
        await this.handleScheduleCommand(post);
        break;
      case 'report':
        await this.handleReportCommand(post);
        break;
      default:
        await this.sendMessage(post.channel_id, 'Unknown command. Type `help` to show help.');
    }
  }
}

// Usage example
const bot = new MattermostBot('https://your-mattermost.com', 'your-bot-token');

// Start WebSocket connection
bot.connectWebSocket();

// Regular status reporting
setInterval(async () => {
  const statusMessage = {
    message: '📊 System Status Report',
    props: {
      attachments: [{
        color: '#28a745',
        fields: [
          { title: 'CPU Usage', value: '45%', short: true },
          { title: 'Memory Usage', value: '67%', short: true },
          { title: 'Active Users', value: '234', short: true }
        ]
      }]
    }
  };
  
  await bot.sendMessage('status-channel-id', statusMessage.message, statusMessage.props);
}, 3600000); // Every hour

Management Operations with mmctl

# Authenticate to Mattermost server
mmctl auth login https://your-mattermost.com --name production --username admin

# Plugin management
mmctl plugin add hovercardexample.tar.gz
mmctl plugin enable hovercardexample
mmctl plugin list
mmctl plugin disable hovercardexample

# Bot creation and management
mmctl bot create mybot --description "Automation Bot" --display-name "Automation Bot"
mmctl bot list
mmctl bot enable mybot

# Webhook creation
mmctl webhook create-incoming \
  --channel general \
  --user admin \
  --display-name "Deploy Notifications" \
  --description "Deployment notification webhook"

# Team/Channel management
mmctl team create --name devops --display_name "DevOps Team"
mmctl channel create devops --name alerts --display_name "Alert Notifications"

# User management
mmctl user create --email [email protected] --username newuser --password password123
mmctl user activate newuser
mmctl team users add devops newuser

Environment Variables Configuration

# .env file
# Mattermost server settings
MATTERMOST_SERVER_URL=https://your-mattermost.com
MATTERMOST_BOT_TOKEN=your-bot-access-token
MATTERMOST_BOT_USERNAME=automation-bot

# Database settings (self-hosted)
MM_SQLSETTINGS_DRIVERNAME=postgres
MM_SQLSETTINGS_DATASOURCE=postgres://user:password@localhost/mattermost?sslmode=require

# File storage settings
MM_FILESETTINGS_DRIVERNAME=local
MM_FILESETTINGS_DIRECTORY=/opt/mattermost/data/

# Plugin settings
MM_PLUGINSETTINGS_ENABLE=true
MM_PLUGINSETTINGS_ENABLEUPLOADS=true
MM_PLUGINSETTINGS_AUTOMATICPREPACKAGEDPLUGINS=true

# Security settings
MM_SERVICESETTINGS_ALLOWEDUNTRUSTEDINTERNALCONNECTIONS=localhost
MM_SERVICESETTINGS_ENABLELOCALMODE=false

# External integrations
GITHUB_TOKEN=your-github-token
JIRA_API_KEY=your-jira-api-key
SLACK_WEBHOOK_URL=your-slack-webhook-url

Docker Compose Configuration Example

# docker-compose.yml
version: '3.8'

services:
  mattermost:
    image: mattermost/mattermost-enterprise-edition:latest
    container_name: mattermost
    ports:
      - "8065:8065"
    environment:
      - MM_SQLSETTINGS_DRIVERNAME=postgres
      - MM_SQLSETTINGS_DATASOURCE=postgres://mattermost:password@postgres:5432/mattermost?sslmode=disable
      - MM_FILESETTINGS_DRIVERNAME=local
      - MM_FILESETTINGS_DIRECTORY=/mattermost/data/
      - MM_PLUGINSETTINGS_ENABLE=true
      - MM_SERVICESETTINGS_SITEURL=https://your-mattermost.com
    volumes:
      - mattermost_data:/mattermost/data
      - mattermost_logs:/mattermost/logs
      - mattermost_config:/mattermost/config
      - mattermost_plugins:/mattermost/plugins
    depends_on:
      - postgres
    networks:
      - mattermost-network

  postgres:
    image: postgres:13
    container_name: mattermost_postgres
    environment:
      - POSTGRES_USER=mattermost
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=mattermost
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - mattermost-network

  nginx:
    image: nginx:alpine
    container_name: mattermost_nginx
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - mattermost
    networks:
      - mattermost-network

volumes:
  mattermost_data:
  mattermost_logs:
  mattermost_config:
  mattermost_plugins:
  postgres_data:

networks:
  mattermost-network:
    driver: bridge

Plugin Manifest Example

{
  "id": "com.company.automation-plugin",
  "name": "Automation Plugin",
  "description": "DevOps automation plugin",
  "version": "1.0.0",
  "min_server_version": "9.0.0",
  "server": {
    "executables": {
      "linux-amd64": "server/dist/plugin-linux-amd64",
      "darwin-amd64": "server/dist/plugin-darwin-amd64",
      "windows-amd64": "server/dist/plugin-windows-amd64.exe"
    },
    "executable": ""
  },
  "webapp": {
    "bundle_path": "webapp/dist/main.js"
  },
  "settings_schema": {
    "header": "Plugin Settings",
    "footer": "See [documentation](https://docs.company.com/automation-plugin) for details.",
    "settings": [
      {
        "key": "EnableNotifications",
        "display_name": "Enable Notifications",
        "type": "bool",
        "help_text": "Enable notifications for automation events.",
        "default": true
      },
      {
        "key": "APIEndpoint",
        "display_name": "API Endpoint",
        "type": "text",
        "help_text": "Enter the external API endpoint URL.",
        "placeholder": "https://api.company.com"
      }
    ]
  }
}