Healthcare tech development ·14 Jun 2026 ·12 min

How to Build a HIPAA-Compliant Patient Portal on Next.js and AWS

The hard parts of a patient portal are identity proofing, record release rules, and the places PHI leaks by accident: analytics, URLs, caches and logs. Framework choice matters far less than teams assume, and EHR integration drives the cost.

Pranav Begade By Pranav Begade
How to Build a HIPAA-Compliant Patient Portal on Next.js and AWS

Almost nothing that makes a patient portal hard is in the framework you pick. The hard parts are proving that the person signing up is the patient whose chart they are about to open, deciding which results are allowed to appear without a human releasing them, and finding every place where protected health information quietly ends up somewhere it was never meant to be. A portal built on Next.js and AWS can be perfectly defensible. So can one built on Rails and bare EC2. The stack is a preference. The list below is not.

Teams get this backwards because the framework decisions are the ones with documentation. There is a page telling you how to configure authentication and a page telling you how to deploy. There is no page telling you that your analytics tag is sending the page title "Lab Results: HbA1c" plus a patient identifier in the URL to a third party on every view, which is a far more likely cause of a breach notification than a cryptography mistake.

So this is a guide to the load-bearing parts: the ones that cost a month each when you find them late. The framework section is near the end, where it belongs.

What a patient portal actually has to do

Strip the marketing and a portal is five capabilities stitched to a clinical system of record: enrolment, appointments, records, messaging, billing. Each has a different failure mode. Enrolment is the one everybody underestimates, so it gets its own section below.

Appointments

Read-only appointment lists are straightforward. Self-booking is not, because the portal has to understand the practice's scheduling rules: which provider, which location, which room or chair, how long the appointment type takes, what the provider's template allows, and whether the slot is still free in the source system at the moment the patient taps confirm. In Denti360, the studio's own dental practice management product, appointments are scoped across branches and chairs, and that structure is the reason a slot is bookable or not. Show availability without modelling those constraints and you book patients into slots that do not exist.

Records access and messaging

Results, medications, allergies, problems, immunisations, notes, documents: the feature patients come for, and the one with the most rules attached. Messaging is cheap to build and expensive to operate, because an inbox with no triage silts up and clinical staff start missing things. Decide who reads it and how fast before shipping it, and put the reply into the record rather than a side channel.

Billing

Two warnings. Billing data is health information too, because a charge code says what was done. And billing is routinely the module that overruns: Denti360's billing and receivables work took longer to build than its estimate implied, and that product only has to satisfy a practice management workflow rather than an insurance claim lifecycle. If your portal touches claims, coverage, or a patient responsibility figure from an adjudication, budget it as a project rather than a screen.

Patient identity is harder than normal authentication

Consumer sign-up asks whether this person controls this email address. A portal has to answer a different question: is this person the human that chart belongs to. Passwords, one-time codes and social logins all prove account control after the binding exists. They say nothing about the binding, and the binding is the risky step. Get it wrong and you show one person another person's medical history, which is the worst thing your product can do.

The approaches, roughly in order of strength:

  • In-person issuance. Staff check a government ID and issue a single-use enrolment token tied to that chart. Strongest, and it ties enrolment to visits.
  • Third-party verification. Document capture plus liveness, or an identity service. Costs money per enrolment and adds a vendor processing identity data for you.
  • Out-of-band code to a contact on file. Only as good as contact hygiene in the source system, which is usually worse than anyone admits.
  • Demographic matching. Name, date of birth and a record number typed into a form. Weakest, still common. Two patients sharing a name and date of birth is not rare, duplicate charts are normal in any practice of size, and the failure is silent.

Then there is delegated access, where a simple permission model stops being simple: a parent viewing a young child's record, that child reaching an age where the parent's access must narrow or end on a date that varies by jurisdiction and data category, a spouse acting under a revocable authorisation, a caregiver allowed appointments but not notes, and your own support staff opening an account to help.

Model this as a first-class grant object from the first migration: who is acting, on whose record, with which scope, from when to when, authorised by what evidence, revoked by whom. Not a nullable parent_id on the user table. Retrofitting a grant model after launch means rewriting every query that reads a record and re-deriving history you no longer have. Denti360 put branch scoping into the schema in the first migration rather than adding it later, and that is the cheap version of the same decision.

Then there are sensitive categories. Behavioural health notes, substance use treatment records, reproductive care, and certain results are treated differently from the rest of the chart in many jurisdictions, sometimes under a separate rule entirely. "Show the patient everything we have" is not implementable, because a proxy may be entitled to part of a record and not the rest. Your access check needs a category dimension, not only a record-owner check.

Information blocking changes the default direction of your API

In the United States, the rules that came out of the 21st Century Cures Act mean a provider generally may not interfere with the access, exchange, or use of electronic health information, with a defined set of exceptions. Get a lawyer to tell you how it applies to your entity. What matters for the build is that it pushes your design the opposite way from what clinical software historically did.

The old default was withholding: results sat in a queue until a clinician pressed release. The new default is release, with a recorded reason when you do not. Consequences:

  • A manual release gate cannot be the default path. Build one and it has to be an exception flow recording which exception applies and who invoked it, not a step every result passes through.
  • Delay needs a stored reason. "This result is held 72 hours so the doctor can call first" is a policy someone may have to justify. Make it configurable per category, attach the justification, log every time it fires.
  • Your portal is not the only client. Patients may use third-party apps against standardised APIs, so the data has to exist in a standard shape with standard codes, not only the shape your components want.
  • Store coded data, not rendered data. Values, units, reference ranges and codes in separate fields. A portal that persists "A1c 7.2% (high)" as a string cannot later serve a standard resource or graph a trend.

All of which argues for treating records access as an API with its own contract rather than endpoints shaped by screens. If you are building against or alongside a clinical system, that data model conversation belongs with the EHR side of the build.

Where PHI ends up by accident

Read this section twice. Portals rarely leak because someone broke the encryption. They leak because health information travelled somewhere nobody was thinking about.

Third-party scripts on authenticated pages

Analytics, tag managers, ad pixels, chat widgets, and session replay tools. On a signed-in page these collect the URL, the page title, referrers, form interactions, and with replay tools the rendered DOM including free text. A page titled with a test name, a URL carrying a record identifier, and a visitor identifier that persists across sessions add up to a disclosure. The rule that survives audit: no third-party script on any authenticated route. Measure with your own server-side events instead, carrying no identifiers you would not put in a log.

URLs and query strings

An identifier in a path or query string ends up in load balancer logs, proxy logs, browser history, the Referer header sent to any external resource, and anything a patient pastes into an email to support. Use opaque identifiers that mean nothing outside your database, keep them out of query strings, and never put a name, date of birth, or test description in a URL.

Rendering and caching

Here the framework choice genuinely matters. Any layer that stores a rendered response or a fetched payload is a place patient data can be handed to the wrong person, and the failure is not subtle when it happens: one patient sees another patient's page.

LayerHow PHI gets inWhat to do
CDN or edge cache, or static generationAn authenticated response cached, or a route pre-rendered with one user's data, then served to another sessionSend Cache-Control: no-store, private on every authenticated response rather than trusting a default, and keep authenticated routes dynamic.
Framework server-side data cacheA fetch memoised across requests, keyed on the URL and not the sessionOpt out for PHI fetches, or put the subject identity in the key. Re-check after every framework upgrade: caching defaults move between major versions.
Client-side router cacheA visited page redisplayed after sign-out or account switchHard-navigate on sign-out and on any identity change, discarding the in-memory cache.
Browser back/forward cacheA restored page showing a signed-out user the previous contentSet the no-store header, then check by hand on iOS Safari as well as Chrome.
Service worker, or localStorageResponses or tokens persisted to disk on a shared deviceKeep authenticated responses out of the worker. Session cookies with HttpOnly and Secure, no PHI in web storage.

Logs, errors, and notifications

Application logs that dump request and response bodies. Slow-query logs containing bound parameters. Error trackers that capture the request payload, local variables and breadcrumbs of recent network calls. An SMS whose text says what the result is, sent to a number nobody re-verified after the patient changed carriers. Push payloads rendered on a lock screen.

Scrub at the point of emission rather than trusting a vendor's filter, and treat an allowlist of loggable fields as safer than a denylist of forbidden ones. Notification copy is part of the security design, not content someone writes at the end: "You have a new result" is fine, naming the test is not.

Copies of the database

Staging seeded from production, a CSV export in a shared drive, a dashboard on the live replica, an impersonation feature with no audit trail, any feature forwarding record text to an external model API. Each is the same data under weaker controls, and a business-associate agreement does not follow it onto a laptop.

Sessions, devices, and the shared family computer

Portal sessions live on the waiting room tablet, the library computer, the phone the whole household uses. Design for that, not for a SaaS dashboard.

  • Idle timeout with a visible warning, and a way to extend it that does not lose typed input.
  • An absolute session lifetime, so a kept-alive tab cannot live forever.
  • Server-side revocable sessions. Issue long-lived self-contained tokens with no revocation list and you cannot sign anyone out, which makes "sign out all devices" a button that lies.
  • Step-up authentication for changing the contact address or password, granting proxy access, or exporting the record. These are what an attacker with a borrowed session wants.
  • A device list the patient can see and revoke, with last-used time and rough location.
  • A public-device mode that skips "remember me", shortens the timeout, and clears client caches on exit.
  • Multi-factor that suits the actual population. An authenticator app is strong and will lock out a share of older patients. Offer SMS too, accept that it is weaker, and design the recovery path carefully: once it exists it is your real authentication mechanism.

Audit logging is a feature, not a middleware line

Health privacy rules require you to say who accessed a record and when. Note the verb: access, not modification. Most application logging captures writes well and reads not at all, which is backwards for this requirement.

A usable access log records the acting user, the subject patient, the record or category touched, the action, the timestamp, the source address and client, and the authority behind it, including any proxy grant or staff impersonation. It is append-only, stored apart from the application database, retained for years rather than weeks, and readable by a compliance officer without a developer writing a query.

The engineering consequence is that the event has to come from the data access layer rather than from controllers. Emit from controllers and you will miss the background job, the export, the resolver someone added, and the admin screen. Emit from the layer that loads a patient record and every path is covered by construction. Keep the write durable through a queue so a slow audit store does not stall a page load, and monitor that queue: an audit trail with a silent gap is worse than none, because you will be asked about the gap.

Talking to the EHR is where the estimate goes wrong

A portal is only interesting because it shows clinical data, so it has to talk to the system holding it. This is routinely the largest and least predictable part of the build, for a structural reason: you are not integrating with a product, you are integrating with a product as configured at one customer site.

PathWhat it gives youWhat it costs
FHIR REST APIModern resources, decent read coverage, an app registration routeVendor review queues, often read-only scopes, a sandbox that differs from production
HL7 v2 via an integration engineReal-time events for admissions, results, schedulingAn engine to run, per-site message variation, network access to the practice, and someone who can read a pipe-delimited message at 2am
Document exchange (CCDA)A summary document per encounter, quicklyCoarse data to parse and reconcile, poor fit for trends or filters
Flat file or SFTP batchWorks with almost anything, including old systemsStaleness, no write path, reconciliation logic you own forever
Vendor API or partner programDeeper access, sometimes including writesNegotiation, certification, dependency on one vendor's roadmap

Four things to plan for whichever path you take. Read access exists and write access usually does not, so promise self-booking and record updates only after confirming the write scope exists for that customer's system and version. Change notification is usually absent, so you poll, and polling frequency becomes a rate-limit conversation. Data quality will be worse than the specification suggests, with free text in coded fields and missing units. And each new practice is its own integration project rather than a configuration screen, so integration belongs in the per-customer cost model. Teams that miss this price the second customer as if the work were done.

If the portal is one module of a larger clinical product, what to build first is a healthcare product sequencing question rather than a portal one.

Where Next.js and AWS actually fit

A React meta-framework like Next.js is a reasonable choice here, for ordinary reasons: one language across the stack, server rendering that keeps record data off the browser, decent form and routing primitives, and a large hiring pool. Nothing about it is disqualifying for health data.

The trade-off is the caching model. The machinery that makes the framework fast is the machinery that can hand one patient's page to another, and its defaults move between major versions. So write the caching policy for authenticated routes down as an explicit rule, add a test that asserts the headers on a signed-in response, and re-check after every upgrade. The framework version is a compliance-relevant dependency.

The alternative worth weighing is a plain client application against a separate API: no server render to cache by accident, one door to the data, at the price of more data in the browser and token handling you have to get right. Either shape passes review. The one that fails is the one where nobody can say which layers hold data.

On the cloud side, three things. A business-associate agreement covers specific eligible services used in specific ways, so check the list before adopting a service rather than after. Managed authentication gives you sign-in, multi-factor and session handling, and none of identity proofing, proxy grants, sensitive-category rules or record-level access logging, which is most of the work above. And infrastructure audit trails record API calls against your cloud account, not a user reading a chart, so they do not meet the access-logging requirement on their own.

One planning number from operating a live clinical product: Denti360 needs ongoing engineering of 15 to 20 percent of the original build cost per year just to stay current, before new features. A portal touching an external clinical system, a payment processor and mobile browsers sits at the upper end of that, because someone else's upgrade becomes your work item. Budget the second year before committing to the first, and if enrolment and access control is the part you are least sure of, settle it on paper before writing the schema. Our notes on patient portal development cover how that work usually gets sequenced.


Building a patient portal and unsure whether your enrolment, access, and EHR integration model will hold up? A Scoping Sprint ($2,300, two weeks) ends with the identity, records access, and integration design made for your case, a prototype, and a fixed quote. Or just start a conversation.

Frequently asked

What does patient portal app development actually involve?
Five capabilities bolted to a clinical system of record: enrolment and identity proofing, appointments, records access, messaging, and billing. The build effort is rarely in the screens. It sits in binding an account to the correct chart, deciding which results may appear without a human releasing them, logging every read of a record, and integrating with whichever electronic health record the practice runs.
How do you make a patient portal HIPAA compliant?
Compliance is a set of controls, not a library you install. The ones that carry weight for a portal are identity proofing at enrolment, record-level access logging that captures reads as well as writes, revocable sessions with short idle timeouts, encryption in transit and at rest, a signed agreement with every vendor that touches the data, and a deliberate hunt for places health information leaks by accident.
Can I build a HIPAA-compliant patient portal with Next.js and AWS?
Yes, and the framework is close to irrelevant to whether you pass review. Next.js works well for portals because server rendering keeps record data off the client. Its caching layers are the risk: an authenticated response cached at the edge or a fetch memoised without the session in the key can hand one patient another patient's page. Write the caching policy down and re-test after every upgrade.
Why is patient identity harder than normal user authentication?
Ordinary sign-up proves someone controls an email address. A portal has to prove the person is the patient that chart belongs to. Email verification, passwords, and one-time codes all prove account control after the binding exists. Demographic matching on name and date of birth fails silently, because duplicate charts are common and two patients can share both. In-person token issuance or a verification vendor is stronger.
What is information blocking and how does it affect the API design?
In the United States, rules from the 21st Century Cures Act mean a provider generally may not interfere with a patient's access to their electronic health information, subject to defined exceptions. For the build it flips the default from withholding to release. A manual release gate becomes an exception flow with a recorded reason, and your data has to be available in standard coded form because patients may use third-party apps, not only your portal.
Where does PHI leak in a patient portal without anyone noticing?
Third-party analytics, tag managers, chat widgets and session replay scripts on signed-in pages are the most common culprit, because they capture URLs, page titles and rendered text. Then identifiers in query strings that land in access logs and referrer headers, error trackers capturing request bodies, notification copy that names a test result, staging databases seeded from production, and any caching layer that stores an authenticated response.
What does audit logging need to record in a patient portal?
Every read of protected health information, not just changes. Record the acting user, the subject patient, the record or category touched, the action, the time, the source address and client, and the authority behind it, including proxy grants and staff impersonation. Emit the event from the data access layer rather than controllers so background jobs and exports are covered. Keep the store append-only, separate, and retained for years.
How long does it take to integrate a patient portal with an EHR?
Longer than the portal, and it is the least predictable line in the estimate, because you integrate with a product as configured at one customer site rather than with a product. Expect read access to exist and write access often not, no change notifications so you poll, data quality below the specification, and a fresh integration project for each practice you onboard. Price it per customer, not once.
Fixed price · $2,3002-week sprint

Building something in this space?

We turn ideas into buildable plans in 2 weeks: clickable prototype, technical plan, fixed quote. Fixed price, credited against the build.

See the Scoping Sprint

Build secure healthcare software

Start a project →
Book a 15-min scoping call