Outside services

Email, text messages, push notifications, card payments and file storage are the jobs our apps can't do alone: they need a mail reputation, a phone carrier, Apple and Google, a card network, or a lot of disk. We rent those, but on our terms. Each one sits behind an adapter so we can switch providers, and files go straight from the browser to storage instead of through our servers. This page describes the approach; nothing here is signed up for yet.

1. The same shape for every service

Every outside service gets one small interface in our code and one class per provider, picked by an environment variable. Rails already ships this shape for several of them (Action Mailer delivery methods, Active Storage services), so where it does, we use it rather than invent our own.

Sms.deliver(to: "+15551234567", body: "Your order is ready")
# SMS_PROVIDER=twilio | telnyx | log

The rules that come with that shape:

2. Files: S3-compatible storage, uploaded directly

The S3 API is the closest thing storage has to an open standard. Speaking only that API lets the same code use AWS or a server we run ourselves:

Self-hostedGarage or SeaweedFS (small, open source, run well on our own disks); Ceph's object gateway if we ever need a large cluster. MinIO is well known, but its open source edition has been scaled back, so it is no longer our first pick.
HostedAWS S3, Cloudflare R2 (no download fees), Backblaze B2, Wasabi.

Uploads skip our servers. The app asks our server for permission, the server answers with a short-lived presigned URL, and the browser, phone app or script sends the bytes straight to storage. Our processes stay small and fast, and a 2 GB video never ties up a web worker. Rails does this out of the box with Active Storage direct uploads; elsewhere we sign the request with the aws-sdk-s3 gem, which works against any S3-compatible endpoint.

  1. The client asks POST /uploads for a file name, size and type.
  2. The server checks the user may upload there and returns a presigned PUT URL, or a presigned POST whose policy caps the size and pins the content type, valid for a few minutes.
  3. The client uploads directly to storage.
  4. The client tells the server it is done; the server checks the object exists, records it, and queues any scanning, resizing or transcoding as a background job.

Keys are tenant-scoped. The server, never the client, chooses the object key, and every key starts with the tenant, so one prefix holds everything a rooftop owns and nothing else:

org/<organization_id>/rooftop/<rooftop_id>/<uuid>/invoice.pdf

Buckets are private. Downloads use presigned GET URLs that expire in minutes, issued only after our own authorization check (see Identity). Bucket CORS allows only our origins, and a lifecycle rule deletes uploads that were started but never confirmed.

3. Email

SMTP is an open protocol, so the simplest adapter is just an SMTP setting, and Action Mailer covers the rest. Delivering mail is easy; landing in the inbox is the hard part, which is why this is one we usually rent.

HostedPostmark (transactional, strong deliverability), Amazon SES (cheapest at volume), Resend, Mailgun.
Self-hostedPostal is open source and capable, but running our own outbound reputation is real work; worth it only at volume.

4. SMS

Text messages have to go through a carrier, so there is no self-hosted option worth having. Providers are interchangeable at the API level; the differences are price per message and coverage.

HostedTwilio, Telnyx, Plivo, Vonage, Amazon End User Messaging (SNS).

5. Push notifications

Push to phones must go through Apple (APNs) and Google (Firebase Cloud Messaging); there is no way around them. We talk to them directly rather than through another broker, so there is one less company in the chain.

Native appsAPNs and FCM, sent from our own server (Rails' Action Push Native does this, as do gems like rpush).
BrowsersWeb Push with VAPID keys, an open standard, sent with the web-push gem. No third party involved.
BrokersOneSignal and similar are fine if we need their extras, behind the same adapter.

6. Card billing

Card payments are the one service where we keep the provider at arm's length on purpose: we never see, send or store a card number. The provider's hosted checkout or card fields collect it, which keeps our PCI scope to the smallest form (SAQ A).

GatewaysStripe (the default), Braintree, Adyen. Paddle or Lemon Squeezy as merchant of record when we would rather they handle sales tax and VAT.
RubyThe pay gem already puts Stripe, Paddle, Braintree and Lemon Squeezy behind one interface in Rails.
Self-hosted billingLago or Kill Bill (open source) for plans, usage metering and invoices, still charging cards through a gateway.

7. The rules

These rules are section 16 of AGENTS.md, which every coding agent here follows.


Provided as is, for information only, with no warranty and no liability. Not legal or professional advice. Security and radio material is for authorized, lawful use only. Full disclaimer.

Visits are logged on our own server: IP address, browser and device details, and pages viewed. We use them to understand traffic and spot attacks, and never share or sell them.