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.
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:
organization_id and rooftop_id, so it is tenant-scoped, searchable and
survives leaving the provider.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-hosted | Garage 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. |
|---|---|
| Hosted | AWS 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.
POST /uploads for a file name, size and type.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.
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.
| Hosted | Postmark (transactional, strong deliverability), Amazon SES (cheapest at volume), Resend, Mailgun. |
|---|---|
| Self-hosted | Postal is open source and capable, but running our own outbound reputation is real work; worth it only at volume. |
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.
| Hosted | Twilio, Telnyx, Plivo, Vonage, Amazon End User Messaging (SNS). |
|---|
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 apps | APNs and FCM, sent from our own server (Rails' Action Push Native does this, as do
gems like rpush). |
|---|---|
| Browsers | Web Push with VAPID keys, an open standard, sent with the web-push gem.
No third party involved. |
| Brokers | OneSignal and similar are fine if we need their extras, behind the same adapter. |
noticed gem fits this well), so the channel adapters stay simple.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).
| Gateways | Stripe (the default), Braintree, Adyen. Paddle or Lemon Squeezy as merchant of record when we would rather they handle sales tax and VAT. |
|---|---|
| Ruby | The pay gem already puts Stripe, Paddle, Braintree and Lemon Squeezy behind
one interface in Rails. |
| Self-hosted billing | Lago or Kill Bill (open source) for plans, usage metering and invoices, still charging cards through a gateway. |
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.