## mustachio-ruby: The Postmark template engine in Ruby

technical-article · 2026-01-21 · João Victor · 3 min · en

> Use your Postmark templates in Ruby with this template engine port.

Mustachio is the template engine that powers Postmark templates. While similar to Mustache, it has key differences in syntax for handling arrays and destructuring that make it uniquely suited for email templating.

You might wonder: why create another template engine when we already have excellent solutions like ERB, HAML, or Slim? I wouldn't recommend abandoning these tools for general web development. However, if you're already using Postmark templates, mustachio-ruby can help reduce your dependency on Postmark's service.

At [Bleu](https://bleu.builders), most of our projects use Postmark for transactional emails. We love their service and build our templates directly in their platform. However, in 2024, Postmark experienced an incident where emails took significantly longer to deliver. OTP codes were delayed by 40+ minutes. While rare, this incident made us realize we couldn't be completely dependent on a single email service. We needed a fallback system using an alternative SMTP provider like Resend.

## The Problem: Template Lock-in

The quick solution was to create a fallback system that used Resend to send emails:

```ruby
module FallbackMailing
  extend ActiveSupport::Concern

  included do
    if Rails.application.config.use_fallback_mailer_service
      def template_model=(model)
        @template_model = model
      end

      def template_model
        @template_model
      end
    end
  end
end

class ApplicationMailer < ActionMailer::Base
  default from: "no-reply@example.com"

  if Rails.application.config.use_fallback_mailer_service
    include FallbackMailing
  else
    include PostmarkRails::TemplatedMailerMixin

    def mail(headers = {}, &block)
      # remove not allowed attributes when using Postmark
      %i[subject header body content_type].each { |key| headers.delete(key) }
      super
    end
  end
end
```

This solution allowed us to send emails through alternative SMTP providers, but we faced another challenge: our templates were stored exclusively in Postmark.

We didn't want to abandon Postmark or maintain duplicate template versions. Our solution was to sync templates—keeping identical copies of our Postmark templates in our codebase. However, we discovered that Postmark's [template engine](https://github.com/ActiveCampaign/mustachio?tab=readme-ov-file) was written in C#, not Ruby. This led us to create **mustachio-ruby**: a Ruby port of the original Mustachio template engine.

## Installation

Add this line to your application's Gemfile:

```ruby
gem 'mustachio-ruby'
```

And then execute:

```bash
bundle install
```

## How mustachio-ruby Works

### Core Components

The template engine consists of four main components that work together to parse, compile, and render templates:

**1. Tokenizer** ([lib/mustachio_ruby/tokenizer.rb](https://github.com/bleu/mustachio-ruby/tree/main/lib/mustachio_ruby/tokenizer.rb))

Parses raw template strings into structured tokens using regex patterns to identify template elements like variables, conditionals, and loops.

**2. Parser** ([lib/mustachio_ruby/parser.rb](https://github.com/bleu/mustachio-ruby/tree/main/lib/mustachio_ruby/parser.rb))

Transforms tokens into executable template functions that can be called with data.

**3. Context Object** ([lib/mustachio_ruby/context_object.rb](https://github.com/bleu/mustachio-ruby/tree/main/lib/mustachio_ruby/context_object.rb))

Manages data access during rendering, including:

- Dot-notation access (`user.profile.name`)
- Parent scope access (`../`)
- Existence checks (`exists?` method)

**4. Token Types** ([lib/mustachio_ruby/token_tuple.rb](https://github.com/bleu/mustachio-ruby/tree/main/lib/mustachio_ruby/token_tuple.rb))

Defines template element types: HTML-escaped variables, raw variables, conditional blocks, array iteration, and negated conditionals.

## Basic Usage

### Simple Variable Interpolation

```ruby
template = MustachioRuby.parse("Hello {{name}}!")
content = template.call({"name" => "World"})
# => "Hello World!"
```

### Array Iteration

```ruby
template = "{{#each Company.ceo.products}}<li>{{ name }} and {{version}} and has a CEO: {{../../last_name}}</li>{{/each}}"
renderer = MustachioRuby.parse(template)

model = {
  "Company" => {
    "ceo" => {
      "last_name" => "Smith",
      "products" => [
        { "name" => "name 0", "version" => "version 0" },
        { "name" => "name 1", "version" => "version 1" },
      ]
    }
  }
}

result = renderer.call(model)
# =>
# <li>name 0 and version 0 and has a CEO: Smith</li>
# <li>name 1 and version 1 and has a CEO: Smith</li>
```

### Conditional Rendering

```ruby
# Show content when user exists
template = MustachioRuby.parse("{{#user}}Welcome {{name}}!{{/user}}")

# Show content when user doesn't exist
template = MustachioRuby.parse("{{^user}}Please log in{{/user}}")
```

### Configuration Options

```ruby
options = MustachioRuby::ParsingOptions.new
options.disable_content_safety = true # Disable HTML escaping
options.source_name = "my_template" # For error reporting
options.token_expanders = [expander] # Custom extensions

template = MustachioRuby.parse(source, options)
```

## HTML Email Templates

You can follow the Postmark [template syntax](https://postmarkapp.com/support/article/1077-template-syntax) to build your email templates and then use mustachio-ruby (or the original C# version) to interpolate your variables:

```ruby
mustachio_template = <<~HTML
  <div>
    <h1>Hello {{name}}!</h1>
    {{#user}}
      <p>Welcome back!</p>
    {{/user}}
    {{^user}} 
      <p>Please log in</p>
    {{/user}}
    <ul>
      {{#each items}}
        <li>{{name}}: ${{price}}</li>
      {{/each}}
    </ul>
  </div>
HTML

data = {
  "name" => "John",
  "user" => { "name" => "John" },
  "items" => [
    { "name" => "Apple", "price" => 1.50 },
    { "name" => "Banana", "price" => 0.75 }
  ]
}

template = MustachioRuby.parse(mustachio_template)
result = template.call(data)

# =>
# <div>
#   <h1>Hello John!</h1>
#   <p>Welcome back!</p>
#   <ul>
#     <li>Apple: $1.5</li>
#     <li>Banana: $0.75</li>
#   </ul>
# </div>
```

## Ruby on Rails Integration

To use mustachio-ruby in Rails, set the body to be the parsed HTML content and the mail content_type to be "text/html" (or "text/plain" if you're using mustachio to interpolate variables in a plain text string):

```ruby
def mail(headers = {}, &)
  template_content = get_template(template_alias)
  options = MustachioRuby::ParsingOptions.new
  options.disable_content_safety = true
  renderer = MustachioRuby.parse(template_content, options)
  html_content = renderer.call(template_model.deep_stringify_keys)
  headers[:body] = html_content
  headers[:content_type] = "text/html"

  super
end
```

## Final Thoughts

Postmark is a fantastic service, and we continue to use it every day. mustachio-ruby isn't a replacement—it's a safety net. It lets you render templates in pure Ruby and send emails even when you don't have access to Postmark.

If your team relies on Postmark templates, I hope mustachio-ruby gives you the flexibility to build resilient email systems without vendor lock-in.

## Learn More

- [mustachio-ruby on GitHub](https://github.com/bleu/mustachio-ruby)
- [Postmark Template Syntax Documentation](https://postmarkapp.com/support/article/1077-template-syntax)
- [Original Mustachio (C#) Repository](https://github.com/ActiveCampaign/mustachio)


Themes: ruby · rails
