Post one covered why Blood exists. This post covers how it’s built — and more importantly, why I made the technical choices I did.
Every architecture decision traces back to the three day-one principles: privacy by architecture, EU data residency, and AI as translator. Constraints aren’t limitations. They’re the product.
The Stack
Let me just list it upfront:
- Frontend: Svelte 5 (Vite build)
- Backend: AWS Lambda (Python 3.12)
- Infrastructure: AWS SAM (CloudFormation)
- AI Provider: Amazon Bedrock Nova 2 Lite (eu-west-3)
- Storage: IndexedDB (client-side), S3 (transient job state)
- CI/CD: GitLab CI → AWS deploy
No React. No Next.js. No PostgreSQL. No Redis. No Kubernetes. No Docker containers running 24/7.
Here’s why each choice happened.
Svelte 5: Compiled, Not Runtime
I’ve built enough frontends to know when I’m being sold complexity.
React is great until you’re debugging why a useEffect fired twice and your IndexedDB transaction locked up. Next.js is powerful until you need to deploy a static site without server-side rendering overhead.
Svelte 5 compiles to vanilla JavaScript. No runtime. No virtual DOM diffing. Just efficient DOM updates.
For Blood specifically:
- Small bundle size matters (users on mobile data)
- No runtime means less attack surface
- Runes (Svelte 5’s reactivity model) are cleaner than hooks for this use case
- Vite build = fast dev server, optimized production builds
The entire frontend compiles to static files. CloudFront serves them from S3. That’s it. No Node.js server, no SSR, no edge functions.
Lambda Over Containers
The backend processes PDFs and calls AI. That takes time — 10-30 seconds depending on PDF size and model response.
I had two options:
- Run a persistent service (EC2, ECS, Kubernetes)
- Use Lambda with async invocation
Option one means paying for idle time. It means managing container health, scaling policies, load balancers. It means infrastructure that costs money even at 3 AM when nobody’s uploading blood tests.
Option two means pay-per-request. Lambda spins up, processes the PDF, calls Bedrock, writes results to S3, spins down. Zero cost when idle.
But there’s a catch: API Gateway has a 30-second hard timeout.
If your Lambda takes 31 seconds, API Gateway returns a timeout error. The Lambda keeps running, but the client never gets the response.
The Async Job Pattern
I solved this with an async job pattern:
- Client POSTs PDF to
/api/analyze AnalyzeFunction(fast Lambda) validates, uploads PDF to jobs bucket, returnsjob_idAnalyzeFunctioninvokesProcessorFunctionasynchronously- Client polls
/api/status/{job_id}every 2 seconds ProcessorFunctiondoes the slow work (PDF extraction → AI → results)- Results written to S3, status updated
- Client fetches results, displays them
Two Lambdas. One fast gateway, one slow processor. The async invocation bypasses the 30-second API Gateway limit entirely.
This pattern shows up in the commit history on May 11th: “Switch to async job pattern to beat API GW 30s hard timeout”
It added complexity. But it meant I could keep everything serverless and pay-per-use.
AWS SAM: Infrastructure as Code
I could’ve clicked through the AWS Console and manually created every resource.
I didn’t.
SAM templates mean:
- Test and prod stacks are identical except for environment variables
- Deployments are repeatable and auditable
- Rolling back is a git revert away
- The entire infrastructure is documented in code
The template defines:
- Two CloudFront distributions (test + prod domains)
- Two Lambda functions (analyze + process)
- API Gateway with throttling (burst 30, rate 10/s)
- S3 buckets (frontend hosting, job state, artifacts)
- IAM roles with least-privilege permissions
- ACM certificates for HTTPS
One sam deploy command deploys the whole stack. GitLab CI runs it automatically on merge to develop (test) or main (prod).
Bedrock Nova 2 Lite: EU Residency + Cost Control
I evaluated three AI providers:
- Ollama (local dev only — not viable for prod)
- OpenRouter (aggregator, adds latency, unclear data residency)
- Amazon Bedrock (EU region, DPA for GDPR Art. 9, pay-per-token)
Bedrock Nova 2 Lite won because:
- Runs in eu-west-3 (Paris)
- AWS signs a DPA covering Article 9 special category data
- Cost: ~$0.0002 per 1K input tokens, $0.0006 per 1K output tokens
- Typical analysis: ~5K input + ~2K output = ~€0.003 per test
At scale, this matters. If Blood processes 10,000 tests/month, that’s ~€30/month in AI costs. Compare to a fixed-cost VM at €50/month idle.
IndexedDB: Client-Side Storage
Privacy by architecture means no server-side database storing user results.
IndexedDB is perfect:
- Stores JSON + blobs (PDFs)
- Persists across sessions
- Quota: ~60% of free disk space (plenty for blood tests)
- No sync needed (data stays local)
Trade-offs:
- No cross-device sync (yet — Google Drive OAuth is on the roadmap)
- User loses data if they clear browser cache
- Can’t query across users (but we don’t want to anyway)
The db.js module wraps IndexedDB operations: saveResult, listResults, getResult, getPdf, deleteResult. Five functions, 150 lines of code, complete data persistence.
CI/CD: GitLab to AWS
GitLab CI pipeline:
stages: [lint, test, build, deploy-test, e2e, deploy-prod]
lint:
- eslint + ruff (JS + Python)
test:
- pytest (backend unit + regression tests)
- Vitest (frontend, minimal for now)
build:
- npm run build (Vite → dist/)
- aws s3 sync dist/ s3://bucket --delete
deploy-test:
- Auto on push to develop
- Deploys to blood-testing.insightlab.be
deploy-prod:
- Manual gate on main
- Requires explicit approval from Gert
- Deploys to blood.insightlab.be
Key design: test is automatic, prod is manual.
This isn’t a medical device (yet), but the discipline matters. Test can break. Prod shouldn’t.
What This Architecture Gives Us
| Goal | How Architecture Delivers |
|---|---|
| Privacy | IndexedDB only, no server-side storage |
| EU Residency | Bedrock eu-west-3, AWS DPA |
| Cost Efficiency | Lambda pay-per-request, €22/month total |
| Scalability | Lambda auto-scales, CloudFront CDN |
| Auditability | SAM templates, GitLab commit history |
| Speed | Svelte compile, static hosting, CDN caching |
Constraints Are Features
Every choice here was constrained by the three principles. That constraint made decisions easier, not harder.
- “Can I use Firebase?” → No, US-based, GDPR concerns
- “Can I use GPT-4?” → No, US data residency, no DPA
- “Can I store results server-side?” → No, violates privacy-by-architecture
When someone asks “why not X?”, the answer is usually “because it breaks one of the three principles.”
That’s not limitation. That’s clarity.
This is post #2 in the Blood Development Log series. Read post #1 → | Series index →