Skip to content

Cloud strategy — Hetzner-first, AWS-by-trigger

Start fully on Hetzner. Adopt AWS managed services later, one at a time, only when a concrete scale or reliability signal makes the managed version worth its premium. This is a deployment decision, not an architecture change — every cloud-specific concern already sits behind a gateway/driver, so "move it to AWS" is a swap, not a rewrite.

Where P0 actually stands

Nothing below has been reversed — the strategy holds, and the installation is still 100% Hetzner for compute. Two rows of the seam table have moved on, and both moved away from AWS rather than toward it:

  • Secrets KEK is no longer a hand-written Kubernetes Secret. It lives in HashiCorp Vault on the control cluster and reaches the api through the External Secrets Operator (Agentfy/gitops, helm/agentfy-api/values.dev.yamlsecrets.vaultKey: dev/agentfy-api). The KMS trigger at P3 is unchanged; the thing KMS would replace is now Vault.
  • Object storage is decided in favour of Hetzner: terraform/storage.tf creates one private, versioned bucket per contour, and AWS S3 is kept for Terraform state and backups only. The code side has not caught up — the only storage driver in this repository writes to a local path.

Which repository declares what: Two repositories.

Thesis: own compute, rent reliability

The whole point of Agentfy.ai 2.0 is cheap ephemeral compute — an agent is a DB row, the runtime is a k8s Job that scales to zero. That economic model only works on Hetzner-class pricing; running bursty agent Jobs on EKS + EC2 would destroy it. So:

  • Compute, k8s, the worker Jobs (incl. their in-pod browser), egress → stay on Hetzner indefinitely. This is where Hetzner's ~10× price/performance advantage lives, and it's the load that scales.
  • AWS is adopted only for managed state, durability, keys, and deliverability — the places where AWS's reliability/compliance is genuinely worth paying for — and only when self-hosting that one piece becomes the bottleneck or the on-call liability.

We rent reliability (managed Postgres failover, KMS HSM keys, S3 durability, SES deliverability) selectively; we own the compute that defines our margin.

Why start on Hetzner

  • Cost — ~10× cheaper compute + egress; pre-revenue and early-revenue margins survive.
  • EU / GDPR — data stays in EU; co-located low-latency cluster.
  • Simplicity — one k3s cluster, one bill, no IAM/VPC sprawl while the product is still moving.
  • Already abstracted — the codebase isolates every cloud concern behind an interface, so starting Hetzner-only costs us nothing in future optionality (see next section).

The seam that makes this safe

Per CleanSlice conventions, external systems are adapters/submodules behind a gateway (infra drivers and system gateways). Each cloud-replaceable concern has exactly one swap point:

ConcernInterface / seamHetzner default (MVP)AWS option (later)
Object storageinfra/storage driver (S3 API)Hetzner Object Storage / R2S3
Secrets KEKsystem secret gateway (wrap/unwrap)KEK in a k8s SecretKMS (wrap DEKs only)
Emailnotification/email IEmailGatewayResendSES
Relational/vector stateinfra/prismaself-host Postgres (pgvector + AGE)RDS/Aurora (app data only)
LLMsystem/llm gatewayClaude API (external — never AWS Bedrock)

Because each is a single implementation behind a stable interface, a migration is a new driver + a data move + a config flip — never a refactor. That's why we can safely defer all of it.

Phases & triggers

Bands are rough and signal-driven — the signal is what triggers a move, the user count is just the band where that signal usually appears. Don't migrate on the number; migrate on the pain.

PhaseRough bandSignal that opens itWhat changes
P0 — Launch0 → ~1k users / pre-PMFshipping the MVP100% Hetzner. Self-host everything; Resend; KEK in k8s Secret; Hetzner OS.
P1 — Traction~1k → 10k / first revenueops toil on a single component exceeds its valueAdopt the cheap managed conveniences that remove toil: object storage → S3/R2 if you need CDN/durability SLAs; email → SES if volume/deliverability demands it. Compute stays put.
P2 — Scale & SLAs~10k → 100k / paid SLAs, on-call painself-hosting stateful systems becomes the bottleneck/riskMove app Postgres → RDS/Aurora for automated HA failover + PITR + read replicas. Keep the LightRAG/AGE Postgres self-hosted (managed PG blocks the AGE extension).
P3 — Enterprise / compliance / global100k+ / SOC2, enterprise, multi-regioncompliance or global latency requirementsKMS for HSM-backed KEK + audited rotation; managed vector/graph at huge scale (OpenSearch / Neptune / Qdrant Cloud); multi-region reads.

What stays on Hetzner — permanently

These are not migration candidates; moving them to AWS would raise cost without buying reliability we need:

  • k3s cluster, core pool (api/app/admin/LightRAG).
  • The workers pool + ephemeral worker Jobs (with the in-pod headless browser) — the cost-sensitive, scale-out load.
  • Egress / bandwidth — Hetzner's flat, generous egress vs AWS metered egress is a decisive margin lever.
  • Redis (queue/pub-sub/locks) — cheap to self-host; managed only if Redis ops ever dominate.

What can migrate, and exactly when

ComponentDefaultMigrate to AWS when…One-way door?
Object storageHetzner OS / R2 (S3-compatible)need CDN, multi-region, or contractual durability/complianceNo — both speak the S3 API; bucket sync + config flip.
EmailResendper-email cost dominates at high volume, or deliverability/reputation needs SESNo — swap IEmailGateway + DNS records.
App Postgresself-host (CNPG)you need automated failover + PITR + read-replica fan-out and DBA toil > managed billSoft — logical replication cutover; plan a maintenance window. Split out LightRAG/AGE PG first.
Secrets KEKk8s SecretSOC2/enterprise demands HSM-backed keys + audited rotationNo — values stay in PG; re-wrap DEKs, bump kekVersion.
Vector / graph (huge scale)pgvector + AGEa single base outgrows pgvector/AGE (benchmark first)Soft — re-embed/rebuild indexes (derived data, not migrated).

Caveat that shapes P2: LightRAG's graph store needs Apache AGE, which managed Postgres (RDS/Neon) does not allow. So the moment app-data moves to RDS, you already run two Postgres (app on RDS, LightRAG/AGE self-hosted) — or move the graph to Neo4j Aura. Decide this at P2, not by accident.

Migration mechanics (why each cutover is low-risk)

  • Storage: Hetzner OS and S3/R2 are all S3-API → rclone/lifecycle sync + change one driver config + env. No application code change.
  • Email: implement the SES IEmailGateway, verify domain/DKIM, flip the binding. Rollback = flip back.
  • KEK → KMS: the secret gateway wraps/unwraps DEKs; KMS only changes how the DEK is wrapped. Encrypted values stay in Postgres untouched; re-wrap under a new kekVersion, keep the old for decrypt during rollover.
  • Postgres → RDS: standard logical-replication/pg_dump cutover behind infra/prisma; the schema and app are identical. Split the AGE/LightRAG database out before the move.
  • Vector/graph: indexes are derived — never "migrated". Stand up the new backend and re-embed from the source markdown/documents; flip retrieval when caught up.

Cost note

Hetzner keeps fixed infra cost low through P0–P1 while revenue is thin. Each AWS adoption is a deliberate trade of money for managed reliability at a point where the reliability is worth more than the cash — never a default. The bill grows with the obligations (SLAs, compliance), not ahead of them.

Guardrails (non-goals)

  • Don't pre-migrate. No AWS service enters before its trigger signal is real. Optionality is already preserved by the gateway seam — buying it early just adds cost and IAM/VPC surface.
  • Don't move compute to AWS. The ephemeral-agent economics depend on Hetzner-priced compute and egress. AWS is for managed state/keys/durability, not for running agent Jobs.
  • Treat each migration as reversible unless proven otherwise (the table marks the soft one-way doors); keep the Hetzner path runnable for rollback through the transition.

See also