Inspiration
Every emergency department visit starts the same way: a sick person sits in a waiting room while the information that would help them sits in three different places that have not talked to each other yet.
The front end of an ED encounter is mostly dead time and rework:
- Registration and intake pull nurses off clinical work to retype information the system already has.
- Language is a safety problem, not a convenience problem. In major metros, roughly one in five ED visits involves a patient with limited English proficiency, and those patients are measurably under-triaged. The fix today is a phone interpreter who shows up after the patient has already been waiting.
- Clinicians walk in blind. Before seeing a patient, an attending flips through a paper intake form or clicks through four EHR screens to reconstruct a story the patient already told the front desk.
- The same data gets entered three times, by the patient, by registration, by the nurse, and is wrong by the third time.
We did not want to build a chatbot bolted onto a hospital website. We wanted the thing that actually removes the dead time: a patient front door that any person can use from their own phone with no app and no account, and a clinician cockpit that turns that intake into a decision before the patient is roomed. Both sides, one system, sharing one source of truth.
That source of truth is Amazon DynamoDB, and the whole product is built around the fact that an ED is bursty, unforgiving, and cannot afford to wait on capacity planning.
What it does
Solace is two surfaces over one backend.
The patient front door
- A patient scans a QR code posted at the waiting-room entrance. No download. No login. No account.
- They pick from 20+ languages, and the entire check-in, including the prompts, the questions, and the spoken summary at the end, happens in that language.
- They describe what is wrong by voice or by typing, in plain words: "I've had crushing chest pain for an hour and my left arm feels numb."
- Solace asks a short set of AI-generated follow-ups that are specific to what the patient just said, not a static form. Chest pain gets "is it getting worse, the same, or better?". A laceration gets different questions entirely.
- They optionally photograph an injury or an insurance card, and OCR pulls the structured fields.
- Within seconds they hear a calm, spoken explanation of their triage priority and what to expect, in their language, with explicit instructions to walk to the front desk if it is life-threatening.
The patient never sees a model name or a confidence interval. They see a person being told, gently and quickly, that they have been heard and that someone is coming.
Atlas, the clinician command center
The clinician side is a single screen we call Atlas. The instant a patient finishes intake, they appear in a live queue ranked by provisional ESI. A clinician tags a patient and every panel fills with that patient's picture at once:
- Patient snapshot: provisional ESI (for example, ESI 2, Emergent), an AI pre-brief written from the intake, clinical flags (such as
rule.esi2.high_risk_presentation), and recommended next steps ("high-risk, do not let wait, expedite to acute bed"). - Ambient scribe: captures the bedside conversation in chunks and finalizes an evidence-linked SOAP note bound to that encounter.
- Copilot: catch-up summaries, flag scans, and answers pulled from the chart on demand.
- Clinical reasoning: suggested labs (troponin, CBC, BMP, coagulation studies, D-dimer), imaging, consults, and a disposition recommendation with the reasoning shown.
- EHR connected record: the matched patient pulled in FHIR R4 shape, with allergies, medications, conditions, family history, primary care, and prior visits.
- Composable admin tools added with one click: coding (an encounter note with suggested E&M and ICD-10 codes auto-sourced from the scribe note), letters and forms (FMLA, education handouts, with chart context auto-built), prior auth (diagnosis, requested service, a ready-to-assemble PA packet), care gaps (open HEDIS measures and a PRAPARE social-needs screen), and result closure.
- Workflows: automations that fire on acuity, like Slack the charge nurse when ESI is 2 or lower, open an outreach task for high no-show risk, or escalate critical results to the on-call.
A clinician sits down and the whole encounter, clinical, administrative, and billing, is already assembled.
The stack at a glance
| Layer | What we used |
|---|---|
| Primary database | Amazon DynamoDB, multi-table, on-demand, CMK-encrypted, TTL on transient tables |
| Frontend and deployment | Vercel, the patient intake SPA, the Atlas clinician terminal, and the marketing site |
| API | FastAPI on AWS Lambda, an ARM64 container image, 2048 MB, Python 3.12 behind API Gateway |
| Edge and security | CloudFront and AWS WAFv2, with IP reputation, OWASP common, known-bad-inputs, and a rate-based rule |
| AI services | AWS Bedrock (Claude), Amazon Transcribe, and Amazon Polly, all under the AWS BAA |
| Triage ML | Stacked ensemble of LightGBM, XGBoost, CatBoost, and an MLP, with SHAP and conformal prediction |
| Media and secrets | Amazon S3 (CMK, presigned delivery) and AWS Secrets Manager (CMK) |
| Observability | CloudWatch, CloudTrail, SNS, EventBridge |
How we built it
Why Amazon DynamoDB, and how the data is actually modeled
This is the part we care about most, because it is the part that is easy to get wrong.
An emergency department is the textbook case for DynamoDB. Traffic is bursty and unpredictable, a quiet floor, then six ambulances, then quiet again. Reads have to be single-digit-millisecond because the live queue refreshes on every arrival and the clinician is staring at it. And the access patterns are known in advance: we always look a patient up by id, a queue up by hospital, an encounter up by patient. That is exactly the shape DynamoDB is built for, and exactly the shape a relational database punishes you for under load.
So we did not drop a relational schema into DynamoDB and call it done. We designed the access patterns first and built a deliberate, multi-table model around them. About 30 tables, all on-demand (PAY_PER_REQUEST) so we never plan capacity, every table encrypted with one customer-managed KMS key (alias/solace), with TTL where state should expire on its own.
The data, in three groups:
- Clinical core:
solace-patients,solace-ehr-patients,solace-clinicians,solace-hospitals,solace-notes,solace-prescriptions,solace-appointments,solace-calls. - Identity and sessions:
solace-magic-tokens,solace-intake-nonces(TTL, self-expiring),solace-blocklist. - Integrity and safety:
solace-idempotency(TTL, exactly-once writes),solace-quotas,SolaceRateLimit,solace-audit-log(append-only, hot in DynamoDB, archived to S3 for six-year HIPAA retention).
The one to look at is solace-patients. Its partition key is patient_id, and it carries a global secondary index on hospital_id (partition) and created_at (sort). That single index is the live queue: query by hospital, sorted by arrival time, in milliseconds. The clinician cockpit reads it on every refresh.
A few decisions are the difference between "uses DynamoDB" and "is designed for DynamoDB":
- Exactly-once intake without transactions. A patient on a flaky waiting-room connection will double-submit. Instead of a distributed transaction, the client sends a stable idempotency key and we store it in a TTL'd
solace-idempotencytable. The same key lands once and returns the cached response on a retry, and the record evicts itself after 24 hours. No cleanup job. - Self-expiring sessions for free.
solace-intake-noncesuses DynamoDB TTL as the session lifetime. The database is the garbage collector. - Audit, hot and cold. Access is written to
solace-audit-logwith a 90-day TTL for hot access, and every record is also archived to S3 as CMK-encrypted JSONL with a lifecycle transition to Glacier, which is how we meet the HIPAA six-year retention requirement without a relational audit trigger. - Hospital-scoped, not session-scoped, queues. The live queue is server-side and keyed by hospital, so the moment any patient checks in, every clinician viewing that ED sees them, which is the entire point of a shared cockpit.
The payoff is that the live clinician queue and the EHR auto-match return in single-digit milliseconds, with zero capacity planning, and the design scales horizontally from one ED to many hospitals by adding partitions, not by re-architecting.
Vercel as the front door
Both human-facing surfaces deploy on Vercel: the patient intake SPA, the Atlas clinician terminal, and the marketing site at mysolaceclinic.com. Vercel gives us instant global delivery and atomic, branch-based deploys, so the patient app a person scans into is a static, edge-cached shell that talks to the AWS backend. It is fast to load on hospital Wi-Fi and on a bad cellular connection alike, which is the only network you can count on in a waiting room.
The triage brain
Triage in Solace is two stages, on purpose:
- Narrative ESI on intake. Claude (via AWS Bedrock, across the Haiku, Sonnet, and Opus tiers depending on the call) reads the patient's own words and produces a provisional ESI level before any vitals exist. This is what powers the spoken result the patient hears and the first sort of the live queue.
- Refinement at the bedside. When vitals are entered, a stacked ML ensemble of LightGBM, XGBoost, CatBoost, and an MLP refines the ESI. We attach SHAP attributions so a clinician sees why the model scored what it did, and split-conformal prediction sets so the model can say "I'm not sure" with a real coverage guarantee instead of a fake confidence number. A deterministic safety floor sits on top, so the system never down-triages a life threat.
The model never makes the call. It hands the clinician a defensible starting point and gets out of the way.
Security and compliance, because it is healthcare
Every AI call routes through AWS-managed services covered by the AWS Business Associate Agreement, so there are no separate vendor BAAs to chase. DynamoDB, S3, the CloudTrail bucket, and Secrets Manager are all encrypted with one CMK. CloudFront sits in front of WAFv2 with Amazon IP-reputation, OWASP-common, known-bad-inputs, and a rate-based rule capped at 50,000 requests per five minutes per IP (tuned for hospital NAT), with Shield Standard on. Clinicians authenticate by SMART-on-FHIR (OAuth and PKCE) or a quick PIN. IAM is scoped to least privilege with an MFA permission boundary.
By the numbers
- About 30 purpose-built DynamoDB tables, on-demand and CMK-encrypted
- 20+ languages of voice-guided intake, with no app and no account
- 4 models in the stacked triage ensemble, plus SHAP and conformal prediction
- 2 human surfaces, patient and clinician, over 1 shared, server-side source of truth
- Single-digit-millisecond queue and EHR-match reads under bursty load
Challenges we ran into
- Modeling clinical state without joins. The instinct is to normalize. The right move in DynamoDB is to enumerate the exact queries the Atlas terminal makes and key for those, then accept controlled duplication. Getting the access patterns right first was the whole game.
- Exactly-once on an unreliable network. Waiting-room connections drop mid-submit. Solving idempotency with a TTL'd table instead of transactions kept writes correct without adding a coordination service.
- Explainability that a clinician will actually trust. A raw probability is worse than useless in triage. Wiring SHAP and conformal prediction into the bedside refinement, and surfacing them in plain language, was harder than training the models.
- Two front ends, one truth. Keeping the patient app and the clinician cockpit in sync meant the queue had to be hospital-scoped and server-side, so a check-in on one device shows up on every clinician's screen instantly.
Accomplishments we are proud of
- A patient can complete a real, multilingual, voice-first intake with no app and no account, and a clinician sees them in the queue the moment they finish, both ends working over one DynamoDB-backed source of truth.
- A clinician cockpit that assembles the entire encounter, provisional ESI, AI pre-brief, ambient scribe, FHIR-shaped EHR, coding, prior auth, care gaps, and workflows, before the patient is roomed.
- A DynamoDB data model that is genuinely designed for the workload, not retrofitted: TTL idempotency, self-expiring nonces, append-only audit, and a hospital-keyed GSI for the live queue.
- It is HIPAA-grade and shippable, not a slide. Everything runs under the AWS BAA on managed services.
What we learned
- DynamoDB rewards thinking backwards. Start from the screen, not the schema. Once we listed every read the clinician actually performs, the table design fell out almost mechanically, and the performance came for free.
- TTL is a feature, not a footnote. Using expiry as exactly-once semantics and as session lifetime removed an entire category of background jobs.
- Explainability is a UX problem. The models were the easy part. Making a busy attending trust a number in four seconds is the hard part, and it is mostly design.
What's next
- More EHR connectors and deeper per-hospital conformal calibration, so the model's uncertainty is tuned to each department's population.
- Multi-region DynamoDB global tables for million-scale, multi-hospital deployments with local-latency reads.
- Expanding the ambient scribe from a SOAP draft to a fully closed documentation loop, signed in one click.
Built With
- amazon-api-gateway
- amazon-bedrock
- amazon-cloudfront
- amazon-dynamodb
- amazon-polly
- amazon-transcribe
- aws-lambda
- aws-waf
- catboost
- fastapi
- lightgbm
- python
- react
- tailwind-css
- typescript
- vercel
- vite
- xgboost
Log in or sign up for Devpost to join the conversation.