Skip to content

machine viewenglishraw: /blog/mustachio-ruby.mdbuild: d68074d2026-08-05T14:02Z

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

Themes: ruby · rails

TECHNICAL ARTICLE · 21 JAN 2026 · JOÃO VICTOR · 3 MIN

mustachio-ruby: The Postmark template engine in Ruby

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, 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:

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 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:

gem 'mustachio-ruby'

And then execute:

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)

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)

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

3. Context Object (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)

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

Basic Usage

Simple Variable Interpolation

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

Array Iteration

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

# 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

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 to build your email templates and then use mustachio-ruby (or the original C# version) to interpolate your variables:

mustachio_template = <<~HTML
  <div>
    <h1>Hello {{name}}!</h1>
    {{#user}}
      <p>Welcome back!</p>
    {{/user}}
    {{^user}} <!-- negative check -->
      <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):

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