<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Vandana Yarala</title>
    <description>The latest articles on DEV Community by Vandana Yarala (@vandana_yarala_92b1e060a0).</description>
    <link>https://dev.to/vandana_yarala_92b1e060a0</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4006015%2F8edf33b7-af7a-44b0-a347-6fa0049a638d.jpeg</url>
      <title>DEV Community: Vandana Yarala</title>
      <link>https://dev.to/vandana_yarala_92b1e060a0</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vandana_yarala_92b1e060a0"/>
    <language>en</language>
    <item>
      <title>Building TrialBridge AI with Vercel and AWS Aurora PostgreSQL</title>
      <dc:creator>Vandana Yarala</dc:creator>
      <pubDate>Sun, 28 Jun 2026 16:58:06 +0000</pubDate>
      <link>https://dev.to/vandana_yarala_92b1e060a0/building-trialbridge-ai-with-vercel-and-aws-aurora-postgresql-4i10</link>
      <guid>https://dev.to/vandana_yarala_92b1e060a0/building-trialbridge-ai-with-vercel-and-aws-aurora-postgresql-4i10</guid>
      <description>&lt;p&gt;&lt;a href="https://trial-bridge-ten.vercel.app/" rel="noopener noreferrer"&gt;TrialBridge AI&lt;/a&gt; is a B2B web application that helps clinical research teams match patients to clinical trials and coordinate the human follow-up work that comes after. This post walks through why we built it, what it does, the architecture, and the engineering decisions behind ii including the part that took the most iteration: connecting Vercel to AWS Aurora PostgreSQL with &lt;strong&gt;passwordless IAM authentication&lt;/strong&gt; over OIDC.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer:&lt;/strong&gt; TrialBridge AI is a research-workflow tool. It does not diagnose, treat, prescribe, or make final medical decisions, and every patient record in the demo is synthetic. That constraint shaped the whole design, as you'll see in the safety section. &lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  1. Why we built &lt;a href="https://trial-bridge-ten.vercel.app/" rel="noopener noreferrer"&gt;TrialBridge AI&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;Clinical trial recruitment is slow and expensive. Research coordinators spend enormous amounts of time reading trial eligibility criteria, cross-referencing patient charts, and figuring out who &lt;em&gt;might&lt;/em&gt; qualify — work that is repetitive, error-prone, and hard to audit. Many promising trials fail to enroll enough participants, and many patients never learn about trials they could have joined.&lt;/p&gt;

&lt;p&gt;We didn't want to build "an AI that decides who is eligible." That's the wrong tool for a regulated, high-stakes domain. Instead, we wanted to build the layer underneath that decision: a system that &lt;strong&gt;organizes the information, applies the trial's own published criteria transparently, explains its reasoning, and hands a clean, prioritized case to a human coordinator&lt;/strong&gt; for review and referral.&lt;/p&gt;

&lt;p&gt;In short: keep the human in the loop, but give them a much faster, more structured starting point.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. What the application does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://trial-bridge-ten.vercel.app/" rel="noopener noreferrer"&gt;TrialBridge AI&lt;/a&gt; is organized around a few core workflows, each backed by its own page in the app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trials&lt;/strong&gt; — a catalog of clinical trials with their structured inclusion/exclusion criteria (condition, phase, location, sponsor, and a list of weighted rules).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patients&lt;/strong&gt; — synthetic patient profiles with demographics, a condition, free-text summary, structured attributes, and lab results.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Matching&lt;/strong&gt; — for any patient, the app evaluates them against a trial's criteria and produces a status (&lt;code&gt;likely_eligible&lt;/code&gt;, &lt;code&gt;possibly_eligible&lt;/code&gt;, or &lt;code&gt;not_eligible&lt;/code&gt;), a numeric score, and a per-criterion explanation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coordinator tasks&lt;/strong&gt; — matches that need human attention become prioritized tasks with status tracking (&lt;code&gt;new&lt;/code&gt; → &lt;code&gt;needs_review&lt;/code&gt; → &lt;code&gt;contact_pending&lt;/code&gt; → &lt;code&gt;referred_to_pi&lt;/code&gt; → &lt;code&gt;closed&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audit log&lt;/strong&gt; — every meaningful action is recorded as an audit event, so the workflow is reviewable after the fact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key idea is that the output is never "this patient is eligible." It's "here is how this patient lines up against each published criterion, here's what's missing, and here's what a coordinator should look at next."&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Architecture overview
&lt;/h2&gt;

&lt;p&gt;The stack is intentionally boring in the best way — managed building blocks, with the interesting logic kept in plain, testable TypeScript.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6bsbn1uwqpcckbdi9vg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi6bsbn1uwqpcckbdi9vg.png" alt="Architecture" width="800" height="533"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key choices:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js 16 App Router&lt;/strong&gt; with React Server Components for data-heavy pages, so reads happen on the server close to the database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drizzle ORM&lt;/strong&gt; for a fully typed schema and queries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A deterministic matching engine&lt;/strong&gt; written as pure functions — no model calls in the eligibility path — so results are explainable and reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AWS Aurora PostgreSQL Serverless v2&lt;/strong&gt; as the system of record, reached with IAM authentication instead of a static connection string.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. How we used Vercel
&lt;/h2&gt;

&lt;p&gt;Vercel hosts the Next.js app and provides the runtime glue that makes passwordless database auth possible.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Server Components for reads.&lt;/strong&gt; Pages like the dashboard, trial detail, and patient detail are async server components that query Drizzle directly. There's no client-side data-fetching layer to secure or cache-bust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server Actions for writes.&lt;/strong&gt; Updating a coordinator task's status or writing an audit event is a server action, co-located with the UI that triggers it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OIDC → AWS credentials.&lt;/strong&gt; This is the important one. Vercel issues a signed &lt;strong&gt;OIDC token&lt;/strong&gt; to the deployment, and &lt;code&gt;@vercel/functions/oidc&lt;/code&gt; exchanges it for short-lived AWS credentials via &lt;code&gt;sts:AssumeRoleWithWebIdentity&lt;/code&gt;. No long-lived AWS access keys live in environment variables.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is what connects the two halves of the stack, so it gets its own section.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. How we used AWS Aurora PostgreSQL
&lt;/h2&gt;

&lt;p&gt;Aurora PostgreSQL is the database, but the interesting part is &lt;strong&gt;how we authenticate to it&lt;/strong&gt;. Instead of storing a database password, we use &lt;strong&gt;AWS IAM database authentication&lt;/strong&gt;: the application asks the AWS SDK's RDS &lt;code&gt;Signer&lt;/code&gt; to generate a short-lived auth token, and that token is used as the Postgres password.&lt;/p&gt;

&lt;p&gt;Here's the actual connection setup (trimmed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;drizzle&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;drizzle-orm/node-postgres&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Pool&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Signer&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@aws-sdk/rds-signer&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;awsCredentialsProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@vercel/functions/oidc&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./schema&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;DB_CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_AWS_ROLE_ARN&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;// IAM role to assume&lt;/span&gt;
  &lt;span class="na"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_AWS_REGION&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_PGHOST&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// Aurora cluster endpoint&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_PGUSER&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;// an rds_iam-enabled DB user&lt;/span&gt;
  &lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DB_PGDATABASE&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5432&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;createPool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;port&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DB_CONFIG&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Signer&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// Vercel OIDC token -&amp;gt; temporary AWS credentials&lt;/span&gt;
    &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;awsCredentialsProvider&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="nx"&gt;roleArn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;clientConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;// The "password" is a freshly-minted IAM auth token on each connection&lt;/span&gt;
    &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getAuthToken&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="na"&gt;ssl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rejectUnauthorized&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;drizzle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;createPool&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The chain of trust looks like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Vercel mints an &lt;strong&gt;OIDC token&lt;/strong&gt; for the deployment (scoped to the team, project, and environment).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;awsCredentialsProvider&lt;/code&gt; calls &lt;strong&gt;AWS STS&lt;/strong&gt; with that token (&lt;code&gt;AssumeRoleWithWebIdentity&lt;/code&gt;) and gets temporary credentials for an IAM role.&lt;/li&gt;
&lt;li&gt;The RDS &lt;code&gt;Signer&lt;/code&gt; uses those credentials to generate a &lt;strong&gt;time-limited Postgres auth token&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pg&lt;/code&gt; connects to Aurora using that token as the password; Aurora validates it because the DB user was granted &lt;code&gt;rds_iam&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;To make this work on the AWS side, three things have to line up:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;IAM OIDC identity provider&lt;/strong&gt; for the Vercel issuer (&lt;code&gt;oidc.vercel.com/&amp;lt;team&amp;gt;&lt;/code&gt;), with the correct audience.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;IAM role&lt;/strong&gt; whose trust policy allows that provider's &lt;code&gt;sub&lt;/code&gt;/&lt;code&gt;aud&lt;/code&gt; to assume it, plus an &lt;code&gt;rds-db:connect&lt;/code&gt; permission scoped to the cluster resource ID and DB user.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;database user&lt;/strong&gt; created with &lt;code&gt;GRANT rds_iam&lt;/code&gt;, and &lt;code&gt;--enable-iam-database-authentication&lt;/code&gt; on the cluster.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- inside the Aurora database&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;app_user&lt;/span&gt; &lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;LOGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="n"&gt;rds_iam&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;app_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;PRIVILEGES&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="n"&gt;trialbridge&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;app_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="k"&gt;SCHEMA&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;app_user&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The payoff: &lt;strong&gt;there is no database password anywhere&lt;/strong&gt; — not in env vars, not in a secrets manager, not in code. Credentials are short-lived and minted on demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Database schema
&lt;/h2&gt;

&lt;p&gt;The schema is defined in Drizzle and models the full workflow from organizations down to individual audit events. The core tables:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Table&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;organizations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The B2B tenant (research site, sponsor, CRO).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;users&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Coordinators / PIs, scoped to an organization.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trials&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Clinical trials: condition, phase, location, sponsor, status, summary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trial_criteria&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Structured inclusion/exclusion rules (&lt;code&gt;field&lt;/code&gt;, &lt;code&gt;operator&lt;/code&gt;, &lt;code&gt;value&lt;/code&gt;, &lt;code&gt;weight&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;synthetic_patients&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Synthetic patient profiles (demographics, condition, attributes).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;patient_labs&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Lab results linked to a patient.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;trial_matches&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A patient↔trial evaluation with status, score, and summary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;match_explanations&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Per-criterion result for a match (&lt;code&gt;met&lt;/code&gt;, &lt;code&gt;missing&lt;/code&gt;, &lt;code&gt;failed&lt;/code&gt;, &lt;code&gt;triggered_exclusion&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;coordinator_tasks&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Human follow-up work with status + priority.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;audit_events&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Append-only record of who did what.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A representative slice, in Drizzle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;trialCriteria&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;pgTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trial_criteria&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;primaryKey&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;defaultRandom&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;trialId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;trial_id&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;trials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;onDelete&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cascade&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;criterionTypeEnum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;     &lt;span class="c1"&gt;// 'inclusion' | 'exclusion'&lt;/span&gt;
  &lt;span class="na"&gt;field&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;field&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;                &lt;span class="c1"&gt;// e.g. "age", "hba1c"&lt;/span&gt;
  &lt;span class="na"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;operator&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;          &lt;span class="c1"&gt;// e.g. "gte", "in", "contains"&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;value&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;                &lt;span class="c1"&gt;// e.g. "18", "male|female"&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;integer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;weight&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;notNull&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two design notes worth calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Criteria are data, not code.&lt;/strong&gt; Each rule is a row with a &lt;code&gt;field&lt;/code&gt;, &lt;code&gt;operator&lt;/code&gt;, &lt;code&gt;value&lt;/code&gt;, and &lt;code&gt;weight&lt;/code&gt;. That means trials can be added or edited without touching the matching logic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explanations are first-class.&lt;/strong&gt; &lt;code&gt;match_explanations&lt;/code&gt; stores &lt;em&gt;why&lt;/em&gt; each criterion passed or failed, so the UI can show the reasoning and the result stays auditable.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Trial matching workflow
&lt;/h2&gt;

&lt;p&gt;The matching engine (&lt;code&gt;src/lib/matching/engine.ts&lt;/code&gt;) is &lt;strong&gt;pure, deterministic TypeScript&lt;/strong&gt; — no LLM in the eligibility decision. It takes a patient profile and a trial's criteria and returns a status, a score, and per-criterion evaluations.&lt;/p&gt;

&lt;p&gt;Each criterion is evaluated independently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;evaluateCriterion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;patient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;CriterionEvaluation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawValue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getFieldValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;patient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;field&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// No data? That's "missing" — a flag for the coordinator, not a rejection.&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawValue&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rawValue&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;rawValue&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;criterionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;explanation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;…&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;passes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;compareValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawValue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;exclusion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;passes&lt;/span&gt;
      &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;triggered_exclusion&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;  &lt;span class="c1"&gt;// hard stop&lt;/span&gt;
      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;met&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;passes&lt;/span&gt;
    &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;met&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* … */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The overall &lt;strong&gt;status&lt;/strong&gt; is then derived conservatively:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Any &lt;strong&gt;triggered exclusion&lt;/strong&gt; → &lt;code&gt;not_eligible&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Any &lt;strong&gt;failed inclusion&lt;/strong&gt; → &lt;code&gt;not_eligible&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;All inclusions &lt;strong&gt;met&lt;/strong&gt; → &lt;code&gt;likely_eligible&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Some inclusions &lt;strong&gt;missing&lt;/strong&gt; (but none failed) → &lt;code&gt;possibly_eligible&lt;/code&gt; — i.e. "a human needs to fill in the gaps."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;score&lt;/strong&gt; is a weighted percentage of inclusion criteria met, where missing data counts as half credit, and a triggered exclusion caps the score low:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evaluation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;met&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="nx"&gt;earned&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evaluation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;missing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;earned&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;criterion&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// …&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;earned&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;totalWeight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This design has a deliberate bias: &lt;strong&gt;missing information never silently disqualifies a patient.&lt;/strong&gt; It surfaces as &lt;code&gt;possibly_eligible&lt;/code&gt; with an explicit "coordinator should verify" note, because the worst outcome in recruitment is quietly dropping someone who actually qualified.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Safety and privacy design
&lt;/h2&gt;

&lt;p&gt;Because this touches healthcare, safety wasn't a feature we added at the end — it constrained the whole build.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Synthetic data only.&lt;/strong&gt; Every patient in the system is fabricated. There is no real PHI, by design, in the demo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No diagnosis, ever.&lt;/strong&gt; The product surfaces structured assessments and explanations; it never outputs a medical decision. A persistent disclaimer component makes this explicit in the UI:&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Research workflow demonstration only. Not for diagnosis or treatment decisions. All patient data is synthetic. TrialBridge AI supports research coordinators in organizing information and preparing cases for human review — it does not diagnose, treat, prescribe, or make final medical decisions.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Human in the loop by construction.&lt;/strong&gt; Matches become &lt;em&gt;tasks&lt;/em&gt;, not actions. The terminal state of a positive match is &lt;code&gt;referred_to_pi&lt;/code&gt; — a human, not the system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic, explainable logic.&lt;/strong&gt; No black-box model decides eligibility. Every status has a traceable per-criterion explanation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditability.&lt;/strong&gt; &lt;code&gt;audit_events&lt;/code&gt; records actions for after-the-fact review — exactly what a B2B compliance reviewer expects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passwordless data access.&lt;/strong&gt; As covered above, there's no database password to leak; credentials are short-lived IAM tokens minted via OIDC.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  9. What we learned
&lt;/h2&gt;

&lt;p&gt;A few takeaways from building TrialBridge AI:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;OIDC-based database auth is worth the setup cost.&lt;/strong&gt; Wiring up the IAM identity provider, role trust policy, &lt;code&gt;rds-db:connect&lt;/code&gt; permission, and &lt;code&gt;rds_iam&lt;/code&gt; database user took the most iteration of anything in the project. The reward — zero stored database credentials — is absolutely worth it for a B2B product where security posture is part of the sale. The most common failure mode was a &lt;strong&gt;trust-policy mismatch&lt;/strong&gt; (e.g. the role allowing &lt;code&gt;production&lt;/code&gt; but the deployment running as &lt;code&gt;development&lt;/code&gt;); matching the OIDC &lt;code&gt;sub&lt;/code&gt;/&lt;code&gt;aud&lt;/code&gt; exactly is the whole game.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Region and account consistency matters.&lt;/strong&gt; The IAM role and OIDC provider are global, but the RDS cluster, the &lt;code&gt;AWS_REGION&lt;/code&gt; env var, and the &lt;code&gt;rds-db:connect&lt;/code&gt; resource ARN must all reference the same region and account. A mismatched default CLI region sent us looking for a "missing" cluster that was simply in another region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep the high-stakes logic deterministic.&lt;/strong&gt; For an eligibility decision, plain, testable functions beat a model call. They're explainable, reproducible, and easy to audit — and in a regulated domain those properties are the product.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model "missing" as its own state.&lt;/strong&gt; The difference between "doesn't qualify" and "we don't have the data yet" is the difference between losing a patient and flagging a quick coordinator follow-up. Encoding &lt;code&gt;missing&lt;/code&gt; as a first-class result changed the whole tone of the output.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design for the human handoff first.&lt;/strong&gt; The most useful artifact isn't the match — it's the prioritized, explained task that lands on a coordinator's desk. Building the workflow (tasks, statuses, audit) around that handoff is what makes it a product instead of a demo.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;em&gt;TrialBridge AI is built with Next.js 16, Drizzle ORM, and AWS Aurora PostgreSQL, deployed on Vercel. All patient data is synthetic, and the application is a research-workflow tool — not a medical device.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note: This article was created for the purpose of entering the H0: Hack the Zero Stack with Vercel v0 and AWS Databases hackathon.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Written by Vandana Yarala &amp;amp; Kamalakar Thota. Questions or feedback? Contact us at [&lt;a href="https://www.linkedin.com/in/vandanayarala/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/vandanayarala/&lt;/a&gt;].&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>vercel</category>
      <category>postgres</category>
      <category>h0hackathon</category>
    </item>
  </channel>
</rss>
