Matthew Lindfield Seager

Enjoyed watching WWDC this year! One nice thing about being in Cambodia is that it went from midnight to 2am, rather than 3am-5am in Sydney.

Almost time for a new laptop, struggling to decide between the 14” M1 Pro MBP or 13.6” M2 MBA 🤔

Almost a perfect bell curve of results in Wordle… add in my 2 misses, that I almost certainly would have got in 7, and it’s almost perfectly symmetrical!

Found a useful tip on how to combine a bunch of files from nested sub-folders into the parent folder…

In my case the folder name was Archive:
find Archive/ -mindepth 2 -type f -exec mv -i '{}' Archive/ ';'

User Friendly Error Messages for Multiple Fields in Rails

The built in error system in Rails, specifically displaying errors in an HTML form when data isn’t valid, is mature and works really well for simple cases.

Adding validates :name, presence: true to a user model is all we need to do to prevent people from saving a user without a name. Straight out of the box we automatically get a count of the errors and details of each error: Error messages "out of the box"

Rails also wraps the field and label in a div with class field_with_errors so two lines of CSS in the application layout is all it takes to initially add some basic error highlighting to an “out of the box” application:

  .field_with_errors { color: #b22; }
  .field_with_errors input { background-color: #fcc; }

Forms errors with two lines of CSS added

More complicated checks are also possible but displaying the results of those checks in a user-friendly way wasn’t very intuitive to me. This post documents the things I tried and the solution I ended up with.

The end goal is to require at least one of two fields to be present for the form to be valid. To make that clear to the user, I want it to show up as a single error but with both fields highlighted:

Single error but with two fields highlighted

Solution 1

The approach you’ll find suggested multiple times on StackOverflow is to add a check to both fields:

validates :email, presence: {unless: :username?}
validates :username, presence: {unless: :email?}

Naive solution, checking both fields separately

Technically this works but I don’t like the way it shows two different errors or that the error messages aren’t entirely truthful. Even if we customise the error messages to make them more accurate, it doesn’t really improve the situation as the error messages are still redundant (not to mention awkwardly worded as they must start with the name of the field):

validates :email, presence: {unless: :username?, message: "or Username must be present"}
validates :username, presence: {unless: :email?, message: "or Email must be present"}

Updating the messages doesn't really help the situation

Solution 2

The next solution is to try a custom validation. By adding an error to :base it applies to the whole model object, rather than an individual field:

validate :email_or_username

def email_or_username
  if email.blank? && username.blank?
    errors.add(:base, message: "At least one of Email and Username must be provided")

This gets us closer to our desired end state of one error for one problem but we lose field highlighting:

One error for one problem but no field highlighting

We can modify the validation method to get field highlighting but then we end up with three errors:

if email.blank? && username.blank?
  errors.add(:base, message: "At least one of Email and Username must be provided")

Correct field highlighting but too many errors

Solution 3

We need some way to distinguish the field errors from the overall record error in a way that will make sense to us in the future. As of Rails 6.1, errors are first class objects and the optional second argument to ActiveModel::Errors.add is a type symbol.

If we add an explicit type to the errors that will help us identify them later:

# app/models/user.rb
if email.blank? && username.blank?
  errors.add(:base, :email_and_username_blank, message: "At least one of Email and Username must be provided")
  errors.add(:email, : highlight_field_but_hide_message)
  errors.add(:username, : highlight_field_but_hide_message)

Then in our generated view we need to somehow ignore or delete the ...hide_message errors. At first I thought I could use ActiveModel::Errors.delete to delete them but there are multiple problems with that approach:

  • to match on a type you first have to match on attribute (either looping through all attribute names or listing attributes explicitly)
  • deleting the error prevents the field from being highlighted so you need to render the fields first, then delete the errors, then show the error message (which would also require some CSS shenanigans to move it back to the top as per the original requirement)

The simplest approach seems to be to take advantage of the fact that Errors is Enumerable and simply reject the errors we don’t want to include in the summary:

# app/views/users/_form.html.erb
<%= form_with(model: user) do |form| %>
  <% visible_errors = user.errors.reject{ |e| e.type == :highlight_field_but_hide_message } %>
  <% if visible_errors.any? %>
    <div class="text-red-700 border border-red-700 border-rounded m-2 p-2 bg-red-200 max-w-md">
      <h2 class="text-xl"><%= pluralize(visible_errors.count, "error") %> prohibited this user from being saved:</h2>

      <ul class="list-disc list-inside">
        <% visible_errors.each do |error| %>
          <li><%= error.full_message %></li>
        <% end %>
  <% end %>
<% end %>

One error message but multiple fields highlighted


With a small amount of additional effort it’s possible to show a single error message but highlight multiple fields. My next task is to override Rails.application.config.action_view.field_error_proc so I can use Tailwind CSS to style the fields with errors.

I’m not sure how I feel about “lossy” Markdown support via autocorrect in a WYSIWYG word processor……

To me the main point of markdown is portability 🤔

Static Assets in Rails 7

Note for future me (written partially for myself, partially in answer to this SO question).

The home for static or public assets is the /public folder. In a default Rails 7 app you will find some error pages (e.g. /public/404.html) and icons (e.g. /public/favicon.ico) already in that folder. These are then served directly from the “raw” domain (at and respectively). They are also accessible under

To reference these static images from ERB you need to make sure you tell Rails it’s an absolute path, not a relative one somewhere in the asset pipeline. This is done with a leading forward slash.

The following compares including a logo called logo.png when it’s stored in app/assets/images vs /public

<%= image_tag "logo.png" %>
<!-- becomes <img src="/assets/logo-1f04fdc3ec379029cee88d56e008774df299be776f88e7a9fe5.png"> or similar -->

<%= image_tag "/logo.png" %>
<!-- note the leading slash. This becomes <img src="/logo.png"> -->

<img src="/logo.png">
<!-- plain HTML if you don't want to use ERB tag -->

You can also use sub-directories; /public/images/logo.png would be available at /images/logo.png (or image_tag "/images/logo.png").

The second paragraph of chapter 2 of The Asset Pipeline Rails Guide contains more information. It mentions that this functionality depends on whether config.public_file_server.enabled is set.

In Rails 7 that config defults to ENV["RAILS_SERVE_STATIC_FILES"].present? in config/production.rb. If you’re using Heroku you can check this setting is present and enabled with heroku config:get RAILS_SERVE_STATIC_FILES.

I know naming is hard but seriously, “standard” vs “default” was the best you could come up with Googlers?!…

The default type is not standard and the standard type is not the default… 🤷‍♂️

Interesting design pattern in Google Workspace Admin Console: Related settings have arrows. Hovering over an arrow highlights the related setting that must be enabled first in order to turn the dependent setting on

“Cryptocurrencies leave me… feeling like the boy watching the naked emperor… So many significant… institutions are admiring his incredible coat, …so technically complicated and superior that normal people… can’t comprehend it and must take it on trust…”

CI on Github Actions with Rails 7, Postgres, esbuild, Tailwind CSS and StandardRB

After a bit of research and a lot of trial and error I finally got Github Actions working for CI on a Rails 7 (alpha 2) app that uses Postgres, esbuild and Tailwind CSS, plus StandardRB for formatting

It’s kind of hard to believe, but it seems you get it all for free!

Here’s what worked for me:

# test_and_lint.yml
name: Test and Lint

    branches: [ main ]
    branches: [ main ]

    runs-on: ubuntu-latest

        image: postgres
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
          - 5432:5432

      - uses: actions/checkout@v2

      - uses: ruby/setup-ruby@v1
          bundler-cache: true

      - name: Install yarn and build assets
        run: |
          yarn --frozen-lockfile
          yarn build
          yarn build:css

      - name: Install psql
        run: sudo apt-get -yqq install libpq-dev

      - name: Build DB
          PGHOST: localhost
          PGUSER: postgres
          PGPASSWORD: postgres
          RAILS_ENV: test
        run: bin/rails db:setup

      - name: Run Tests
          PGHOST: localhost
          PGUSER: postgres
          PGPASSWORD: postgres
          RAILS_ENV: test
        run: |
          bundle exec rake test
          bundle exec rake test:system

    runs-on: ubuntu-latest
      - name: standardrb
          GITHUB_TOKEN: $&#123;&#123; secrets.GITHUB_TOKEN }}
        uses: amoeba/standardrb-action@v2

Big thanks to Andy Croll, his instructions were super helpful to get the basic build and test workflow working with Postgres.

Vincent Voyer’s instructions were helpful for the yarn installation step.

From there, I just needed to add the yarn build and yarn build-css commands to trigger the build steps defined in package.json.

The Gift of Context

Today I had an implementation question for one of our campus principals and she gifted me with the reasoning behind her answer, in addition to answering the question. I call it a gift because it helps me understand her priorities and empowers me to make small related decisions that I might have otherwise had to keep going back to ask her about.

It actually reminded me of something I learned a long time ago in the ADF called “Commander’s Intent”. When issuing orders down the chain of command, each commander should explain the intent or strategy behind their orders to their subordinates. If, or more accurately- when, the plan goes out the window, junior officers, NCOs and even individual soldiers should know enough of the context to adjust their own actions to keep working towards the overall strategy.

For example, instead of just ordering a detachment to deploy a radio transmitter on top of a certain feature (hill) by 0400 hours, the commander should first explain that at 0500 hours a parachute squadron will be deploying into a remote area and needs a backup radio link in case their satelite communications fail.

Now let’s say the radio detachment:

a. has a vehicle failure, b. finds the road marked on the map doesn’t exist, c. encounters an enemy patrol on their planned route, or d. is faced with some other unforseen event

The commander on the ground can evaluate options and come up with a new plan instead of having to radio back and forth with HQ trying to explain the situation to someone who isn’t there and then wait for new orders. Based on their first hand experience of the situation and their knowledge of the overall goal, the detachment commander can evaluate various options and then confirm the new plan with HQ. Options considered might include:

a. waiting for logisitics support to fix the vehicles or continuing on foot with the required equipment, b. choosing an alternate deployment location, c. engaging with the enemy patrol or being very careful to avoid them so as not to give away the larger mission, d. working with another detachment or partner force to provide the back up communications channel

It was a good reminder for me as a manager that I need to share the “why” with my team, not just the “what”. I need to give them the gift of context and intent so they know which tasks are important, when to postpone a task that can wait (or abandon one that no longer contributes to the overall goal) and when to change course without having to ask.

I enjoyed reading this blog post about how Signal is actively (and a little bit cheekily) guarding the privacy of its customers…

Spent way too long last night debugging something that worked in dev but not in test… it was all because I hadn’t thought about credentials in the test environment.

I have now updated my Rails credentials post with an improved approach

Encrypted Credentials in Rails

Rails can encrypt keys for you. It encrypts and decrypts them using other keys. Once your keys have been decrypted using the other keys, you can look up your keys by their keys. If that sounds confusing, this post may be the key to getting a better understanding of Rails Credentials.


Learning 1: RAILS_ENV=production rails credentials:edit, RAILS_ENV=development rails credentials:edit and rails credentials:edit all do exactly the same thing. The way to edit per-environment credentials is by adding the --environment flag; rails credentials:edit --environment production.

Learning 2: Regardless of which file they’re defined in, credentials are accessed again using or[:child][:grandchild][:great_grandchild] if you nest credentials.

Learning 3: If an environment-specific credentials file is present, Rails will only look in that file for credentials. The default/shared credentials file will not be used, even if the secret is missing from the environment-specific file.

Bonus Tip: ActiveSupport::OrderedOptions is a handy sub-class of hash that gives you dynamic method based on hash key names and the option of raising an error instead of retuning nil if a requested hash key doesn’t have a value assigned.

That’s the short version. Read on if you’d like some additional context, a bit information about how the Credentials “magic” actually works and some practical implications. If you’re super bored/interested, read on beyond that for some mildly interesting trivia or edge cases.

A note on terminology As noted earlier, the term keys is very overloaded. Credentials, passwords or secrets are often referred to as keys, as in API key. Additionally, Rails Credentials uses a key to encrypt each credential file. Finally, hashes, the data type Rails Credentials uses to store decrypted credentials in memory, use keys to retrieve values. Which is why we can accurately but unhelpfully say, keys are encrypted by other keys and looked up by yet different keys. In an effort to avoid confusion I have used the following naming convention throughout this post:

  1. The term Credentials (upper case C) is shorthand for Rails Credentials, the overall Rails facility for storing secrets needed by your application.
  2. The terms credentials or credential (lower case C) refer to the actual application secret(s) you store using Credentials.
  3. The term “name” is used to refer to the hash key (or YAML key) of a credential.
  4. The term “file encryption key” is used to refer to the main secret that Credentials uses to encrypt a set of your credentials on disk.
  5. Any other unavoiable use of the word “key” will be preceded by a descriptor such as hash key, YAML key or API key.


I’m using Rails’ built in Credentials feature to store Google Workspace API credentials. After seeing how easy it was to delete real Google Workspace users from the directory, I decided I really should be using the test domain Google generously allows education customers to set up. So after adding a Service Account to our test domain, it was time to separate the production credentials from the development/staging credentials.

My first thought was to run RAILS_ENV=production rails credentials:edit but when I did, the existing credentials file opened up. I then tried to specify the development environment to see if maybe I had it backwards but once again the same credentials file opened up.

There’s nothing in the Rails Guide on security about it but eventually I found a reference to the original PR for this feature which explains the need to specify the environment using the --environment flag.

Here are some of the things I learned while exploring this corner of Rails.


1. RAILS_ENV has no effect on the rails credentials commands

The command rails credentials:edit, regardless of the value of RAILS_ENV, will always attempt to open and decrypt the default credentials file for editing; config/credentials.yml.enc. The way to change which environment you would like to edit credntials for is to use the --environment flag.

When dealing with the default credentials file, the encryption key is obtained from the RAILS_MASTER_KEY environment variable if it is set, otherwise the contents of config/master.key is tried. When you close the decrypted file, it is re-encrypted with the encryption key.

If you specify an environment using (for example) rails credentials:edit --environment production, then a different credentials file will be opened (or created) at config/credentials/production.yml.enc. This one might use the same encryption key or it might not. If the same RAILS_MASTER_KEY environment variable is set, it will use that to encrypt the file. If it isn’t set, it will use (or create on first edit) a different key stored in a correspondingly named file, config/credentials/production.key in our example.

Here’s a table showing 4 different credential commands, the files they maintain, and the location of the encryption keys used for each file:

Command Credentials File Encryption key Environment Variable Encryption Key File (if ENV VAR not set)
rails credentials:edit config/credentials.yml.enc RAILS_MASTER_KEY /config/master.key
rails credentials:edit --environment development /config/credentials/development.yml.enc RAILS_MASTER_KEY /config/credentials/development.key
rails credentials:edit --environment test /config/credentials/test.yml.enc RAILS_MASTER_KEY /config/credentials/test.key
rails credentials:edit --environment production /config/credentials/production.yml.enc RAILS_MASTER_KEY /config/credentials/production.key

2. First level credentials are accessible as methods, child credentials must be accessed via their hash keys

With your credentials successfully stored, they can all be accessed within your Rails app (or in the console) via the Rails.application.credentials hash. The credential names in the YAML are symbolized so credential_name: my secret password can be accessed via Rails.application.credentials[:credential_name]. For your convenience, first level credentials are also made available as methods so you can access them using

If you nest additional credentials, they form a hash of hashes and can be accessed using standard hash notation. I can’t imagine why you’d want more than 2, maybe 3, levels in a real application but if you had six levels of nesting the way to access the deepest item would be[:child][:grandchild][:gen_4][:gen_5][:gen_6]. Child credentials can’t be accessed as methods, you must use the hash syntax to access them:[:child].

If you want an exception to be raised if a top level credential can’t be found, use the bang ! version of the method name:!. Without this you’ll just get back nil. You will need to manually guard against missing child credentials yourself though. One way to do this would be![:child].presence || raise(":child is blank"))

3. Only one credentials file can be used in any given environment

If an environment-specific credentials file is present, Rails will only look in that file for credentials. The default credentials file will not be used, even if the requested credential is missing from the environment-specific file and set in the default file.

One implication of this is that, if you use environment specific files, you will need to duplicate any shared keys between files and keep them in sync when they change. I would love to see Credentials improved to first load the default credentials file, if present, with all its values and then load an environment-specific file, if present, with its values. Shared credentials could then be stored in the default file and be overridden (or supplemented) in certain environments.

Take aways

1. Environment-specific files introduce new challenges

Choosing to adopt environment-specific files means choosing to keep common credentials synchronised between files. Small teams may be better off sticking with namespaced credentials in the default file. To my mind, the neatest option is simply adding an environment YAML key where necessary:

# credentials.yml.enc
aws_key: qazwsxedcrfv # same in all environments

  development: &google_defaults
    project_id: 12345678 # shared between all environments
    private_key:  ABC123
    <<: *google_defaults # exactly the same as development
    <<: *google_defaults
    private_key:  DEF456 # override just the values that are different
# Application

If separate files are needed, I think the next best option would be to try to limit yourself to two files; one shared between dev, test and staging, and another one for production. However this will get messy the moment you need to access any production credentials in staging (for example). You’ll then need to either keep all 3 files fully populated or start splitting the contents of one or both of the files using the [Rails.env.to_sym] trick.

2. File Encryption Key sharing/storage

If you lose your file encryption Key, the contents of the encrypted file will also be lost. Individuals could store this file in local backups or store the contents of the file in their password manager.

If multiple team members need access, the file encryption key should be stored in a shared vault. I’m a big fan of myself.

3. File Encryption Key rotation

One way to rotate your File Encryption Keys is to: 1. Run rails credentials:edit to decrypt your current credentials 2. Copy the contents of that file before closing it 3. Delete credentials.yml.enc and master.key (or other file pairs as necessary) 4. Re-run rails credentials:edit to create a new master.key 5. Paste the original contents in, then save and close. This will create a new credentials.yml.enc file 6. Update the copy in your password manager 7. Clear your clipboard history if applicable

Edge cases and trivia

If RAILS_MASTER_KEY is not set and the encryption key file (see table above) does not exist, a new encryption key will be generated and saved to the relevant file. The encryption key file will also be added to .gitignore. The new encyption key will not be able to decrypt existing credential files.

Whilst RAILS_MASTER_KEY lives up to it’s “master key” name and is used by all environment files, config/master.key does not and is not.

The --environment flag accepts either a space, --environment production, or an equals sign, --environment=production.

If you specify a credential name (YAML key) with a hyphen in it, the .name syntax won’t work. Similarly if you name a child credential with a hyphen, you will need to access it with a strange (to me) string/symbol hybrid. The required syntax is Rails.application.credentials[:'hyphen-name'] and[:'child-hyphen-name'] respectively.

You can’t change the file encryption key by running credentials:edit and then changing the file encryption key whlie the credentials file is still open. The original file encryption key is kept in memory and used to re-encrypt the contents when the credentials file is closed.

Even though you don’t use RAILS_ENV to set the environment, the environment name you pass to the --environment flag should match a real envrionment name. If you run rails credentials:edit --environment foo, Rails will happily generate foo.yml.enc and foo.key but unless you have a Rails environment named foo the credentials will never be (automatically) loaded.

Some YAML files in Rails are parsed with ERB first. Credentials files are not so you can’t include Ruby code in your credentials file.

YAML does allow you to inherit settings from one section to another. By appending &foo to the end of a parent key you can “copy” all the child keys to another YAML node with <<: *foo. See the example in Takeaway 1 above for a fuller example.

In development, Rails.application.credentials will not have any credentials loaded (in @config) until after you first access one explicitly or access them all with Rails.application.credentials.config.

This may also be theoretically true in production, but in practice the production environment tries to validate secret_key_base at startup, thereby loading all the credentials straight away.

Whilst technically the credentials live in the Rails.application.credentials.config hash, Credentials delegates calls to the :[] and :fetch methods to :config. This allows us to drop the .config part of the method call.

Missing methods on Rails.application.credentials get delegated to :options. The options method simply converts theconfig hash into ActiveSupport::OrderedOptions, a sub-class of Hash. OrderedOptions is what provides the .name shortcuts and the .name! alternatives. I can think of a few other use cases where OrderedOptions would be handy! If you already have a hash you need to use ActiveSupport::InheritableOptions to convert it into an OrderedOptions collection.

Why Action Mailbox can't be used with Gmail

I’ve seen a few questions today about how to get Rails’ Action Mailbox working with Gmail so you can process Gmail messages in a Rails app. The short answer is:

  • Gmail doesn’t support it
  • Rails doesn’t support it

A longer answer, which I also posted on Stack Overflow in reply to the second question above, follows.

Action Mailbox is built around receiving email from a Mail Transfer Agent (MTA) in real time, not periodically fetching email from a mailbox. That is, it receives mail sent via SMTP, it doesn’t fetch mail (using IMAP or POP3) from another server that has already received it.

For this to work it is dependent on an external (to Rails) SMTP service receiving the email and then delivering the email to Action Mailbox. These external services are called “Ingresses” and, as at the time of writing, there are 5 available ingresses.

Of the five, four are commercial services that will run the required SMTP servers for you and then “deliver” the email to your application (usually as a JSON payload via a webhook).

You could already use those services in a Rails App and handle the webhooks yourself but Action Mailbox builds a standardised set of functionality on top. Almost like a set of rails to guide and speed the process.

In addition, the fifth ingress is the “Relay” ingress. This allows you to run your own supported MTA (SMTP server) on the same machine and for it to relay the received email to Action Mailbox (usually the raw email). The currently supported MTAs are:

To answer the specific questions about Gmail:

  1. How could they integrate that with Action Mailbox?

They couldn’t directly. They would need to also set up one of the 7 MTAs listed above and then somehow deliver the emails to that. The delivery could be accomplished with:

  • Forwarding rules managed by the user at the mailbox level
  • Dual delivery, split delivery or some other advanced routing rule managed by the admin at the domain level
  1. Would one use Gmail’s API, or would that not be appropriate for Action Mailbox?

Even if there were a way to have Gmail fire a webhook on incoming email (I’m not aware of any special delivery options outside the advanced routing rules above), there is currently no way to connect that theoretical webhook to Action Mailbox.

  1. If Gmail doesn’t work, what is different about SendGrid that makes it integrate appropriately?

Sendgrid (to use your example, the others work more or less the same way) offers an inbound mail handling API. Just as importantly, the Rails Team has built an incoming email controller to integrate with that API.

Given the lack of Gmail APIs and the lack of a Rails ingress controller, the only way I can think of that you could connect Action Mailbox to an existing Gmail mailbox would be for some other bit of code to check the mailbox, reformat the fetched email and then pose as one of the supported MTAs to deliver it to Action Mailbox.

It would be an interesting exercise, and would possibly become a popular gem, but it would very much be a kludge. A glorious kludge if done well, but a kludge nonetheless.

PayPal won’t let me login using my long, secure, unique password unless I verify using a mobile number I can’t access.

They won’t let me reset my password… without that number.

They won’t even let me use any of their 4 help methods… unless I login… with that number.


Tip for deploying a brand new Rails app

Hot tip, if you deploy a brand new Rails app to production and it doesn’t work, it might not be a problem with Ubuntu, Ansible, Capistrano, Nginx or Passenger… it might just be that it’s trying to show the “Yay! You’re on Rails!” page which only works in development 😩🙃

Before you spend hours researching and troubleshooting, throw this in your config/routes.rb file and see if it gets the site working:

  root to: -> (env) do
    [200, { 'Content-Type' => 'text/html' }, ['<h1>Hello world!</h1>']]

Goodbye Friday night ¯_(ツ)_/¯

Good rule of thumb on what software to use (from…):

Universal problem (e.g. a web server): use open source

Industry problem (e.g. SIS): use commercial off the shelf

Biz specific: build it yourself

Loving the tighter integration between MarsEdit 4.4 and, especially being able to edit pages!

The web interface is excellent but it’s still a web interface with all the usual downsides.

I just heard on a podcast that YouTube is the second biggest search engine in the world by traffic volume!!!

Before repeating, I thought I should fact check it. According to tubics it’s a myth… YouTube is “only” third biggest… behind Google and (wait for it) Google Images 🤯

I just listened to another great MicroConf talk, this time by Jason Cohen.

I’m really enjoying the MicroConf On Air podcast series. I find listening to a talk much more manageable than watching a video of one.

A Tale of Two Airlines

In February, we booked flights to Cambodia for July, on two different airlines with a few days stay in Singapore in between (this was before the whole world shut down due to COVID-19).

We are planning on moving there for two years to work in a small school and having to raise funds to support ourselves so we’re being very careful with our expenses. We booked with two budget airlines, offering cheap but generally non-transferrable and non-refundable flights. Scoot is the budget arm of Singapore Airlines while Jetstar is the budget arm of Qantas. I’ve already written in praise of Scoot and since then, Scoot has continued to exceed our expectations!!! Unfortunately we can’t say the same for Jetstar…

Scoot first contacted us in mid-March, long before our booked flight and as soon as it was clear the world was changing. They proactively offered us a free flight change (despite our fares being non-transferable). They also gave us a couple of months to decide whether to accept their offer which was VERY welcome in the midst of so much uncertainty.

In late May, Scoot contacted us again with an even more generous offer (plus multiple options to choose from). They let us choose between a 100% refund to our credit card (processed in the next 14 weeks) or a 120% refund in the form of travel vouchers (processed straight away). The travel vouchers were tempting but, given we needed to book new flights to Cambodia, we decided the refund made more sense.

In mid-June we received the full refund to our credit card, about 11 weeks earlier than promised. At this point our travel date was less than a month away and one airline had treated us wonderfully and refunded our flights whilst the other had not even made contact with us.

The next day we finally heard from Jetstar that our onward flight had been cancelled and that Jetstar was “offering all customers impacted by this announcement a Jetstar credit voucher to the full value of their untraveled booking”. Compared to Scoot, Jetstar was glacially slow, completely inflexible and totally unsympathetic.

Warning: Rant ahead

As it turns out, the Jetstar “Customer Guarantee” is worthless:

3. Our team are always here to help, 24 hours a day, 7 days a week

Nope! They’ve halved their hours and they offer no way of getting in touch other than chat. I had to wait almost 2 hours to chat to someone and then another 20 minutes to be connected to a supervisor. Overall the chat took more than 2 hours and 58 minutes.

4. We’ll let you know your choices if your flight is changed before you travel

Nope! They didn’t give us any choice and they waited months longer than a comparable airlines to let us know.

5. We’ll keep you updated and provide options if things don’t go to plan on the day

Nope! We received zero updates for three months (long after they stopped selling tickets for our flight so obviously they knew it wasn’t going to fly) and then they didn’t provide any options when our flight was cancelled.

6. You will get what you paid for


7. You can have confidence in how quickly we will respond to an issue

Nope. Not only did they not resolve my issue (did I mentionbut when I asked to register a formal complaint the supervisor refused. I later discovered he should have given me a case number and/or referred me to their call centre.

8. You can have confidence in how quickly we will refund your money

Nope. They refused to refund our money.

Fantastic talk by Joanna Wiebe on how to improve site copy!

Key takeaways for me were:
1) Listen to customers
2) Focus on each elements one job (e.g. a button’s one job is to be clicked)
3) Reduce friction and anxiety (talk about benefits, not work)

I really enjoyed this interview with… on Startups for the Rest Of Us:…)

Nearly skipped… when I saw it was about service-based architectures but I’m glad I didn’t because the principles seemed just as relevant to dealing with errors in a monolithic architecture (even if the approach might need to vary)