Building a Rails marketplace MVP for sport schools: Elasticsearch, Stripe and a small budget

ekoodorailselasticsearchstripestartup

A year ago I started eKoodo, a marketplace that connects people with good sport schools and instructors around the world. You want to learn kitesurfing, or finally stop falling on your snowboard, and you want someone decent to teach you. We try to help you find them and book them.

This post is about how the first version got built. Not a success story (it is way too early for that), just what we picked, why, and a few things I would tell myself one year ago.

The constraints

Money was limited and time was short. The tech team was two freelance programmers, plus a freelance UX/UI designer. I am not the one writing most of the code: my job is mostly running operations, talking to schools, preparing decks and doing whatever nobody else has time for. Everybody does a bit of everything.

With a setup like this you cannot afford to build the perfect platform. You need something online that real people can use, so you can learn if the idea holds up. That is the whole lean startup thing, and honestly it is less a philosophy than a necessity when the bank account decides for you.

Why Rails

For a rails marketplace mvp the choice was not really a debate. A few reasons:

  • Speed. Rails gives you users, forms, admin pages, emails and a database schema in days, not weeks.
  • Gems for almost everything we needed: authentication, image uploads, search, payments.
  • It is easy to find freelancers who know it, and easy for a new one to open the project and understand where things are.
  • Convention over configuration means fewer long discussions about folder structure. We had no time for those.

On the frontend we kept it boring: server rendered views and jQuery. No single page app. Nobody ever booked a surf lesson because the page didn't reload.

Everything runs on AWS. It is not the cheapest option at our size, but it means we will not have to migrate when (if!) traffic grows.

Search with Elasticsearch

Search is the core of the product. A user comes with three questions: which sport, where, and when. The "where" part is the tricky one, because people search for a spot or a region, not an exact address, and they want results around it.

We could have done it in PostgreSQL with some geo extension, but we also wanted full text search on school descriptions, filters on sports and levels, and room to play with ranking later. So we went with Elasticsearch, using the elasticsearch-model and elasticsearch-rails gems (the old Tire gem is retired, so no point starting with it). The elasticsearch rails integration is pretty painless: you include a module in your model, define a mapping and how the record is serialized, and callbacks keep the index in sync.

A simplified version of what a school looks like on the index side:

ruby
class School < ActiveRecord::Base
  include Elasticsearch::Model
  include Elasticsearch::Model::Callbacks

  settings do
    mappings do
      indexes :name,        type: 'string'
      indexes :description, type: 'string'
      indexes :sports,      type: 'string', index: 'not_analyzed'
      indexes :location,    type: 'geo_point'
    end
  end

  def as_indexed_json(options = {})
    {
      name: name,
      description: description,
      sports: sports.map(&:slug),
      location: { lat: latitude, lon: longitude }
    }
  end
end

And a search for kitesurf schools within 50 km of a point:

ruby
School.search(
  query: {
    filtered: {
      query:  { match_all: {} },
      filter: {
        and: [
          { term: { sports: 'kitesurf' } },
          { geo_distance: { distance: '50km', location: { lat: lat, lon: lon } } }
        ]
      }
    }
  }
)

Two lessons here. First, mark fields like sports as not_analyzed, otherwise the analyzer splits your slugs and filters behave in strange ways. Second, the geocoding of schools matters more than the query. Half of our "search bugs" were schools with a pin in the wrong place, often in the sea. Which, for a kitesurf school, is almost correct.

Payments with Stripe

Stripe is still pretty young, and compared to the payment gateways I have seen before it feels like it was designed by people who actually had to integrate one. The docs are clear, the test mode works, and the Ruby gem does what you expect.

The basic flow is simple: Stripe.js turns the card into a token in the browser, so card data never touches our servers, and on the backend we create the charge.

ruby
charge = Stripe::Charge.create(
  amount:      booking.amount_cents,
  currency:    booking.currency,
  source:      params[:stripe_token],
  description: "eKoodo booking ##{booking.id}"
)

The part that is not simple is the marketplace side. With stripe marketplace payments you are not just charging a customer, you are handling money that partly belongs to a school, with commissions, refunds when the wind doesn't show up, and different countries and currencies. Stripe is moving fast on this and some pieces are still evolving, so we are keeping our own logic as thin as possible and I expect to rewrite parts of it. I am fine with that.

What I would do differently

We built a few features nobody asked for, like detailed instructor profiles with a lot of fields that almost no school fills in. Meanwhile schools kept asking for a simple way to update availability, which we underestimated. Talking to schools every week taught us more than any feature we shipped on assumptions.

I would also set up proper analytics from day one. For the first weeks we were guessing what users did on the site, and guessing is expensive when every developer hour counts.

The product is changing almost every week now, small releases, look at the numbers, talk to people, repeat. Some of the code above will probably look embarrassing in six months. That is kind of the point.