Rahul came to a clinic three times in 37 days. In July he said his lower back had been aching, worse in the mornings. Three weeks later, to a different clinician, he said he kept getting a pain when he stood up from his desk. Yesterday he said the ache was back again.
No word appears in all three sentences. The only word any two of them share is "back" — a body part in one, and "it has returned" in the other. A keyword search over his record does not merely miss the pattern; it matches the wrong sense of the word and returns nothing useful.
Open https://nirog-memory.vercel.app — no account needed — and ARIA opens by saying it out loud: "You have been in 3 times about your lower back in the last 37 days." CockroachDB's vector index connected those three sentences in 1,740 ms, correctly skipping the cold he mentioned in between. Open the doctor's side and the same three visits are on the chart, in his own words.
Nirog is a rural tele-health platform: ARIA takes the history by voice, a doctor receives it as an SBAR handover, and the consultation happens in the browser. What this hackathon is about is the middle of that — the memory between visits.
Inspiration: The Intake Bottleneck
Imagine a patient in rural who simply feels unwell. Right now, that patient's reality is a bus ride, lost wages, and a full day of travel just for a ten-minute consultation. Care should begin with a conversation, not a bus ride. We looked at rural India and realized that the healthcare system doesn't just break at the diagnosis but it breaks at the intake. For hundreds of millions of people, a ten-minute consult costs a full day of travel and lost wages, so care is deferred until it becomes an emergency. Existing text-first apps exclude low-literacy users, and when patients finally see a doctor, their story gets lost in translation. We were inspired to build a system that patiently listens to a frightened person's rambling account and translates it into a structured, clinician-ready format.
What is ARIA
ARIA is a 3D nurse who takes the clinical history before the consultation. She speaks (Amazon Polly, ARIA, en-NΖ), listens (Amazon Transcribe streaming), and the part this hackathon is about she reads the patient's history out of CockroachDB before she says hello, so the conversation starts where the last one ended.
What she refuses to do matters as much. She never tells a patient what they might have. The output goes to the clinician. Ada, Babylon and Your.MD all died handing patients a guess and carrying the liability with no doctor in the loop; that failure is a design decision, not bad luck, and Nirog inverts it.
CockroachDB is the memory, and it is not a cache
Every complaint is stored as the patient said it, with its embedding written to the same row in the same statement so there is no window in which the record and its vector disagree, and no second store to reconcile.
VECTOR(1024)columns, Amazon Titan Text Embeddings V2, cosine distance.- A C-SPANN vector index keyed
(patient_id, embedding vector_cosine_ops). Thepatient_idprefix scopes every search to one person. That is a performance property and a privacy property at once: a query that cannot express "search everyone" cannot accidentally do it. - 2,230 complaints across 405 patients in the index. Not a toy dataset.
- Cosine, not L2, because Titan does not guarantee unit-norm output and under L2 a long complaint drifts away from a short one that means the same thing. Magnitude tracks verbosity, not meaning.
recall_eventrecords every recall the query, how many matches, the top distance, the latency, which embedding backend answered, and whether memory was degraded. Including the lookups that found nothing, and the ones that failed.
The two layers, and why the model is never the one deciding
The embedding finds candidates. A fixed arithmetic rule decides what they mean: three or more separate visits naming one body region within ninety days.
It counts visits, not complaints three complaints in one appointment are one presentation, and counting complaints would let a talkative patient trigger the flag on their first visit. That is the sort of false alarm that teaches doctors to ignore banners.
No model writes any part of the chart. A model smoothing "the ache is back again" into "recurrent lumbar pain" invents a claim the patient never made, and how somebody describes their own pain is itself clinical information. A doctor who asks "why was this flagged?" gets a list of dates and the patient's own sentences, never a similarity score.
When the memory is unreachable, it says so
This is the part I would most like judged.
An unreachable database must never render as a clean record. On a chart, "no prior complaints" and "we could not check" look identical and a doctor reads that absence as reassurance. An agent that let a failed lookup render as a clean chart would have turned an infrastructure failure into a clinical finding, and nobody would know.
Every read returns a MemoryOutcome carrying degraded, so it is impossible to
use a result without handling the case where memory was down. When it is:
NOT ASSESSED. The patient's history was unavailable when this was written. No recurrence flag appears because none was looked for.
ARIA opens with "I can't get to your records right now, so I won't be able to
tell you if this has come up before." The doctor's panel goes amber with the
same sentence. recall_event.degraded records it.
The wording is asserted in the test suite, because here the wording is the
safety property. Point DATABASE_URL at a dead host and reload the chart
refuses to lie.
ARIA holds the conversation two ways
openai.gpt-oss-120b on Amazon Bedrock conducts the intake when it is reachable.
Underneath it, a deterministic OLDCARTS engine asks the same history in fixed
clinical order, adapting the wording and the red-flag question to the body region.
If the model dies mid-consultation she keeps asking questions instead of
apologising, and the handover still gets written.
The model is the layer that improves the product, never the layer it depends on. That was not a design flourish it was built during two days when Bedrock was blocked at the account level, and it is the reason the outcome of that never mattered.
How we built it, and what it cost
CockroachDB tools used: Distributed Vector Indexing, the Cloud Managed MCP Server, and the agent-ready ccloud CLI. Concretely:
The MCP Server connected Claude Code straight to the live cluster, read-only,
no proxy. Reading query plans through it produced two corrections I would not
otherwise have found. The vector index was first created on (embedding) alone,
and the plan showed the patient_id filter being applied after the vector
search ranging over every patient's vectors and then discarding all but one
person's. Adding patient_id as a prefix column turned that into a prefix
spans: entry. Separately, the recall query carried AND embedding IS NOT NULL,
which was redundant and made the index ineligible. Removing it exposed the real
bug underneath: embedding <=> $vec returns NULL for an unembedded complaint,
Number(null) is 0, and the mapping was ranking complaints with no vector at
all as perfect matches at the top of a doctor's screen.
The ccloud CLI backs npm run cluster:status, which reads cluster topology
and the Cloud audit log as JSON. recall_event records every read of a patient's
history; the control-plane audit log records every administrative action against
the cluster holding it. A system asking doctors to trust its memory should answer
both questions.
CockroachDB carries a second job beyond the vector index. A WebRTC call needs two browsers to swap an offer, an answer and ICE candidates before any media flows, and that is normally a second managed service. Here it is a table: call_signal, where each peer appends what it has to say and reads what the other appended. Media never touches it — once the browsers find each other the video is peer to peer — and rows are swept after ten minutes.
It cost us two bugs worth writing down. A joining peer replayed the last minute of signals, so a room used earlier handed the new arrival a stale offer it could never complete. And the joining peer minted its starting cursor from the browser clock, then asked the database for rows newer than it — a client a second ahead of the cluster waits forever for messages already sitting in the table. It connected locally and would not connect in production until the cursor came from the same clock that stamps the rows.
AWS services used: Amazon Bedrock (Titan Text Embeddings V2 for every vector
in the recall path; openai.gpt-oss-120b for the intake conversation and the SBAR
handover), Amazon Polly (ARIA, en-NZ neural : ARIA's voice), Amazon Transcribe
(streaming, en-NZ, PCM 16 kHz her ears). All on the same IAM user, scoped to
exactly those actions.
Honest engineering note. The vector index is real, built, and returns correct
results, but at demo scale CockroachDB's optimizer does not choose it, and it is
right not to. Scoped by patient_id there are four rows to read, and reading them
beats descending a C-SPANN tree. npm run db:explain prints both plans, the
planner's and the same query with the index forced, showing identical rows and
distances. We did not add an index hint to the production query. Making the
demo look better by making the software slower is not an engineering decision.
Challenges
Fifteen megabytes of nurse held the page hostage. On the deployed site the
patient could not leave the ARIA screen. Tapping a tab did nothing. Not a Link,
not location.href, not a form submit while buttons worked, React was hydrated
and the console was clean. With JavaScript disabled the same tab worked instantly.
The avatar is a 14.9 MB GLB and Vercel serves /public with max-age=0, so every
visit re-downloaded all of it: 101 seconds at 147 KB/s, during which every other
request on the connection queued behind it. It never reproduced locally, where the
same file arrives in under a second. Fixed by fetching the model at low
priority so a navigation outranks it, cutting the file to 6.2 MB (WebP textures
capped at 512px, all 16 morph targets intact), and serving it immutable. Found by
clicking through the deployed site in a real browser every route had been
answering 200 the whole time, which is exactly why fetching URLs never caught it.
Memory looked fine and was recording nothing. The CockroachDB write sat below
an isConfigured() early return, so with no clinical service configured every
turn returned before it. The seeded history still recalled perfectly, so the demo
looked flawless while nothing a patient said was ever stored. A judge talking to
ARIA would have been forgotten the moment they finished.
Switching Bedrock on silently broke recall. Cosine distances are only comparable within one embedding space. Titan compresses short sentences into 0.5–1.0 where the offline embedder spreads them over 0.1–1.0, so thresholds tuned for one killed region inheritance under the other. Each space carries its own cut-offs now, measured on a labelled battery of 20 complaint pairs, selected by the provider recorded on the row, and pinned by tests.
Transcribe's partial results arrived after the answer was sent, which would have put words in the patient's mouth. Only non-partial results are kept.
Accomplishments
The thing I am most pleased with is a negative result. Meera returns no flag. She has one complaint and comes back clean; Priya returns watch, not recurrence. A tool that finds a pattern in everyone is not detecting anything, and the boring cases are the evidence that the interesting one means something.
170 offline tests (no credentials needed), 18 live-cluster checks, and a Playwright crawler that walks the judge's path and reports dead ends.
What we learned
That fetching URLs is not testing. Every route returned 200 through hours of checks while the patient-facing page was, in fact, unusable on the deployed site. The bug was only visible to something that clicked. Real-browser walking now runs against production, not localhost.
What's next and who pays
The buyer is not the patient and not the individual doctor. Teleconsult platforms (Practo, 1mg, Apollo 24/7) pay per-consult costs and want shorter consults; an intake API that hands their doctor a written history before the call is a direct margin improvement, priced per consult or per seat. India's teleconsultation market plus the ABDM/ABHA national mandate, which Nirog already aligns with, is the route to volume. The moat is not the avatar and not the knowledge base both replicable. It is the deterministic red-flag rule set and the SBAR output format: clinical judgment encoded as rules that improve every time a real clinician corrects them.
Next concrete step: get one real clinician to mark up ten generated handovers. Their corrections are simultaneously the product roadmap and the only credible evidence.
Competitors
Ada, Babylon and Your.MD assumed the output goes to the patient. Nirog assumes the opposite. Every stateless symptom chatbot assumes the visit is the unit of memory; Nirog assumes the patient is the unit, which is why memory is the product rather than a feature of it.
What is genuinely limited
- The recurrence rule is a rule. It finds repetition in a body region over time. It does not know what causes it, and it says so on every screen.
- With the model unreachable, the interview is a structured questionnaire. It takes a complete history; it does not reason about causes.
- Thresholds were tuned against a labelled battery of sentence pairs, not a clinical corpus. Three visits in ninety days is a defensible starting point, not a guideline this project is in any position to establish.
- Demo data is seeded, and every screen that shows it says so.
- The teleconsultation relays through Metered's free TURN tier, which is sized for a demo and not for a clinic's traffic.
Built With
- amazon-bedrock
- amazon-polly
- amazon-titan-embeddings
- amazon-transcribe
- amazon-transcribe-streaming
- amazon-web-services
- ccloud-cli
- cockroachdb
- cockroachdb-mcp-server
- cockroachdb-vector-index
- gpt-oss-120b
- healthcare
- metered
- next.js
- playwright
- postgresql
- react
- tailwindcss
- telemedicine
- three.js
- typescript
- vercel
- vitest
- webrtc
Log in or sign up for Devpost to join the conversation.