dhaga.docs
Self-hosting

Cost & scaling on AWS

A service-by-service migration map off Supabase + Vercel, a cost model at 1k / 10k / 100k users, and the scaling architecture — built around the one number that moves the bill: model inference.

Leaving Supabase + Vercel to self-host on AWS at SaaS scale: a service-by-service migration map, a cost model at 1k / 10k / 100k users, and the scaling architecture — built around the one number that actually moves: model inference.

AWS-preferred · Postgres + pgvector · Claude Haiku + Sonnet · figures as of 2026-07-20.

The headline

Your infrastructure is cheap; your model inference is the business. At every scale below, AWS infra runs $0.18–$0.41 per user / month and drops as you grow. Claude usage scales linearly with activity and, if proactive AI runs uncapped, becomes 3–8× the infra bill. Self-hosting is mostly a decision about the DB and compute — but the thing to engineer hardest is metering and capping the LLM.

1. Service map — what replaces what

Two escape routes per layer: the AWS-native managed service (less ops, AWS-preferred), and a fully-portable open-source equivalent (zero lock-in, cheapest on bare metal). Your app is already built to move — better-auth is self-hosted, the DB is boring relational Postgres, and the LLM/search gateways make providers swappable.

LayerTodayAWS-nativePortable / self-host
Web app + APIVercelECS Fargate or App Runner (containers); EKS at scaleDocker on Hetzner / Fly / bare metal
Postgres + pgvectorSupabaseAurora PostgreSQL (Serverless v2) or RDS Postgres — enable vectorPostgres + pgvector container; Crunchy
Connection poolingSupabase poolerRDS ProxyPgBouncer sidecar
Cache / hot readsElastiCache (Redis / Valkey)Redis / Valkey container
Object storage (card photos, exports)Supabase StorageS3 + CloudFrontMinIO
Job queue / nightlyVercel cron + routesSQS + EventBridge Scheduler; workers on FargateBullMQ (Redis) / river (Go)
Search indexPG FTS (SearchIndex gateway)Amazon OpenSearch — drop-in via the gatewayElasticsearch / Meilisearch / Typesense
Authbetter-auth (in-app)Unchanged — it runs inside your app on any hostUnchanged
Transactional emailResendAmazon SESPostal / SMTP relay
SecretsVercel envSecrets Manager / SSM Parameter StoreHashiCorp Vault / SOPS
CDN + TLS + LBVercel edgeCloudFront + ALB + ACMCaddy / nginx + Cloudflare
On-device primitives (OCR, embeddings)local ONNXKeep on-device / in-app; GPU only if you centralize themKeep on-device / in-app
LLMAnthropic APIClaude Platform on AWS (keeps Batch) · or Bedrock (no Batch)Anthropic API direct; or Ollama / vLLM (self-host)
ObservabilityVercel AnalyticsCloudWatch + X-RayGrafana + Prometheus + Loki

The one AWS gotcha that matters for you

Bedrock is AWS-native but does not support the Message Batches API, the server-side web-search tool, or automatic prompt caching — so your nightly Batch enrichment (−50%) and web-search enrichment both break there. Claude Platform on AWS is Anthropic-operated on AWS infrastructure with AWS IAM + Marketplace billing and same-day API parity — it keeps Batch and web search. If you want AWS-native billing without losing those, choose Claude Platform on AWS over Bedrock. The simplest option of all is calling the first-party Anthropic API straight from your ECS/EKS tasks.

2. Cost at three scales

Monthly, order-of-magnitude. MAU = monthly active users; assume ~40% of them trigger AI in a given month. AWS figures assume on-demand + modest reserved coverage; the Claude figures assume metering is on (see §3). Treat as planning brackets — model your real numbers in the AWS Pricing Calculator.

ScaleAll-in / monthInfra / userAWS infraClaude (metered)
1,000 MAU~$0.9–1.9k~$0.41~$410~$0.5–1.5k
10,000 MAU~$7–18k~$0.25~$2.5k~$5–15k
100,000 MAU~$65–170k~$0.18~$18k~$50–150k

AWS infra line items

Service1k MAU10k MAU100k MAU
Compute (Fargate/EKS + workers)$120$550$4,500
Aurora Postgres (writer + replicas)$180$950$7,000
RDS Proxy$0$120$600
ElastiCache$45$300$1,400
S3 + CloudFront + egress$40$300$2,600
OpenSearch (optional)$0$300$2,000
SES · SQS · Secrets · CloudWatch$25$180$1,200
AWS infra subtotal~$410~$2,700~$19,300

Read the trend, not the cents

Per-user infra falls with scale ($0.41 → $0.18) as fixed services amortize and reserved/savings plans kick in. The AI line does the opposite — it tracks usage. That divergence is the whole strategy: optimize AI spend, not server spend.

3. The cost center: Claude inference

Your architecture already picks the right model per job — Haiku for extraction, Sonnet for search/drafts, Batch for nightly. Here's what that costs, and where it explodes.

TierModelInput /MTokOutput /MTokLevers
Extraction / parsingclaude-haiku-4-5$1.00$5.00cache the stable system prefix; structured output
Search / draftsclaude-sonnet-5$3.00 ($2 intro*)$15.00 ($10*)prompt-cache the graph context; effort tuning
Nightly jobseither, via Batch−50%−50%enrichment, digests, change-detection

* Sonnet 5 introductory pricing $2/$10 per MTok runs through 2026-08-31; model durable planning at $3/$15. Prompt-cache reads bill at ~0.1× input; cache writes 1.25× (5-min) / 2× (1-hour).

Two spend regimes

Metered & capped — sustainable

A moderately active user costs roughly ~$1–2 / month: ~$0.20 extraction (Haiku, cached) + ~$0.50 search (Sonnet) + ~$0.50 enrichment (Sonnet via Batch). Your free-tier cap of 25 cloud AI actions/month is exactly the right instrument — it bounds the worst case per user before it starts.

Proactive & uncapped — the blow-up

The danger isn't the chat box — it's proactive AI running nightly across a large graph: watchlist re-checks, enrichment sweeps, digest generation over thousands of contacts. Uncapped, that's $10–50+ / user / month and it's invisible until the invoice. This is the single biggest cost decision in the product — cap by tier, meter every action, and put latency-insensitive work on the Batch API.

Cheapest paths to the same quality

  • Prompt caching is free money. The stable system prefix + the user's graph context are the same across calls — cache them and input drops to ~0.1×. Keep volatile content (today's date, the query) last so the prefix stays byte-stable.
  • Batch everything that can wait. Enrichment, change-detection, digests → Message Batches at −50%. (Keep this on the Anthropic API or Claude Platform on AWS — not Bedrock.)
  • Right-size the model per task, then cap effort. Haiku already handles extraction; don't let a Sonnet call do a Haiku job.
  • Meter from day one (you already do) so a runaway tenant is a throttled tenant, not a surprise.

4. Scaling architecture

The compute layer auto-scales trivially; the database is the ceiling. The move that unlocks real concurrency is the same one your docs already track — transaction-mode pooling with per-transaction RLS — plus caching the hot reads so most requests never touch Postgres.

The request path:

Users · mobile · extension

CloudFront + WAF

Application Load Balancer

App tier — ECS Fargate / EKS   (Next.js + API, autoscaled)
        ↓   fans out to
  ├─ ElastiCache Redis          hot reads · rate limits
  ├─ RDS Proxy (txn pooling)    → Aurora writer, read replicas
  ├─ S3                         photos · exports → CDN
  └─ SQS + EventBridge          → Fargate workers (extraction · enrichment · digests)
        ↓   Haiku / Sonnet · nightly Batch
Claude — API · Platform on AWS · Bedrock

The five levers, in priority order

  1. Transaction pooling + per-transaction RLS. Session-pinned pooling caps you at ~pool_size concurrent DB requests. Transaction mode (RDS Proxy / PgBouncer) multiplexes thousands of clients onto a few backends — but requires moving RLS scoping to SET LOCAL inside each transaction. This is the prerequisite for real scale.
  2. Cache the hot reads. Graph payload, home dashboard, contact lists → ElastiCache, keyed per user (already RLS-scoped), invalidated on write. At 10k+ MAU this is the biggest lever for keeping DB load flat.
  3. Read replicas. Route graph/search reads to Aurora replicas; writes to the primary.
  4. Short transactions. Never hold a connection across an LLM or network call — the principle already applied to the extraction worker generalizes everywhere.
  5. Queue the heavy work. Extraction, enrichment, embeddings → SQS + Fargate workers, off the request path.

5. When to self-host the model

The LLM gateway makes Claude ↔ Ollama/vLLM a drop-in — so this is a pure cost/quality/ops trade you can defer until the numbers demand it.

OptionEconomicsWhen it wins
Claude API / Platform on AWS (default)Pay-per-token; Batch −50%; caching 0.1×. Zero GPU ops.Until inference is a large, steady fraction of COGS. Almost always the right start.
Self-host on AWS GPUg5/p4d/p5 at ~$1–30+/hr committed. Expensive per GPU-hour; you pay for idle.Rarely — AWS GPU pricing erases most of the savings vs. the API.
Self-host on dedicated / Hetzner GPUFar cheaper GPU-hours; you own scheduling, batching, uptime.Very high, steady extraction volume where a small open model matches Haiku on your evals.

Recommended sequence

Stay on Claude with Batch + caching. If inference becomes a large share of COGS, self-host the extraction tier first (a small open model on GPU, gated behind the existing LLMClient interface) and keep Sonnet-grade reasoning on Claude — you capture most of the volume savings without risking the quality-sensitive path. Never call a bigger model where a smaller one passes your evals.

6. Migration order

  • Prove portability first — you can already run the whole stack via docker compose; that's your escape hatch and your staging target.
  • Move the database to Aurora Postgres (pgvector), fronted by RDS Proxy, and land the transaction-pooler + per-transaction-RLS rework here — it's the load-bearing change for scale.
  • Containerize the app + workers onto ECS Fargate behind ALB + CloudFront; wire SQS for nightly jobs.
  • Add ElastiCache and cache the hot reads before you need to.
  • Point the LLM gateway at Claude Platform on AWS (keeps Batch + web search) or the first-party API — not Bedrock, unless you drop those features.
  • Flip search to OpenSearch through the SearchIndex gateway only if PG FTS stops keeping up — the seam is already there.

Bottom line

Self-hosting on AWS is a well-trodden path and the infra stays cheap — a fraction of a dollar per user, falling as you grow. Spend your engineering budget where the money actually is: the transaction-pooler rework that unlocks concurrency, and the metering/caps/Batch discipline that keeps model inference — your real cost center — from running away.

Planning brackets, not quotes

Cost figures are planning brackets — validate against the AWS Pricing Calculator and current Claude pricing for your workload. Prices current as of 2026-07-20; Sonnet 5 introductory rate expires 2026-08-31.

On this page