Files
warmbly/docs/VENDOR_LOCKIN.md
T
Matthew Meszaros 3943b8a2e7 feat: update docs for postgres-backed secrets
Refresh developer docs, deployment notes, and public-site copy to describe the Postgres-backed encrypted key and message-map model after DynamoDB removal.
2026-06-02 15:55:01 +02: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) n/a — postgres works everywhere Add dynamodb, etcd, etc. by implementing the Store interface
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.