Files
warmbly/docs/VENDOR_LOCKIN.md
T
Matthew Meszaros 13c4ebb7a6 rename web-admin -> admin
Shorter, cleaner path. The 'web-' prefix was redundant given the dir
sits at the repo root next to web/ and is unambiguously the admin web
app. Git tracked the rename so blame + history follow through to the
new location.

Updated README.md and docs/VENDOR_LOCKIN.md references plus the package
README header. No code changes.
2026-05-27 16:17:47 +00:00

10 KiB

Vendor Lock-in Audit

Honest accounting of where Warmbly does and doesn't lock you into a specific cloud / SaaS provider. Status as of the pluggable-backends refactor.

TL;DR

  • Workers: zero vendor lock-in. A worker runs on any Linux box with outbound HTTPS + SMTP/IMAP. No AWS, no GCP, no Stripe SDK, nothing.
  • Backend: mostly self-hostable. Six pluggable interfaces (KMS, encrypted keys, blob store, event bus, codec, cache) all ship with self-hostable implementations. Three remaining concerns still pin to specific vendors: task scheduling, captcha, and payments. All three are optional in the sense that you can run the platform with them stubbed; payments + captcha are only needed if you're running a public SaaS.

The pluggable layer (no lock-in)

Concern Interface Self-host default AWS / cloud option Other options
KMS / root key kms.Provider local (AES-256-GCM, key from env/file) aws-kms Add vault, gcp-kms, azure-keyvault by implementing the interface
Encrypted DEKs encryptedkeys.Store postgres (backend) / http (worker) dynamodb ScyllaDB Alternator via DynamoDB API; add etcd etc.
Blob storage storage.Store filesystem s3 (AWS) One s3 impl also works for MinIO, Cloudflare R2, Backblaze B2, Hetzner Object Storage — set AWS_ENDPOINT_URL_S3
Event bus eventbus.EventBus nats (JetStream) kafka Redis Streams, etc. by implementing the interface
Codec codec.Codec json avro (requires Schema Registry) Protobuf etc.
Cache (*cache.Cache) Redis (any Redis-compatible: Valkey, KeyDB) Redis Could be abstracted further if needed
Internal HTTP auth bearer token Self-issued via openssl rand -base64 32 n/a JWT-per-worker is the planned follow-up

Selection is per-process via env vars (KMS_PROVIDER, BLOB_PROVIDER, EVENTBUS_PROVIDER, etc.). Every selector has a self-hostable default; AWS is not the implicit fall-through except for KMS where the historical default is preserved for the hosted Warmbly install.

See docs/INTERNAL_API_AUTH.md for the worker → backend auth model.

What's still vendor-pinned

1. Task scheduling: Google Cloud Tasks

What: internal/infrastructure/gtasks/gtasks.go schedules deferred work (send this email at 9am, retry this in 30 min, etc.).

Why it matters: Required by the backend if you want scheduled cold campaigns and warmup pacing.

Self-hostable today? Partial. The local dev stack uses google/cloud-tasks-emulator (open-source emulator) so you can run end-to-end without GCP. The same emulator works in production if you self-host it (a Java service from Google's own repo).

True lock-in? Yes for production hosted on cloud. A native replacement would be:

  • Embed a simple time-wheel scheduler in the backend using Postgres for state
  • Use a job queue like River, Asynq, or Faktory
  • Use Postgres LISTEN/NOTIFY + a polling worker

Any of these would be ~1 week of focused work. Not done because the existing GCP emulator is a "good enough" self-host path for now.

2. Captcha: Cloudflare Turnstile

What: internal/pkg/captcha/turnstile.go protects signup / login / password reset against bot abuse.

Why it matters: Only relevant for public SaaS where you accept signups from the world. An internal-only Warmbly instance (e.g., one agency, one team) doesn't need it.

Self-hostable today? Effectively yes if you set AUTH_TURNSTILE_BYPASS_TOKEN to a value your forms always send — the captcha check passes through. Suitable for trusted single-tenant deploys.

True lock-in? Soft. Cloudflare Turnstile is free and works against any origin. If you genuinely need a captcha and don't want CF, replace it with hCaptcha or a self-hosted alternative like ALTCHA — same shape, different SDK.

3. Payments: Stripe

What: internal/app/stripe/service.go and related — subscription plans, billing portal, webhook handling.

Why it matters: Only relevant for SaaS that charges money. A self-hosted Warmbly that's free or licensed by lump sum doesn't need it.

Self-hostable today? Yes by not configuring Stripe credentials — the service no-ops on missing config and the relevant admin pages hide.

True lock-in? Soft. Same shape applies — swap Stripe for LemonSqueezy, Paddle, or your own billing if you want. The repo doesn't go out of its way to be Stripe-specific outside of internal/app/stripe/.

4. Secrets loader: AWS Secrets Manager / SSM

What: cfg.LoadXxx() calls in internal/config/ pull config from AWS Secrets Manager / SSM. Used for production hosted Warmbly so secrets don't sit in env files.

Self-hostable today? Yes — all callers fall back to plain env vars when the AWS lookup fails. Self-hosters set env vars; AWS-hosted reads from Secrets Manager. Same code path, different source.

True lock-in? None for self-hosters. The AWS path exists, but it's not required.

5. Geo IP database

What: internal/infrastructure/geo/ loads a MaxMind-format GeoIP DB for IP-based location lookups (admin views, abuse detection).

Self-hostable today? Yes. Operator drops a MaxMind GeoLite2 database (free) at the configured path. Works fully offline.

True lock-in? None. Open data format, multiple sources.

6. Sentry (error reporting)

Self-hostable today? Yes. Sentry has a self-host offering. Set SENTRY_DSN to point at it or leave it unset to disable.

True lock-in? None.

What a fully self-hosted Warmbly looks like

Minimum viable stack (no clouds required):

┌────────────────────────────────────────────────────────────────┐
│ Single VPS or small fleet                                      │
│                                                                │
│ ┌──────────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐    │
│ │   Backend    │  │ Consumer │  │  Worker  │  │ Tracking │    │
│ │   (Go)       │  │  (Go)    │  │  (Go)    │  │  (Rust)  │    │
│ └──────┬───────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘    │
│        │               │              │              │         │
│        ├───────────────┴──────┬───────┘              │         │
│        │                      │                      │         │
│        ▼                      ▼                      ▼         │
│ ┌──────────────┐  ┌─────────────────┐  ┌─────────────────────┐│
│ │  PostgreSQL  │  │      NATS       │  │  Local filesystem   ││
│ │  (primary    │  │   JetStream     │  │  (blob store) +     ││
│ │   DB +       │  │   (event bus)   │  │   AES key file      ││
│ │   DEK store) │  │                 │  │   (local KMS)       ││
│ └──────────────┘  └─────────────────┘  └─────────────────────┘│
│                                                                │
│        ┌─────────────────┐                                     │
│        │      Redis      │  ← cache (any Redis-compatible)     │
│        └─────────────────┘                                     │
└────────────────────────────────────────────────────────────────┘

      Outbound: SMTP/IMAP to mailbox providers + HTTPS to AI APIs
      No required calls to AWS / GCP / Stripe / Cloudflare

Env-var configuration for this stack:

KMS_PROVIDER=local
KMS_LOCAL_MASTER_KEY=$(openssl rand -base64 32)

ENCRYPTED_KEYS_PROVIDER=postgres   # backend
ENCRYPTED_KEYS_PROVIDER=http       # worker
ENCRYPTED_KEYS_BACKEND_URL=https://api.yourdomain.com
ENCRYPTED_KEYS_WORKER_TOKEN=...    # same as INTERNAL_API_TOKEN
INTERNAL_API_TOKEN=$(openssl rand -base64 32)

BLOB_PROVIDER=filesystem
BLOB_FS_ROOT=/var/lib/warmbly/blobs

EVENTBUS_PROVIDER=nats
NATS_URL=nats://nats.local:4222

CODEC_PROVIDER=json   # if you don't want Schema Registry

# Optional / for SaaS only:
# STRIPE_SECRET_KEY=...
# AUTH_TURNSTILE_SECRET=...
# CLOUD_TASKS_QUEUE_NAME=...

Status by component

Component Self-host ready? Notes
Backend API (Go) yes Run on any Linux/Docker host
Consumer (Go) yes Same
Worker (Go) yes No DB, just outbound HTTPS + SMTP/IMAP
Tracking (Rust) yes Standalone HTTP service
Realtime (Elixir) yes Standalone Phoenix service
Admin UI (admin/) yes Vite build, deploy as static files
Dashboard (web/) yes Same
Postgres yes Any 14+ install
Redis yes Or Valkey, KeyDB
NATS JetStream yes Single binary, ~20MB
Kafka (optional) yes For Avro / Schema Registry preference
KMS yes Local impl; AWS/GCP/Vault by config
Blob store yes Filesystem; or any S3-compatible
Encrypted DEK store yes Postgres
Task scheduling ⚠ partial Needs Cloud Tasks emulator self-hosted
Captcha ⚠ optional Bypass-token mode for trusted deploys
Payments ⚠ optional Stripe — only if you charge
Sentry ⚠ optional Or self-hosted Sentry, or off

The "⚠ partial / optional" rows are the only places a clean-room self-hoster has to make a deliberate operational choice. Everything else just works.