Library / SDK
stitchfix/stitches avatar
stitchfix/stitches

stitches, the Rails engine for JSON microservices, and the header that identifies without authenticating

Create a Microservice in Rails with minimal ceremony

544 stars21 forksRubyMIT

At a glance

What is it?
stitches turns an ordinary Rails application into a microservice with API key authentication, header based API versioning and structured errors, without replacing Rails or its model layer. The part to read carefully is the calling service header, which the documentation itself calls self-declared and unsigned, and which identifies a caller without ever authorizing one.
Who is it for?
stitches suits a team that already writes Rails and wants to deploy some of its applications as services, with shared authentication, versioning and error behaviour in one gem instead of in each codebase. It does not suit a non-Rails service, a deployment whose ingress does not strip inbound headers, or a team whose services need cryptographic caller identity rather than a label.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 24 days ago.
What is it written in?
Mainly Ruby, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

A Rails engine that turns an ordinary Rails app into a JSON microservice

stitches exists to remove the ceremony that usually surrounds building a microservice in Ruby. The pitch in the documentation is that you create microservices in Rails by pretty much just writing regular Rails code, and the argument is that the gem supplies the parts Rails does not: transparent API key authentication, router level API version based on headers, a way to document endpoints through acceptance tests, and structured errors that can be built from invalid Active Record objects, from exceptions, or by hand.

The reason that works is that Rails already knows how to render JSON, and already has routing, models, controllers and migrations. So the microservice is not a new architecture. It is the same application you would write for a server rendered site, with the views left out. You write the controller, you return JSON, and the gem handles who is allowed to call and which version of the route they are reaching.

The gem is a Rails engine, and it is published as a Ruby gem under the MIT licence. The recent releases are v5.1.0 on 2026-06-26, v5.1.1 on 2026-06-30 and v5.2.0 on 2026-07-15, and the last push was on 2026-09-29.

Who it is for is a team that has already decided to run several Rails applications as services and wants the shared plumbing in a gem rather than in each application's own code, while keeping the ergonomics of Rails controllers and models rather than switching to a lighter framework.

Installation is a gem line and three generators

The setup is short enough to read in one go. First the gem goes into the Gemfile:

ruby

ruby gem 'stitches'

code

Then the bundle is installed, and then the application is set up with generators:

bash

bash > bin/rails generate stitches:api > bin/rails generate stitches:api_migration # only if you're using API key authentication > bundle exec rake db:migrate # only if you're using API key authentication

code

The comments in that block carry the important detail. The stitches:api generator sets up the API side of the application, while stitches:api_migration and the following db:migrate step are only needed if you are using the API key authentication feature. That means a service that authenticates some other way, for example with JWT, does not need the api_clients table at all.

The generator names are also the API of the gem. Anything named stitches: follows the standard Rails generator convention, so the namespace tells you the feature is stitches and the suffix tells you what it produces. If you later need to add a column to the api_clients table, the upgrade notes in the documentation name the generators for exactly that, such as stitches:add_enabled_to_api_clients, stitches:add_deprecation and stitches:add_disabled_at_to_api_clients, with the set you need depending on which version you are upgrading from.

The repository layout is what you would expect for a gem: lib/ for the engine and its railtie, spec/ for the test suite, a stitches.gemspec, a Rakefile, a .circleci/ directory for continuous integration, a .env.example, an owners.json and a build-matrix.json.

Disabling API key auth entirely, or per request

Not every service needs key authentication, and the gem lets you turn it off. The configuration is a single setting:

ruby

ruby Stitches.configure do |config| config.disable_api_key_support = true end

code

The more interesting option is that the setting can also be a callable. The documentation describes accepting a lambda, a proc, or any object responding to #call, which receives the Rack env hash and should return a truthy value to skip auth:

ruby

ruby Stitches.configure do |config| config.disable_api_key_support = ->(env) { env['HTTP_HOST'].to_s.include?('.int.example.com') } end

code

The documented use case is a service that serves both internal and external traffic on different hostnames, so the same deployment can skip auth for an internal host and require it for everything else, without a separate deployment for each.

That is a sensible design and it has a consequence worth stating plainly. A decision that depends on the request host means the security boundary now partly lives in the network and in the routing layer in front of the application. If a request can reach the service with a host header it should not have, the callable decides the wrong way. The gem gives you the hook, but it does not verify that the host is trustworthy.

Calling service identity is a header, and the documentation says so bluntly

When API key auth is off, a service loses the ability to know who is calling it. Stitches 5.1 and later add two mechanisms to get some of that back, and the documentation is unusually direct about their limits.

The header is named X-StitchFix-Calling-Service by default and is configurable. The automatic path is CallingServiceMiddleware, registered through the railtie, which runs after the ApiKey middleware. When no auth middleware has populated the caller identity environment variable, named Stitches.configuration.env_var_to_hold_api_client, it reads the calling service header and fills that variable with a CallingServiceClient struct. The documentation's emphasis is that existing code reading the identity object's .name keeps working, with the value now coming from the header rather than a database lookup.

The struct is deliberately thin. CallingServiceClient implements .name as the header value, and .id and .key as nil.

The resolution order for the environment variable is explicit: an ApiKey middleware that authenticated a key gives an ApiClient record with .name, .id and .key; a JWT or other middleware that set the variable gives that object, for example a user; if neither ran but the calling service header is present, the CallingServiceClient struct is used; otherwise it is nil. Middleware ordering is ApiKey, then CallingServiceMiddleware, then ValidMimeType.

The opt-in path is the CallingServiceName concern, for cases where you want the header value strictly, such as metrics tags where a human user's name must never appear:

ruby

ruby class Api::ApiController < ActionController::API include Stitches::CallingServiceName end

# Returns the header value only, empty string if absent: calling_service_name # => "my-app" or ""

code

The comment in that snippet says it returns the header value only, an empty string if absent.

The security section: identification is not authorization

The documentation has a security section, and it is the part of the README most worth reading in full, because it describes a mechanism that can be misused in a way that is easy to miss.

The header is described as a self-declared, unsigned header. Any caller that can reach your service can set it to any value. From that follow four instructions: do not use it as a sole authorization mechanism for sensitive operations unless the network layer guarantees only the legitimate service can reach the endpoint; strip the header at public ingress points such as Traefik, an ALB or an API gateway, to stop external callers spoofing internal service identities; the header replaces API key based identity, not authorization, and if you need to verify the caller's identity cryptographically you should use mTLS or a service mesh AuthorizationPolicy.

The documentation then lists safe uses and unsafe uses, which is the clearest way to think about it. Safe: stats tagging, logging, updated_by audit fields, routing hints, and non security critical behavioural branching. Unsafe without network enforcement: access control decisions, privilege escalation gates, and bypassing user facing safety flows.

So the useful framing is that this header is metadata, and the gem is explicit that it must not be treated as an authentication decision. The header name is configurable, and changing it is a one line configuration change:

ruby

ruby Stitches.configure do |config| config.calling_service_header = "X-My-Custom-Header" end

code

A team that renames the header should also remember that the stripping rule at the ingress point has to match the new name.

Caching and key deprecation leniency from version 4.0.0

Two behaviours are documented for teams upgrading, and both are about not breaking live traffic during a key rotation.

The first is an in-memory cache, available when upgrading to version 4.0.0 and above, enabled with two settings:

ruby

ruby Stitches.configure do |config| config.max_cache_ttl = 5 # seconds config.max_cache_size = 100 # how many keys to cache end

code

The values are documented in the comments: the TTL in seconds and how many keys to cache. That is a small, bounded cache, which tells you something about the intent, which is to absorb repeated lookups rather than to hold a large working set.

The second is leniency for disabled API keys, which lets old keys keep working for a window after they have been disabled, as long as the window is not exceeded. The configuration is:

ruby

ruby Stitches.configure do |config| config.disabled_key_leniency_in_seconds = 3 * 24 * 60 * 60 # Time in seconds, defaults to three days config.disabled_key_leniency_error_log_threshold_in_seconds = 2 * 24 * 60 * 60 # Time in seconds, defaults to two days end

code

The comments give the defaults: three days for the leniency window and two days for the error log threshold. The behaviour is that a disabled key used within the leniency window is allowed, and that if the disabled_at field is not populated the behaviour stays as it always was, with the request denied when the enabled field is set to true.

The logging is a nice piece of design. Any use of a disabled key generates a log, and the severity depends on how long ago the key was disabled. Before the threshold it is a warning, after the threshold it is an error. The documentation also states that the error log threshold should never be greater than the leniency window, so that you get an escalating series of warnings before access is finally refused. That is a rotation safety valve, and it is worth knowing it exists, because a team that finds disabled keys still working in production has usually found this setting rather than a bug.

Where stitches is the wrong tool

The first wrong case is a service that is not Rails. The gem is a Rails engine, it relies on the railtie, the generators produce files for a Rails application, and the whole premise is that you write ordinary Rails code. If your service is a Go or Python application, there is nothing here to use.

The second wrong case is a team that wants API documentation generated from the code. The documentation says the gem gives a way to document your microservice endpoints via acceptance tests, which is a specific approach: your docs come from tests that exercise the endpoints. That is a good approach for keeping docs honest, but it means the documentation artefact is tied to running your test suite, and it is not the same thing as an OpenAPI document generated from a schema.

The third wrong case is a deployment where the ingress point does not strip headers. The calling service identity feature is safe by construction only if the network layer enforces who can reach the service, and the documentation names specific infrastructure for that, Traefik, an ALB or an API gateway. In a cluster where any pod can call any service, an unsigned header is an invitation to spoof, and the gem says so.

The fourth wrong case is a service with no database and no need for API key auth, where the extra generators and the api_clients table are overhead rather than value. The gem supports disabling the feature, but the code and the migration are still in the picture.

The fifth wrong case is expecting this to replace a service mesh. It identifies a caller from a header it does not verify, and it makes no attempt to establish who is really on the other end. Cryptographic identity is mTLS or a mesh policy, as the documentation itself says.

Stitches against hand rolling the same plumbing

The alternative is to write the same pieces into each service yourself, or to adopt a framework built for APIs rather than for Rails.

Hand rolling means each service gets its own authentication filter, its own version routing logic, its own error rendering and its own key rotation handling. You write that code once and then you maintain it once per service, and every service drifts a little. The gem's argument is the opposite one: put the shared behaviour in a dependency and let the services keep only their own domain code.

The cost of the gem is that you inherit its opinions. The middleware order is fixed, the identity resolution order is fixed, the header name is a convention, and the key rotation behaviour is a specific leniency window with a specific logging policy. A team whose services need a different shape of authentication, or who already run a mesh that handles identity, is paying for features it will not use and inheriting a resolution order it does not control.

A framework built for APIs is the other comparison. Such a framework starts from the assumption that you are building a service, with routing, serialization and error handling designed for that. Rails starts from the assumption that you are building a web application, and stitches adds the service specific parts. If your team already writes Rails controllers by hand and likes the Active Record and model layer, that assumption is an advantage rather than a handicap, since you can keep the parts you know.

So the honest summary is that stitches is a good fit when Rails is already your framework of choice and the microservices are a new deployment shape for existing applications. If the microservices are the reason you chose your stack, the fit is weaker.

Editorial conclusion

stitches suits a team that already writes Rails and wants to deploy some of its applications as services, with shared authentication, versioning and error behaviour in one gem instead of in each codebase. It does not suit a non-Rails service, a deployment whose ingress does not strip inbound headers, or a team whose services need cryptographic caller identity rather than a label. Before you adopt the calling service feature, decide where the header will be stripped, because the documentation states that any caller able to reach the service can set it, and treat the API key path as the one that actually authenticates. If you are upgrading from an older version, check which of the stitches:add_enabled_to_api_clients, stitches:add_deprecation and stitches:add_disabled_at_to_api_clients generators your starting point requires, and set the disabled key leniency window deliberately rather than leaving the three day default in place.

Frequently asked questions

How do I install the stitches gem and set up a microservice?

Add gem 'stitches' to the Gemfile, run bundle install, then run bin/rails generate stitches:api. If you are using API key authentication you also need bin/rails generate stitches:api_migration followed by bundle exec rake db:migrate.

Can stitches be used without API key authentication?

Yes. Set config.disable_api_key_support = true in the Stitches.configure block, or pass a callable that receives the Rack env hash and returns a truthy value to skip auth per request, for example when a service serves internal and external traffic on different hostnames.

What is the X-StitchFix-Calling-Service header and is it safe to authorize on?

It is a self-declared, unsigned header that identifies the calling service, and the documentation is explicit that it replaces API key based identity, not authorization. Any caller that can reach the service can set it, so the guidance is to strip it at public ingress points, never use it as a sole authorization mechanism, and use mTLS or a service mesh AuthorizationPolicy when cryptographic identity is required.

How do I customize the calling service header name?

Set config.calling_service_header in the Stitches.configure block, which defaults to X-StitchFix-Calling-Service. The documentation also notes that the header name should match the rule you apply at your ingress point.

What does the disabled key leniency setting do?

It lets an old API key keep working for a window after it has been disabled, as long as the disabled_at field is set and the window is not exceeded. The defaults are three days of leniency and a two day error log threshold, and any use of a disabled key is logged, as a warning before the threshold and an error after it.

Official sources

  1. Issues
  2. License: MIT
  3. README
  4. Releases
  5. stitchfix/stitches on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/stitchfix-stitches.svg)](https://hysenlabs.com/projects/stitchfix-stitches)

Was this useful?