Back to BlogIndustry Guides

Building a Lab Results Portal Patients Can Trust

Rupak Amin

Founder & Lead Engineer, RAITHub

13 min read

A lab results portal lets patients and providers see test results online. It needs 5 parts: release rules, reference ranges and flags, PDF reports, role-based access with an audit log, and notifications that never put a result in an email or SMS. In the US, holding results back from patients can raise 21st Century Cures Act information-blocking questions, so every delay needs a documented basis.

If you would rather have it built for you, see how RAITHub would build this below.

This is engineering guidance. RAITHub has not shipped a lab results portal or any regulated health product; its health-adjacent client work is a healthcare scheduling app, which is not regulated. RAITHub is a software studio, not a laboratory or clinical provider, and the release and clinical rules below belong to your lab director, clinicians and counsel.

What does a lab results portal need to do?

Show the right result, to the right person, at the right time, and prove afterwards who saw what. The five parts:

PartWhat it doesEngineering detail that matters
Release rulesDecide when a result becomes visible to the patientOnly verified, final results are released; any hold needs a reason, an owner and an end date
Reference ranges and flagsShow each value against its normal range, with high, low and critical flagsStore the range used at the time with the result, so a later range change cannot rewrite history
PDF reportsA printable, official copy of the reportGenerated on the server, streamed only after an access check, never from a public file URL
Roles and audit logPatients see their own results; providers see their patients'; staff manage the queueAccess enforced in the database query, plus an append-only log of every view, download, release and hold
NotificationsTell the user a result is readyThe message says only that something is ready; the result is behind a sign-in

When should a lab result be released to the patient?

When it is verified and final, unless a documented rule says otherwise. Your lab and clinical leads decide the rules; the portal enforces them the same way every time.

The US information-blocking point

The 21st Century Cures Act set up a framework against information blocking: practices that "interfere with the access, exchange, or use of electronic health information", unless required by law or covered by an exception. The actors are healthcare providers, developers of certified health IT, and health information networks and exchanges. ONC's page also points to research showing patients prefer immediate access to test results.

So a blanket policy of "hold every result for 7 days until the doctor calls" deserves scrutiny. One of the exceptions is preventing harm (45 CFR 171.201). Among its conditions, the practice must be "no broader than necessary", and the risk generally has to be determined by a licensed health care professional, either case by case or through a written organisational policy based on clinical and technical expertise. Whether a given delay fits is a legal question; the engineering consequence is that every hold must record who set it, why, and when it ends.

General information, not legal or medical advice. Whether you are an actor under the information-blocking rules, and which exceptions apply, depends on your organisation and state law. Confirm with your adviser.

Release rules that usually make sense

  • Preliminary results go to the ordering provider, not the patient.
  • Final, verified results are released to the patient automatically, or after a short documented window if your policy sets one.
  • Holds are set by an authorised clinician for one result, with a reason from a fixed list and an expiry. When the hold expires, the result is released without anyone remembering to do it.
  • Corrected results replace the original on screen, keep the original in history, and notify everyone who already viewed it.

What does release gating with an audit row look like in code?

A minimal sketch: a PostgreSQL schema that stores the reference range with each result and allows only time-limited holds, a TypeScript check that decides whether a viewer may see a result, and an append-only audit table. It is illustrative, not production code.

-- schema.sql
create table lab_result (
  id                uuid primary key,
  patient_id        uuid not null,
  ordering_provider uuid not null,
  test_code         text not null,
  value_numeric     numeric,
  unit              text,
  ref_low           numeric,           -- range used for THIS result, copied at verification
  ref_high          numeric,
  flag              text check (flag in ('L', 'N', 'H', 'LL', 'HH')),
  status            text not null check (status in ('preliminary', 'final', 'corrected')),
  verified_at       timestamptz,
  hold_until        timestamptz,
  hold_reason       text,
  hold_set_by       uuid,
  -- a hold is all-or-nothing: reason, owner and end date together
  check ((hold_until is null) = (hold_reason is null) and (hold_until is null) = (hold_set_by is null))
);

create table result_access_audit (
  id         bigserial primary key,
  result_id  uuid not null references lab_result (id),
  actor_id   uuid not null,
  actor_role text not null,
  action     text not null check (action in ('view', 'download_pdf', 'release', 'hold', 'denied')),
  reason     text,
  at         timestamptz not null default now()
);

-- the app's database role may add audit rows, never change them
revoke update, delete on result_access_audit from app_user;
grant insert, select on result_access_audit to app_user;
// release-gate.ts
type Status = 'preliminary' | 'final' | 'corrected'
type LabResult = {
  id: string
  patientId: string
  orderingProvider: string
  status: Status
  verifiedAt: Date | null
  holdUntil: Date | null
}
type Viewer =
  | { id: string; role: 'patient' }
  | { id: string; role: 'provider'; careTeamPatientIds: string[] }

type Decision = { allowed: boolean; reason: string }

export function canViewResult(viewer: Viewer, r: LabResult, now = new Date()): Decision {
  if (viewer.role === 'provider') {
    const onCareTeam = r.orderingProvider === viewer.id || viewer.careTeamPatientIds.includes(r.patientId)
    return onCareTeam ? { allowed: true, reason: 'care_team' } : { allowed: false, reason: 'not_care_team' }
  }
  // Patients: own result, verified, final or corrected, no active hold.
  if (r.patientId !== viewer.id) return { allowed: false, reason: 'not_owner' }
  if (r.status === 'preliminary' || !r.verifiedAt) return { allowed: false, reason: 'not_final' }
  if (r.holdUntil && r.holdUntil > now) return { allowed: false, reason: 'on_hold' }
  return { allowed: true, reason: 'released' }
}

// Every decision, allowed or denied, becomes one audit row.
export function auditRow(viewer: Viewer, r: LabResult, d: Decision, action: 'view' | 'download_pdf') {
  return {
    result_id: r.id,
    actor_id: viewer.id,
    actor_role: viewer.role,
    action: d.allowed ? action : 'denied',
    reason: d.reason,
  }
}

Three choices are deliberate. The database refuses a hold without a reason, an owner and an end date, so an indefinite silent hold cannot exist. Denied attempts are logged as well as successful views, because a patient opening someone else's result ID is the event you most want to see. And the check runs on the server for the page, the API and the PDF download alike. In production, also filter by patient in the SQL query itself, ideally with row-level security, so a forgotten check cannot leak a list. The RBAC design guide and the audit log design guide go deeper.

How should reference ranges, flags and PDF reports work?

Reference ranges and flags

  • Ranges are versioned. They vary by test, method, unit, age and sex, and they change when the lab changes an analyser. Copy the range onto the result when it is verified.
  • Flags come from the lab, not the portal. Show the flag the laboratory system assigned (low, normal, high, critical). Do not recalculate it in the front end.
  • Say what a flag means and what it does not. Plain wording, reviewed by your clinical lead, such as "outside the lab's usual range; your clinician will interpret this result". No generated interpretation.
  • Critical values stay with your lab's procedure. Urgent results are normally phoned to the provider; the portal is not that channel.

PDF reports

In the US, the CLIA rule on test reports (42 CFR 493.1291) lists required content, including patient identification, the name and address of the performing laboratory, the report date, the test performed, and the result with units where applicable. It also allows labs to give patients access to completed reports that, using the lab's authentication process, can be identified as theirs. In practice:

  • Generate the PDF from the same verified data as the screen, so they cannot disagree.
  • Stream it from an authenticated endpoint that runs the same gate and writes an audit row; never store reports in a public bucket or send them as email attachments.
  • Mark corrected reports clearly and keep every version.

How do you notify patients without leaking results in email or SMS?

By sending a message that contains nothing worth reading. Email and SMS can be seen on lock screens, shared devices and forwarded inboxes.

Put in the messageNever put in the message
"You have a new message in your portal. Sign in to view it."The test name (an HIV or pregnancy test name alone is sensitive)
Your organisation's name and a link to the sign-in pageValues, flags or words like "abnormal" or "urgent"
A support numberA link that opens the result without signing in

Apply the same rule to push notifications and to page titles, which show up in browser history. Let patients choose their channel, and record their preference as a versioned consent event.

Buy, build or hire: which is right for a lab results portal?

OptionExample and costChoose this whenWatch out for
Off-the-shelf LIS with a portalCrelioHealth: plans listed from $600 a month, with a branded patient portal on the $2,750 Pro plan, when we checkedYou run a lab and need the laboratory information system as well as the portalYou work within the vendor's release rules, roles and notification wording
No-code builderBubble: paid plans from $59 a month on annual billing when we checkedYou want a clickable prototype with synthetic data to test the experienceCheck the vendor's terms for health data before any real result goes in
Custom portal on top of your LIS or EHRMarket rates vary; try the MVP cost estimatorYour lab system has no usable portal, or you serve several labs, clinics or brands from one front endYou own the integration, the access model and the tests

If your existing laboratory system or EHR already has a patient portal that meets your release and notification rules, use it. A custom portal pays off when it has to join several systems or serve audiences the vendor portal cannot.

Why RAITHub for this

For the web engineering of the portal and its access model, with your lab director, clinicians and counsel owning the rules. RAITHub is a founder-led software studio, founded in 2024 in Dhaka, Bangladesh, working with clients worldwide in English. The evidence it can offer:

  • Row-level access at scale. Sundor Skin, a B2B wholesale platform, has row-level security across 146 PostgreSQL tables, 88 permission codes and 12 staff roles, with 530+ tests. "Patients see only their own rows" is the same mechanism.
  • Distinct roles tested end to end. PropDesk, a property management platform, has 4 roles and 1,024 tests.
  • Several audiences on one platform. TheSkinProof, the founder's own venture rather than a client, runs 5 portals on 217 API endpoints with 750+ tests.
  • Health-adjacent client work, stated plainly. A healthcare scheduling app, among 8 client projects; not a regulated product. See the health software guide, the HealthTech page and the HCP portal guide for related gating patterns.
  • Working hours. US teams get a daily 2-hour evening overlap (Dhaka 19:00–21:00, New York 08:00–10:00 in winter), with written daily handoffs for the rest.

RAITHub signs NDAs and DPAs and works inside your controls. Production and patient data stay in your own covered cloud account, and development uses synthetic data. RAITHub holds no HIPAA, SOC 2 or ISO certification and signs no BAAs.

When you don't need us

  • Your LIS or EHR portal already fits. Configure it.
  • You need a vendor that signs a BAA or holds certifications. RAITHub does neither.
  • You need a validated laboratory system. The LIS that produces and verifies results is a specialist product; RAITHub builds the portal around it, not the system of record.
  • You want the developer to decide release policy. RAITHub implements your clinical and legal decisions; it does not make them.
  • You need native mobile apps. RAITHub builds web apps and PWAs only.

How RAITHub would build this

  • Scope: a patient and provider web portal (or PWA) reading verified results from your laboratory system or EHR.
  • Release engine: your rules as code, with time-limited holds, automatic release and correction handling, tested branch by branch.
  • Access and audit: row-level security, provider care-team access, staff queue roles, and an append-only audit log of every view, download, hold and denial.
  • Reports: versioned reference ranges, lab-assigned flags and server-generated PDFs behind the same gate.
  • Notifications: content-free email and SMS, with channel preferences stored as consent events.

Timeline: 4–6 weeks for a fixed-scope portal through the SaaS and web platform service. If the work is mainly a new integration layer with your laboratory system, it follows the backend and API range of 6–12 weeks.

What you receive: automated tests and CI, handover docs and runbooks, and full IP under NDA.

Next step: book the free 15-minute technical audit, then get a written fixed quote. Please do not send patient data or real reports in the first message.

Frequently asked questions

Can a lab delay releasing results to patients?

Sometimes. In the US, delays can raise information-blocking questions under the 21st Century Cures Act unless an exception, such as preventing harm, applies. Each hold should record who set it, why and when it ends. Confirm your policy with your adviser.

Should lab results be sent by email?

No. Send a message that says only that something is ready, with a link to sign in. Never include the test name, values, flags or a link that opens the result without authentication.

How should reference ranges be stored?

Copy the range used onto each result when it is verified. Ranges change by method, age and sex and when analysers change, and a stored copy keeps old results correct.

What should a lab results portal log?

Every view, PDF download, release, hold and denied attempt, with the actor, role, result, reason and time, in an append-only table the application cannot edit.

How long does a lab results portal take to build?

A fixed-scope portal on top of an existing laboratory system or EHR fits 4–6 weeks. A new integration layer with the lab system follows a 6–12 week backend range.

Has RAITHub built a lab results portal?

No. RAITHub has shipped no regulated health product. This guide is engineering guidance based on the role-heavy, row-level-secured platforms it has built, such as Sundor Skin and PropDesk.

Lab results portalPatient portal developmentInformation blockingReference rangesRBACAudit loggingHealthTech

Ready to discuss your project?

Book a free 15-minute technical audit with our engineering team.