Health IT: More I, less T

“USCDI vs. USCDI+ vs. EHI vs. HL7 FHIR US Core vs. IPA. Definitions, similarities, and differences as you understand them. Go!” —Anonymous, Twitter

I spent about a decade working in “Health Information Technology” — an industry that builds solutions for managing the flow of healthcare information. It’s a big tent that boasts one of the largest trade shows in the world and dozens of specialized venture funds. And it’s quite diverse, including electronic health records, consumer products, billing and cost management, image management, AI and analytics of every flavor you can imagine, and more. The money is huge, and the energy is huger.

Real world progress, on the other hand, is tough to come by. I’m not talking about health care generally. The tools of actual care keep rocketing forward; the rate limiter on tests and treatments seems only our ability to assess efficacy and safety fast enough. But in the HIT world, it’s mostly a lot of noise. The “best” exits are mostly acquisitions by huge insurance companies willing to try anything to squeak out a bit more margin.

That’s not to say there’s zero success. Pockets of awesome actually happen quite often, they just rarely make the jump from “promising pilot” to actual daily use at scale. There are many reasons for this, but primarily it comes down to workflow and economics. In our system today, nobody is incented to keep you well or to increase true efficiency. Providers get paid when they treat you, and insurance companies don’t know you long enough to really care about your long-term health. Crappy information management in healthcare simply isn’t a technology problem. But it’s an easy and fun hammer to keep pounding the table with. So we do.

But I’m certainly not the first genius to recognize this, and the world doesn’t need another cynical naysayer, so what am I doing here? After watching another stream of HIT technobabble clog up my Twitter feed this morning, I thought it might be helpful to call out four technologies that have actually made a real difference over the last few years. Perhaps we’ll see something in there that will help others find their way to a positive outcome. Or maybe not. Let’s give it a try.

A. Patient Portals

Everyone loves to hate on patient portals. I sure did during the time I spent trying to make HealthVault go. After all, most of us interact with at least a half dozen different providers and we’re supposed to just create accounts at all of them? And figure out which one to use when? And deal with their circa 1995 interfaces? Really?

Well, yeah. That’s pretty much how business works on the web. Businesses host websites where I can view my transaction history, pay bills, and contact customer support. A few folks might use aggregation services to create a single view of their finances or whatever, but most of us just muddle through, more-or-less happily, using a gaggle of different websites that don’t much talk to each other.

There were three big problems with patient portals a few years ago:

  1. They didn’t exist. Most providers had some third-party billing site where you could pay because, money. But that was it.
  2. When they did exist, they were hard to log into. You usually had to request an “activation code” at the front desk in person, and they rarely knew what you were talking about.
  3. When they did exist and you could log in, the staff didn’t use them. So secure messaging, for example, was pretty much a black hole.

Regulation fixed #1; time fixed #2; the pandemic fixed #3. And it turns out that patient portals today are pretty handy tools for interacting with your providers. Sure, they don’t provide a universal comprehensive view of our health. And sure, the interfaces seem to belong to a long ago era. But they’re there, they work, and they have made it demonstrably easier for us to manage care.

Takeaway: Sometimes, healthcare is more like other businesses than we care to admit.

B. Epic Community Connect & Care Everywhere

Epic is a boogeyman in the industry — an EHR juggernaut. Despite a multitude of options, fully a third of hospitals use Epic, and that percentage is much larger if you look at the biggest health systems in the country. It’s kind of insane.

It can easily cost hundreds of millions of dollars to install Epic. Institutions often have Epic consultants on site full time. And nobody loves the interface. So what is going on here? Well, mostly Epic is just really good at making life bearable for CIOs and their IT departments. They take care of everything, as long as you just keep sending them checks. They are extremely paternalistic about how their software can be used, and as upside-down as that seems, healthcare loves it. Great for Epic. Less so for providers and patients, except for two things:

Community Connect” is an Epic program that allows customers to “sublet” seats in their Epic installation to smaller providers. Since docs are basically required to have an EHR now (thanks regulation), this ends up being a no-brainer value proposition for folks that don’t have the IT savvy (or interest) to buy and deploy something themselves. Plus it helps the original customer offset their own cost a bit.

Because providers are using the same system here, data sharing becomes the default versus the exception. It’s harder not to share! And even non-affiliated Epic users can connect by enabling “Care Everywhere,” a global network run by Epic just for Epic customers. Thanks to these two things, if you’re served by the 33%+ of the industry that uses Epic, sharing images and labs and history is just happening like magic. Today.

Takeaway: Data sharing works great in a monopoly.

C. Open Notes

OpenNotes is one of those things that gives you a bit of optimism at a time when optimism can be tough to come by. Way back in 2010, three institutions (Beth Israel in MA, Geisinger in PA, and Harberview in WA) started a long-running experiment that gave patients completely unfettered access to their medical records. All the doctor’s notes, verbatim, with virtually no exception. This was considered incredibly radical at the time: patients wouldn’t understand the notes; they’d get scared and create more work for the providers; providers fearing lawsuits would self-censor important information; you name it.

But at the end of the study, none of that bad stuff happened. Instead, patients felt more informed and greatly preferred the primary data over generic “patient education” and dumbed-down summaries. Providers reported no extra work or legal challenges. It took a long time, but this wisdom finally made it into federal regulation last year. Patients now must be granted full access to their providers’ notes in electronic form at no charge.

In the last twelve months my wife had a significant knee surgery and my mom had a major operation on her lungs. In both cases, the provider’s notes were extraordinarily useful as we worked through recovery and assessed future risk. We are so much better educated than we would otherwise have been. An order of magnitude better than ever before.

Takeaway: Information already being generated by providers can power better care.

D. Telemedicine

It’s hard to admit anything good could have come out of a global pandemic, but I’m going to try. The adoption of telemedicine as part of standard care has been simply transformational. Urgent care options like Teladoc and Doctor on Demand (I’ve used both) make simple care for infections and viruses easy and non-disruptive. For years insurance providers refused “equal pay” for this type of encounter; it seems that they’ve finally decided that it can help their own bottom line.

Just as impactful, most “regular” docs and specialists have continued to provide virtual visits as an option alongside traditional face-to-face sessions. Consistent communication between patients and providers can make all the difference, especially in chronic care management. I’ve had more and better access to my GI specialists in the last few years than ever before.

It’s only quite recently that audio and video quality have gotten good enough to make telemedicine feel like “real” medicine. Thanks for making us push the envelope, COVID.

Takeaway: Better care and efficiency don’t have to be mutually exclusive.

So there we go. There are ways to make things better with technology, but you have to work within the context of reality, and they ain’t always that sexy. We don’t need more JSON or more standards or more jargon; we need more information and thoughtful integration. Just keep swimming!

SMART Part 4: Healthcare data sucks, and FHIR is no exception

Welcome to the finale! While the full codebase has been available from the beginning, parts 1, 2, and 3 of the series have focused mostly on the logistics of getting a SMART app registered, authorized and running within the various EHR environments. After all that noise, you’d think you could just write your app in peace, but no. Why? Because healthcare data freaking sucks.

There are a few reasons for this, some legit and others not so much. It’s useful to understand the whys, so we’ll talk about them first. But if you’re going to write real apps, not just connectathon demos, you need a strategy for muddling through it, so we’ll do that too. With real code.

Health records are stories first

Consider your own health history for a moment. Even for the healthiest among us, there are childhood and adult vaccinations, a few broken bones, allergies that wax and wane, that weird skin thing you were embarrassed to ask the doc about, colonoscopies or mammograms… it’s a long list. If you’re part of the 60% of the population that manages a chronic condition, the list gets a lot longer. And it’s all important. The care you need today is a direct result of the care you’ve received in the past.

Now let’s make it concrete: tell me when you had your last tetanus shot. Maybe you have a patient portal somewhere with that data, if you remember which one and if you remember your username and if you had an hour to look for it. But in the ER today with a rusty nail stuck in your foot? Pick one: “When I was a kid;” “Not sure if I ever have had one;” “Two years ago when I was on vacation.”

At least currently, a booster is recommended if your last shot was more than five years ago (or ten if it’s a clean wound). So even with those vague answers, a doc can judge whether or not you need a shot — probably yes for the first two, and no for the last. Plus they can enter that into your record so that there is better information available for your next ER visit.

The point is that even “simple” medicine is hard, and any information is better than none when trying to make care decisions. Getting an extra tetanus booster is no big deal — but for other complex, difficult-to-diagnose conditions docs face every day, the stakes are much higher.

This is why medical dramas are always popular on TV. Every case is a detective story, piecing together life history and lab results and images and Medline searches until a consistent picture finally emerges. So while “got a rash from the flu vaccine” or “travelled to the rainforest as a teen” may not seem important in isolation, as clues to something larger they can be pivotal.

As I used to say a lot in my HealthVault days, comprehensive messy data trumps spotty clean data any day. This is uncomfortable for folks that create data models and apps. We like clean data. “As a kid” may be super-helpful to a doc wondering when you got the measles, but it’s (nearly) useless for a model trying to predict the optimal age to get a shingles vaccine.

We make it worse

It’s tough to build data models (and APIs) that support the narrative, messy data needed to deliver comprehensive care and serve as the basis for compute-based solutions, that’s for sure — and I certainly don’t have a silver bullet to offer. But again and again, the formats we create just make things worse by punting the issue. Everything can be null, everything can be a list, controlled vocabularies are just recommendations, you get the idea. Ironically given this series, I was and remain pretty blasé about FHIR itself because it is exactly the same. Sure it’s easier to parse and the documentation is excellent, but at the end of the day, pretty much the entire burden of interpreting the data gets dumped on the app developer.

To wit: pretty much every app in the world needs to display the patient’s name. These apps aren’t name experts. They just want the freaking name to show on a web page. But in the FHIR patient record:

  • There is a list of names (and it’s nullable).
  • Each HumanName entry in the list has a nullable NameUse constant, but the standard doesn’t have explicit priority rules for picking the best.
  • Each entry has a nullable Period, which if present might indicate that the name is no longer in use, after some more parsing work and simple but finicky computation.
  • The structure has a “text” element intended for display … but it’s nullable.
  • The family name used to be nullable array, but in later versions is a nullable singleton.

You might say that it’s the job of a client library to wrap up this complexity and make it usable. And to be sure, that’s exactly what I’ve tried to do with SmartEhr (more on this in a minute!). But that’s just a band aid over a lazy API. And because typically APIs support multiple client libraries across languages, you’re pretty much guaranteed that decisions about how to interpret the data are going to diverge between them. What a mess. Left to a developer looking to get through to the “real work” of their application, it can get ugly fast:

String name = patient.name.get(0).text;

This is real code I’ve seen out there. It might look good at the connectathon with fake data, but that silly little line has two potential NullPointerExceptions and one IndexOutOfBounds expression, and as a kicker may have picked the wrong entry. Sweet.

At this point in the program I’d like to remind you that we’re just talking about getting the patient’s name.

Anyways, I can complain all I want — but the API ain’t changing, so our only option is to build in some of this developer-friendly functionality at a higher level. Doing this well is quite tricky, because by definition the code is, to use the popular jargon, opinionated. Patient.bestName makes choices:

  • We interpret a null period to mean the name is active.
  • We “prefer” names in the order they appear in the HumanNameUse enum. (Even looking at this just now I’m wondering if I should have moved “anonymous” to the top of the HumanNameUse enum, since it’s intended to be privacy-protecting.)

SmartTypes

Making these helpers as clean and maintainable as possible is the job of SmartTypes.java — a collection of structures that parse raw FHIR resources from JSON and then selectively add helpers to improve the developer experience. This breaks down into three main bits:

  1. Defining the structures based on the specification.
  2. Adding FHIR-specific parsing logic in the form of JsonDeserializers.
  3. Designing and writing opinionated helpers.

Unlike comprehensive FHIR mappings like HAPI, SmartEhr is parsimonious about which resources it codifies and which fields within those resources are parsed out. This hurts a little, because I’d like to be helpful to everyone. But it takes careful thought in the context of a use case to do a good job, so I decided it was better to leave expansion to the future. If you add a resource or helper, please send me a pull request, and/or let me know if I can help think it through.

Defining the structures is relatively simple. Using the excellent FHIR reference material, just create a Java object with public fields that you care about associated with the types you want those fields to be. Sometimes the right type is obvious and sometimes it’s a little more complicated — the key is to pick something that is going to be the most useful to a developer. This is a hierarchical exercise, as you may have to define new “primitive” structures along the way.

The FHIR standard has evolved over the years. But because the changes from DSTU2 to R4 have been pretty subtle, I’ve been able to mostly abstract them away for developers. This provides nice flexibility if an EHR hasn’t stayed current, but may end up being too much of a hassle to be worth it long-term — we’ll see.

When it comes to parsing the json into these objects, I can’t say enough good things about the Gson engine — SmartEhr uses it extensively throughout. Each top-level structure is given a static “fromJson” method that basically asks Gson to do the work (e.g., Patient.fromJson). We add two bits of goodness to this process ourselves.

First, we do a bit of post-processing on the parsed object to make it easier to use. Primarily we use this to sort lists in order of descending priority (where “priority” is something I’ve just done my best to assess where the standard isn’t forthcoming). You could imagine taking this post-processing further, for example adding dummy elements to empty lists to avoid the need for annoying null checks, but I haven’t gone there.

We also add parsing logic to patch some FHIR quirks and novel types. This logic is implemented as a set of JsonDeserializer classes attached to our Gson parser:

  • The “date” and “dateTime” FHIR types are parsed into LocalDate and ZonedDateTime respectively. This is a classic optionality situation; each FHIR subject allows for increased precision (e.g., YYYY, YYYY-MM or YYYY-MM-DD for dates) based on what is known. Since developers most often want to compare dates, this is hard to work with — these deserializers default missing values to get to something reasonable. Be careful here though! “1990” isn’t really the same as “1990-01-01” … use case matters.
  • For the Condition type, it’s easier to deal with ClinicalStatusCode and VerificationStatusCode as enums rather than structures, so we clean them up. Similarly, we generate a clean set of ConditionCategoryCodes to reduce developer friction.
  • In a few places, string arrays have been changed to strings between versions. The LaxStringDeserializer makes that go away by space-concatenating arrays if needed.

These steps get us pretty far — the objects are clean and validated and in many cases can be used as-is. But as we discussed earlier, there’s still a ton of room for interpretation. The helpers we’ve added to the structures wrap this up, and so far fall into a few buckets:

  • Picking of the “best” from a nullable list of items, such as in Patient.bestAddress or bestName. This is generally a combination of ordering the “use” constants and checking for valid periods (Period.current).
  • Condition.bestGuessOnset is a particularly interesting one, and my least favorite helper implementation. Onset may be recorded as a dateTime, but the standard also allows it to be given as an Age, Period, Range or free-text String. I really should be at least trying to use those alternate types, but as yet have not.
  • Weeding through obsolete or erroneous entries, as in Condition.validAndActive. Of particular note here is the validation status — if something is “entered_in_error” it is typically not removed from the record, just marked as such. These can easily lead a naïve developer astray if not handled.
  • Sometimes it’s just nice to have an easy way to display items onscreen; helpers like Address.display and HumanName.displayName take care of this.

SmartTypes is definitely just the seed of a complete developer-friendly data model for this stuff. But I hope that the general pattern will make it easy for myself and others to grow it as needed.

Enough Said

Well, I think that about does it. These posts have been dense and wonky, but I’ve tried to include enough color that readers come away with two uber-points:

  1. SMART on FHIR (not just FHIR) is a transformative technology for clinical care. There is no better way to get your software innovation into clinical practice.
  2. SmartEhr and SmartServer are simple, production-class, almost dependency-free, license-free libraries that can help you build your SMART on FHIR app quickly and with a minimum of hassle.

Please let me know how I can help. Send me bugs (they are surely there) and pull requests. I would be super-excited to see apps based on these libraries make it into the EHR app stores. Go do that!

*** A last note and request (this will show up at the bottom of each article in the series). I’ve spent a lot of time in this industry, and the systemic impediments to progress and innovation can make even good folks feel hopeless sometimes. I really, truly believe that SMART is one of those rare technologies that has matured at exactly the right time to change the game. But there’s no guarantee — not enough folks know about it, and it’s too hard to use. If you swim in this pool, please help me fix that:

  1. Share these articles with folks that use and implement EHRs. Tell them to look at the “app store” for their system and add an app to their test system. Tell them to ask vendors if they have a SMART interface to their solution.
  2. Share these articles with folks that build care delivery solutions. Explain how they can use SMART to add functionality for customers without a custom login and without having to do an integration project with custom IT teams.
  3. Contact me if I can help. There’s a form here on the website, and I’m @seanno on Twitter, or use LinkedIn, or whatever. I’m happy to answer questions, make some connections, and heck I might even write some code for you if it makes a difference.

SMART Part 3: The launch sequence

There are a lot of moving parts in the SMART-on-FHIR game, but the launch sequence is where the magic really happens. The launch sequence provides your SMART app with automated login as well as user, patient and encounter context — everything necessary to create a friction-free and natural user experience. In this installment of the SMART on FHIR saga, we’ll walk through the process in detail.

Of course, the whole reason I wrote SmartEhr/SmartServer was to handle stuff like this for you. If I’ve been successful, then this article may not be that relevant. Still, it’s always useful to know what’s going on under the covers, and hopefully Google will leave a trail here for folks struggling to build their own implementations.

The Big Picture

SMART launch is actually covered quite well in the official documentation. It’s a bit broad for our purposes though (we only care about “EHR Launch”) and leaves out a bunch of implementation-dependent quirks that can easily trip things up. In short, EHR Launch proceeds like this:

  1. Within the EHR user interface, the user navigates to a SMART app using a link or button set up by the EHR administrator. This opens up a web browser, typically within an iframe. The browser is directed to a pre-configured “launch” URL hosted by the SMART app with some magic parameters appended to the query string.
  2. Upon receiving this request, the SMART app confirms the source EHR that initiated the sequence, then redirects the browser back to the EHR with query string parameters that confirm the identity of the SMART app and request access to the specific data types required.
  3. Next the EHR performs final verification. In some launch scenarios the EHR also asks the user (through UX shown in the iframe) to login and/or select a target patient — but this does not happen with our embedded “EHR Launch” scenario, because that context is already set up (kind of the point of all this). The EHR redirects the browser one more time to the SMART app, adding a short-lived authorization code to the query string.
  4. The SMART app makes a back-end request (independent of the browser) to the EHR, exchanging the authorization code for an “access token” and user/patient context.
  5. Finally, the SMART app renders its user experience in the iframe created so very long ago. The app retrieves EHR data using the access token and FHIR resource URLs, and does what it does. Excepting refresh token management, that’s where the story ends, with happy users and hopefully some innovative care workflows!

For those that need a little more jargon in their lives, all this is basically an OAuth2 exchange. We’re going to dive into each step in much more detail below … buckle up!

Setup: URLs

The security of a SMART app connection depends on each system knowing where the other one lives. In step #3 above, the EHR will only send the authorization code to a “return URL” that is preconfigured for the app. Each EHR system has its own interface for managing this configuration; we walked through the Epic and Cerner flavors in the previous article in the series. Once you’ve seen one, the others will feel very familiar.

Similarly, the SMART app needs to know which EHRs are trusted and where they live. Each app can do this however they like; for SmartEhr we use a JSON configuration file that contains one entry for each trusted EHR. For example:

{
    "SiteId": "EpicSandbox",
    "IssUrl": "https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4",
    "ClientId": "3b0e5c6a-e5df-438f-b654-864aed042ce0"
}

The “IssUrl” is the FHIR base URL for the relevant EHR instance; it must match the “iss” parameter that comes in on the “launch URL” we’ll talk about later. These two intertwined settings provide the basis for secure exchange of information. Hopefully it goes without saying that these need to be trusted HTTPS URLs for it to work as intended, but hey I’ll say it anyways.

The “ClientId” is assigned to the SMART app by the EHR, and also must be present in order for the exchange to work properly — you’ll find it in the same portal you use to set your return URL and such.

A couple of things to note about site configuration in SmartEhr. First, it is conceivable that you might have two EHRs using the same IssUrl setting — for example in a cloud-hosted multitenant situation. In that case, the EHR “launch” URL must be configured to also pass a “siteid” parameter that matches an entry in the SmartEhr configuration (SiteId values are arbitrary but should be unique). This does not change the requirement that the IssUrl value must match what we see on the launch URL; it simply allows us to differentiate between multiple tenants.

Second, the IssUrl does double-duty. It is the FHIR base URL for the EHR — that is, the root of the URL that we will use to query for data about the patient. But it also hosts an endpoint that contains “metadata” information about the EHR’s capabilities. Most importantly, this metadata contains two additional URLs: The “authorization” URL is the basis of the redirect in Step #2 above, and the “token” URL is used to fetch (and refresh if allowed) access tokens in Step #3.

SmartEhr pre-loads these URLs on a per-IssUrl basis at startup. By default we use the “/metadata/” well-known endpoint for this purpose. An alternative mechanism using the endpoint “.well-known/smart-configuration.json” can be selected by including the value “UseWellKnownMetadata” = true in a site configuration; there isn’t really any advantage to one or the other. If for some reason an EHR doesn’t support either of these endpoints, the URL values can be hardcoded in the site configuration with the values “AuthUrl” and “TokenUrl”.

Setup: Scope

As with the overall launch sequence, there’s pretty good documentation for launch scopes on the HL7 site. It’s just that there is a LOT there and it can be hard to navigate, so we’ll add a bit of color commentary.

Scopes are an OAuth2 concept that enable granular access to a user’s data; each scope is an application-defined string that represents some logical subset. In the FHIR case, an example is “user/Condition.Read” which is the scope required to look at (but not modify) a condition record for any patient that the EHR user has access to.

In many typical OAuth2 scenarios, the logged-in user is presented with a list of the scopes needed by an application at runtime, and explicitly grants or denies access to them. (They can even choose to grant some of the requested scopes but not all — a seemingly nice bit of flexibility that in reality just causes most apps to fall on their face.)

Our EHR launch scenario is a little different, in that the scopes needed by the SMART app are pre-configured up front on the EHR’s developer site. Theoretically an EHR could still show these values to the user for approval, but in my experience that doesn’t happen. This is actually much nicer (in this specific scenario) for the actual EHR user, but does create some redundant noise in the protocol that’s a little annoying.

These are the key scopes that you’ll likely run into:

ScopeNotes
launchRequired for the EHR launch scenario, this scope gives you access to the “context” of the EHR — who is the patient and what is their current encounter.
openid fhirUser user/Practitioner.readProvides the identity (in the form of a FHIR resource) of the logged-in user (openid fhirUser) and the rights to retrieve that resource (user/Practitioner.read). This is important if, for example, you need to know the user’s email address or NPI number.
patient/Patient.readDemographic details for the in-context patient. Replace “Patient” after the slash with other resource names as needed (e.g., Condition, MedicationRequest).
user/Patient.readAs above (including the use of other resource names), but granting access to all resources of that type available to the user (i.e., across many patients).
[patient/user]/[Resource].writeEach of the “.read” scopes has a corresponding “.write” version. I haven’t focused on these in SmartEhr so far, but it’s a simple extension and perhaps a good blog post someday.
[patient/user]/[Resource].searchUsing the same pattern as for read, the “.search” suffix allows parameterized queries for resources. I find this one annoying, because the official documentation says it should just be read and write to encompass both actions, but we live in the world as it is.

The patient/… and user/… scopes technically support wildcard syntax (e.g., patient/*.* to read and write everything about the in-context patient). A useful concept, but somewhat frowned upon even in the official documentation — and Cerner doesn’t even implement it. So despite the mess that comes with lengthy scope strings, better to avoid the wildcards.

Frankly getting these scopes right, and in sync with the EHR’s developer portals, can be tedious and a bit frustrating. For example, it took me longer than it should have to figure out that to get a list of conditions for a patient, Cerner wants “Condition.read” while Epic wants “Condition.search”. Combine this with the fact that there is often a time lag (of indeterminate length) between editing your configuration and having it apply in the sandbox and, well, ugh.

Another example: in the R4 API version Epic has broken down Conditions into different types (Problems, Health Concern, Genomics, etc.), and will fail searches that don’t include parameters to filter to what the app has access to. Why? I can understand adding granularity to the permissions model, but then just return the records the app can see. Healthcare standards, man.

SmartEhr always sends “launch openid fhirUser” in the scope string. Additional scopes are added based on the value of the “Scope” parameter in the json configuration file.

Refresh Tokens

Less common, but important scopes if you will have long-running sessions, are “online_access” and “offline_access”. These both ask to enable “refresh tokens” for long-lived access to EHR data. In my experience, EHR vendors are (understandably) not super-hot on this concept, but it can be useful and it is implemented in SmartEhr. Refresh behavior basically works like this:

  • App configuration at the EHR developer site must indicate a need for refresh tokens. This will also require the app to have a “Client Secret” which is kept private (in the case of SmartEhr, in the site configuration json).  
  • The “online_access” or “offline_access” scope is provided during launch, indicating to the EHR (ahem, again) that you need a refresh token. The difference is that “online_access” provides a refresh token that is valid only as long as the currently logged-in EHR user stays logged in. “offline_access” persists indefinitely (more or less).
  • The app will receive a refresh token in addition to the access token in Step #4 of the “Big Picture” described at the top of this article.
  • If the access token expires, a new one can be fetched by sending the refresh token and the client secret to the EHR’s “TokenUrl” endpoint. Successful return will also include another refresh token so the app can rinse and repeat as needed.

“online_access” seems like a great idea for an app that stays interactive with the user, because otherwise there’s no way to ensure synchronization with the EHR’s login timeout. But in my experience, implementation is really spotty and I’ve literally been told by one vendor to “just not worry about it.” It’s true that the access token you’re granted generally will outlast a typical session, so that’s not crazy, I guess. Feels messy.

“offline_access” is perfect for surveillance apps that will periodically look for new data, or for writing data back to the EHR that may come later — for example, the user may ask for some long-running computation and have the results written back as a PDF document.

Once things are set up in the EHR developer portal, you can enable refresh tokens in SmartEhr by adding the appropriate values to the json configuration. For example at lines 3 and 9 below:

{
    "Smart": {
        "Scope": "... [on/off]line_access",
        "Sites": [
            {
            "SiteId": "EpicSandbox",
            "IssUrl": "https://fhir.epic.com/interconnect-fhir-oauth/api/FHIR/R4",
            "ClientId": "3b0e5c6a-e5df-438f-b654-864aed042ce0",
            "ClientSecret": "SECRET_SHARED_WITH_EHR"
            }
        ]
    }
}

The Launch URL

I can’t believe we’re this far into this and haven’t even actually started the launch. But we’re here at last. All the code for this part is in SmartEhr.launch; it might be helpful to have that open to refer to along the way.

Looking back at the “Big Picture,” Step #1 starts with the EHR sending a browser to the app’s launch URL as configured in the EHR developer portal — this is what we’ve simulated in the last two articles using the various EHR sandbox links. The EHR adds two query string parameters to this URL:

  • iss: the FHIR base URL for the EHR, corresponding to the “IssUrl” values in the json config.
  • launch: an opaque (to the app) string that identifies the EHR session and context.

The first thing we do is to allocate a new Session that we’ll maintain throughout the app’s lifecycle. It starts out pretty empty, but great things are in store for it.

Next, we need to find the correct SiteConfig for this EHR. Back in the Setup section we talked about the optional “siteid” query string parameter. We’ll use that value to look up the SiteConfig if it’s present, or if not will use the “iss” parameter. Either way, we succeed only if we can identify exactly one matching config and the provided “iss” query string parameter matches our configured value for the site.

Finally, we need to build up a URL where the EHR will confirm all is well before giving us the keys to get an access token. We pile a ton of query string parameters onto this URL:

  • response_type: always the string “code”.
  • aud: always (for our scenario) the “IssUrl” value in the SiteConfig.
  • client_id: the “ClientId” value in the SiteConfig.
  • redirect_uri: The URL that we would like the EHR to send the browser back to after confirming things are OK. This must match the value pre-configured in the EHR developer portal.
  • launch: the same value we received on the query string from the EHR. Round-tripping this value lets the EHR associate our request with the initial launch.
  • state: our growing “Session” object, dehydrated into a string. Just as “launch” helps the EHR keep track of context through this negotiation, “state” lets us do the same and we’ll see it again later. It’s important to keep tabs on how big this value is — as I found out the hard way, beyond 4k some browsers (and EHRs) will puke.  
  • scope: as described in the “scopes” section, a space-separated string containing all of the scopes the app requires to run. I find this frustrating — the EHR already knows what scopes we need because we configured it back at their developer portal. It makes sense for other launch types, but for us it just means keeping two lists in sync for no real benefit. Ah well.

The launch method returns this complete URL to the caller to execute the redirect. Note that throughout all of this, “SmartEhr” is the headless engine that can be used within any web framework. If you’d like to see how that engine is used in an actual web context, take a look at SmartServer, in particular for this stuff SmartServer.registerLaunchHandler.

Successful Authorization

After the EHR decides that everything we just sent is OK, they send the browser back to the app’s return URL (the “redirect_uri” as configured in the EHR developer portal and sent in the redirect_uri query string parameter above). The EHR adds two parameters to this URL:

  • code: a short-lived token that we’ll trade for our real goal, an access token.
  • state: The same value we sent to the EHR in the last redirect.

The code for this phase is in SmartEhr.successfulAuth. We first “rehydrate” the Session using the state parameter, which provides the link back to the correct site configuration. Next up is a backend call (i.e., not using browser redirects) to the EHR to swap “code” for an access token. This call is a POST to the “TokenUrl” value in the SiteConfig, including the following form elements (in normal application/x-www-form-encoded format):

  • grant_type: always “authorization_code”
  • code: the code parameter we received on the query string
  • redirect_uri: the same redirect_uri we sent in the last phase. Don’t really get this one, but hey.

If we’re going to request refresh tokens, we send this request with a Basic Authorization header where the “user” is the configured ClientId and then “password” is the ClientSecret. If not, we just append ClientId to the query string as client_id.

This is the first time we’ve made a direct request from our app to the EHR, rather than going through the browser. For all of these, be sure to set the “Accept” header of your requests to “application/json”. Without it, Epic will return results in XML and Cerner won’t return anything at all. To be fair, this is buried in the spec, but come on now.

There is a ton of great stuff in the payload returned by this call; SmartEhr sifts it out in SmartEhr.parseTokens. For starters it includes the basic authorization stuff: the access token, an expiration timestamp for the token, and if appropriate a refresh token. We also get an identifier for the in-context patient, and a Boolean value “need_patient_banner” that you shouldn’t ignore. If this value is true, it means the app’s web page is not embedded in the EHR with visible patient context, so the app should prominently display the patient’s name and possibly other demographics such as birthdate. Users engaged in clinical care move quickly; accidentally working with the wrong chart can have disastrous results so this is a real safety issue — make sure your app is a good citizen!

The last piece of data we extract from this payload is the FHIR Resource identifier (a relative URL under the IssUrl) for the logged-in user. This value is buried in the “id_token”, which is a JSON Web Token string. There are a few steps to decoding this value, but ultimately we pull the value out of the “fhirUser” value in the token json. I’ve found EHRs to be inconsistent in returning relative or absolute URLs in this value; SmartEhr normalizes it so developers can count on a consistent format.

All of this goodness gets stored in the Session object that is then returned from the successfulAuth method. It’s important that this object be available in each request made by the logged-in user’s browser, so it needs to be stored in some kind of session manager. SmartServer accomplishes this in the registerReturnHandler method by storing the dehydrated Session in a session cookie, but whatever your web environment uses for session management is fine.

With an in-memory session manager, you can just store the object itself and be done with it. However, in production it’s more likely that you’ll use some sort of session that works across machines (the cookie that SmartServer uses is one example of this). In that case, you’ll need to be aware that the Session contents may change during a request — for example, if the access token expires and is refreshed. Each request that uses the Session should be wrapped with something like this:

String sessionData = getSessionDataFromMySessionManager();
SmartEhr.Session session = mySmartEhrInstance.rehydrate(sessionData);

// code does stuff with session

sessionData = mySmartEhrInstance.dehydrateIfUpdated(session);
if (sessionData != null) storeSessionDataInMySessionManager(sessionData);

You can see an example of this happening in the implementation of SmartServer.SessionHandler. SmartServer apps just inherit their handlers from SessionHandler and things are managed behind the scenes. Whatever web framework you use almost certainly has a similar mechanism to add pre- and post-actions to your handlers.

This seems a good time to reinforce that the SmartEhr object itself wants to be a long-lived process singleton, rather than something created on each request. This comes down to all of the metadata requests that happen on creation of the object. The singleton nature isn’t enforced, but maybe it should be. I’ll think about that.

Apps doing App Things

Anyways — at last, the app can just be an app. Each request has access to an authorized Session object, and can use it together with the global/singleton SmartEhr object to request FHIR data in whatever way and pattern makes sense.

I’ll be honest, I can’t imagine that anyone actually has read this far. That’s fine — the point here was to be super-comprehensive and call out all the little quirks that make developing from the spec a challenge. Google, please serve it up to those in need.

And I suppose maybe walking through all the grotty details will encourage you to just use SmartEhr, and maybe even SmartServer too. If you do, and especially when you find bugs, let me know!

Next up (and maybe the last, not sure) in the series, we’ll talk about working with FHIR resources and dealing with the challenges of extreme optionality in healthcare standards. Follow me on Twitter or LinkedIn or whatever to get a ping when that one comes out.

*** A last note and request (this will show up at the bottom of each article in the series). I’ve spent a lot of time in this industry, and the systemic impediments to progress and innovation can make even good folks feel hopeless sometimes. I really, truly believe that SMART is one of those rare technologies that has matured at exactly the right time to change the game. But there’s no guarantee — not enough folks know about it, and it’s too hard to use. If you swim in this pool, please help me fix that:

  1. Share these articles with folks that use and implement EHRs. Tell them to look at the “app store” for their system and add an app to their test system. Tell them to ask vendors if they have a SMART interface to their solution.
  2. Share these articles with folks that build care delivery solutions. Explain how they can use SMART to add functionality for customers without a custom login and without having to do an integration project with custom IT teams.
  3. Contact me if I can help. There’s a form here on the website, and I’m @seanno on Twitter, or use LinkedIn, or whatever. I’m happy to answer questions, make some connections, and heck I might even write some code for you if it makes a difference.

SMART Part 2: A real app in less than 100 lines

In the first article of this series, I argued that anyone building or using healthcare technology should be excited about and embracing SMART on FHIR, right now. By integrating tools and workflows directly into the EHR user experience, SMART sidesteps many of the problems that have historically stymied innovation in care delivery. It’s super cool.

However, building a truly production-ready SMART app from scratch is a big lift. Much of this comes down to excessive optionality in the standards; trying to serve every scenario makes it painful to do simple and common things. That’s where this project comes in: SmartEhr and SmartServer are real, concrete, license-free Java classes that you can use to dramatically accelerate SMART development.

Time for the rubber to meet the road. First, we’re going to build a real, production-class SMART app using this framework using one class and just under 100 lines of Java. Future articles will go deeper; for now we’re just going to get some code running and get comfortable with sandbox environments.

XrayServer

Xray was built specifically for this walkthrough, but it’s actually generally useful for investigating resources and seeing FHIR data on the wire. After going through the launch sequence, the landing page makes it easy to call FHIR resources and view the JSON output. Think about it as a baby version of Postman for SMART.

1. Get the code

Your development machine will need reasonably recent copies of the JDK (I’m using OpenJDK v11, but it should work with any JDK >= v8); Maven and Git. You probably have them already, but if not come back when you’re good. I’ll wait here.

All the code you need is in the shutdownhook repository on github. Use the green “Code” button on that page to get the code — either by cloning it or just downloading a zip and unpacking it. There are a few projects in the repository but you only care about these two:

2. Build it

Use the commands below to first build the toolbox code and install it into your local Maven repository, then build the smart-trials project. The end result is a “fat” jar in the target directory that has no external dependencies and includes main() entrypoints for XrayServer and the ClinicalTrialsServer we’ll cover later in the series.

cd YOUR-SRC-DIR/shutdownhook/toolbox
mvn clean package install
cd ../smart-trials
mvn clean package
ls -l target/smart-trials-1.0-SNAPSHOT-jar-with-dependencies.jar

If you have any problems building, let me know. I work pretty exclusively in Linux (as nature intended) so sometimes miss Mac or Windows weirdnesses. Happy to fix that up if it happened here; better yet send a pull request!

3. Run it!

The jar is bundled with a “get started” Xray configuration as well as a self-signed “localhost” certificate that allows the HTTPS server to manage browser exchanges. This makes it easy to get started with development, but obviously neither of them are production-ready. That’s ok for our purposes today.

Run the server with these commands:

cd YOUR-SRC-DIR/shutdownhook/smart-trials
java -cp \
    target/smart-trials-1.0-SNAPSHOT-jar-with-dependencies.jar \
    com.shutdownhook.xray.XrayServer

The server will start listening for requests on port 7071. Point your browser at https://localhost:7071/ and go through the motions of accepting the scary untrusted certificate — you need to do this now so that the redirect and iframe negotiations work later on. Also, if you’re running a firewall on your server, make sure to open up port 7071. On CentOS 7, I use something like:

firewall-cmd --zone=public –-permanent --add-port=7071/tcp

Now point your browser at the SMART App Launcher (https://launch.smarthealthit.org/). There’s a ton of options on this page, but just leave them alone and scroll down to the bottom. Enter https://localhost:7071/launch into the “App Launch URL” box and click “Launch App!”

MAGIC TIME. If all goes well, you’ll be prompted to select a practitioner and a patient, then land on a screen that looks like the screenshot to the right. All that noise on the page doesn’t really matter, but feel free to click around and get a feel for what FHIR resources look like. Sweet!

What Just Happened?

The App Launcher acts as a fake EHR system. By providing the /launch URL, you’re telling that EHR to open up Xray in an iframe, adding some identification parameters to the query string. Xray verifies that these look good, then redirects the browser back, saying “please give me the ok to get an access token.” The EHR ensures a patient and provider context is set up, and once again sends the browser back to Xray for some final housekeeping and the landing page.

Beyond the logistics, the most important part of this dance is agreement on the types of data that Xray requires to do its job. In the real world, this information is pre-registered with the EHR (this is skipped in the SMART App Launcher) and confirmed during the negotiation. As a testing/inspection tool, Xray makes a pretty broad request for data types, but restricts itself to “read-only” operations.

The launch negotiation is encapsulated in the launch and successfulAuth methods of SmartEhr, and specified quite well in the SMART documentation for the masochists in the audience. We just need to make sure the configuration is set up correctly.

Epic Time

The SMART App Launcher lets us skip a lot of that configuration, which is super-convenient for getting started, but leaves a bunch of gaps for apps intended for the real world. Each vendor has their own developer program which interprets the specification just a little differently. Our first target is Epic because it is by far the dominant EHR system in the United States.

Epic on FHIR is the homepage for the Epic development program. Sign up (for free), click “Build Apps” and then “Create” to set up Xray as an application in your account, using the following guide to fill out the form:

  • Application Name can be anything you like. For a production app, this information will be published in the Epic directory, but we’re never going to launch this app, so anything works.
  • Application Audience should be “Clinicians or Administrative Users.” SMART apps can work in many scenarios, but we’re focused on tools for providers, embedded in the EHR.
  • Incoming APIs is where we set our required access to data.
    • All access types are tagged with a FHIR API version: DSTU2, STU3 or R4. SmartEhr supports all of these, but we’ll just use the “R4” versions for Xray.
    • Every SmartEhr app needs at least Patient.Read access.
    • If you need information about the logged-in provider (such as their email address), you’ll need Practitioner.Read.
    • The static links on the Xray landing page include shortcuts to the patient’s problem list and medications, so also include Condition.Search (Problems) and MedicationRequest.Search (Orders).
    • Add any other data types you’re interested in as well; you can make these requests from Xray using the “Custom” input box.
  • Redirect URI should be https://localhost:7071/return (note the https:// prefix is included in the dropdown already).
  • Xray does not Require Refresh Tokens so leave this checkbox blank.

Click the “Save” button, then scroll to the bottom of the page where some new options will appear:

  • SMART on FHIR Version should be set to “R4” as discussed above.
  • Summary, like application name, is important for published apps but can be anything for Xray.

Leave the rest of the fields as they are and click “Save & Ready for Sandbox”. This will send you back to the application list; if you click on your new app, you’ll see “Client ID” and “Non-Production Client ID” fields at the top of the page. Your app is ready to go!

Before we move on, two side notes. First, there often is a delay between editing settings for your application and seeing them live in production. I have wasted a ton of time thinking my changes had no effect, only to have them light up ten or fifteen minutes later. Be patient!

It’s also worth touching on the publication process for FHIR applications so that they can be used at real Epic sites. The simplest way to get this done is do simply mark your app as ready with the “Save & Ready for Production” button. You can then share your “production” and “non-production” (for test systems) Client IDs with interested Epic customers — details are in the Epic documentation.

If you want to show up in Epic’s “App Orchard” directory of applications, life is a little more complicated (and expensive). You’ll have to join the Epic App Orchard program and go through a process whereby your application is approved for publication. You may also need to be in this program if your app needs to access data not part of the current FHIR standard — for example, at least as of this writing, billing and insurance information is only available through custom Epic APIs beyond FHIR.

We’re almost there. Epic knows about Xray, but Xray doesn’t know about Epic. Back on your development machine, fire up an editor and open the file YOUR-SRC-DIR/shutdownhook/smart-trials/src/main/resources/xray.json. In the “Sites” array, you’ll see an entry for “Epic” — replace the “ClientId” value with your “Non-Production Client ID.” Rebuild the jar and re-launch Xray to pick up the new configuration.

OK — let’s run this thing! Back on the Epic on FHIR site, use the “Documentation” dropdown and choose “Launching your App from Epic.” Click the “Try It” button, select a test patient, choose your app in the list, set the launch URL to https://localhost:7071/launch (just like with the SMART App Launcher), and click “Launch.”

MORE MAGIC TIME. The Epic sandbox doesn’t embed your app in a simulated EHR like we saw with the SMART App Launcher, but the Xray page should look very similar. This is pretty cool — the exact same code that works with the SMART App Launcher is working with Epic as well!

I’ll see your Epic and raise you a Cerner

The other behemoth in the EHR world is Cerner — most notable recently for winning a $16B contract to be the EHR for the Veteran’s Administration. Cerner also happens to be home to one of my all-time favorite developers, so woot for that.

Cerner’s SMART developer program is similar to Epic’s in a lot of ways; you can sign up for free and use their “Code Console” to set up and test applications. Once you’re logged into to the console, choose “My Apps” and then “New App” to get started. Not surprisingly, the information you provide is nearly identical to Epic — the only extra question is about OAuth2, to which you should say YES.

The interface for selecting access looks familiar too, but here we see a distinction between “User Scopes” and “Patient Scopes.” The difference is explained in the SMART specification, but in short “user” means any data the logged in user has access to (possibly including many patients), while “patient” is limited to data for the one patient that was selected in the EHR for use in your app. Usually we’re going to want the “patient” versions; an exception is user/Practitioner.read which is required to get information about the logged-in user.

Cerner has also chosen to disallow “wildcard” scopes for their FHIR applications (the SMART reference linked above talks about these), so if you choose additional data types for use in Xray, you’ll have to update the “Scopes” string in your xray.json config file to match.  

After creating your application, clicking it from the “My Apps” list will show a summary page that includes your Client Id. Copy this value and add it to the xray.json config file under Sites/Cerner. Also review the “FHIR Spec” link on that page; you may (or may not) need to update the “IssUrl” config setting with that value. Remember to re-build and re-launch Xray to pick up the new configuration.

EVEN MORE MAGIC TIME. Click on the “Begin Testing” button and use the provided demo credentials to log into the Cerner Sandbox EHR. After the same now-familiar negotiation, Xray will show up connected to Cerner data. Three for three, woo hoo!

Enough Said (for today)

As a SMART app developer, we’ve done a whole lot here with very little code. I thought “under 100 lines” was a catchy way to say that, but the meat of the app is really less than fifty — three functions that each receive authenticated SmartEhr and Session instances, ready to make FHIR requests. No muss and no fuss. In the next article, we’ll look in more detail at the launch sequence. Things are getting increasingly nerdy folks. Just the way I like it.

*** A last note and request (this will show up at the bottom of each article in the series). I’ve spent a lot of time in this industry, and the systemic impediments to progress and innovation can make even good folks feel hopeless sometimes. I really, truly believe that SMART is one of those rare technologies that has matured at exactly the right time to change the game. But there’s no guarantee — not enough folks know about it, and it’s too hard to use. If you swim in this pool, please help me fix that:

  1. Share these articles with folks that use and implement EHRs. Tell them to look at the “app store” for their system and add an app to their test system. Tell them to ask vendors if they have a SMART interface to their solution.
  2. Share these articles with folks that build care delivery solutions. Explain how they can use SMART to add functionality for customers without a custom login and without having to do an integration project with custom IT teams.
  3. Contact me if I can help. There’s a form here on the website, and I’m @seanno on Twitter, or use LinkedIn, or whatever. I’m happy to answer questions, make some connections, and heck I might even write some code for you if it makes a difference.

SMART Part 1: Sneaking Innovation into Care Delivery

There’s no shortage of innovation in Healthcare… sort of. Better to say that there’s no shortage of innovation in diagnosis and treatment. The drugs and tests and equipment coming to market these days are stunning. But “Health IT” — the software and systems that coordinate and manage the delivery of care — not so much.

Which isn’t to say that there aren’t good ideas — it’s just super-hard to actually get them implemented and into use. Enormously complex “Electronic Health Records” from companies like Epic and Cerner rule the roost, and don’t generally make it easy to share the sandbox. I have some sympathy for the challenges they face. Healthcare is a weird confluence of science and art and building software to support it efficiently and accurately is just a hard job. Love them or hate them, EHRs are where healthcare workflow happens, period, full stop. When you spend up to $1B to install software (usually less, but looking at you, Mayo) … you’re gonna use it.

All of this makes it really tough to introduce new software into a healthcare environment. IT departments visibly pale at the suggestion. Users are rightfully resistant to logging into yet another app with yet another interface. Even getting over all that, data integrations are insanely expensive and difficult, so double-entry of information (and the mistakes that come with it) are rampant.

The Big Idea

So how do you impact care with software? Don’t fight city hall — instead, find a way to wedge into the EHR, where folks live and breathe. Of course, that’s historically been much easier said than done, thanks to a lack of integration standards and enthusiasm for the concept amongst EHR vendors. (This “lack of enthusiasm” has been so problematic that language in the federal Cures Act actually calls it out!)

Viva the Revolution! Back in 2009, Zak Kohane and Ken Mandl wrote a landmark article in the NEJM titled “No small change for the health information economy.” The full text is behind the journalwall, but in short they said, “We need an iPhone App Store for Health IT and we’re going to figure out how to make it happen.” This kicked off a series of events that ultimately have come together as SMART on FHIR.

SMART on FHIR

At its core, SMART on FHIR (I’m just going to call it SMART from now on) provides a way to embed your own application into the context of an EHR experience. It’s a combination of technologies that let you:

  • Share the EHR’s login context, avoiding extra passwords and double-login.
  • Exchange data (read and write) with the EHR, avoiding duplicative data entry or storage.
  • Appear within the EHR user interface, providing a comfortable look-and-feel for users.
  • (Not strictly part of FHIR but key) Publish your applications in EHR “app stores” so users can find and, more importantly, install them without a heavy IT lift.

Sounds awesome. And it is, but fair warning: over the course of these articles I’m going to complain a lot about how SMART works. I mean, A LOT. So let me reinforce that I truly believe that it can and should be “the” transformative technology that breaks the juggernaut on Health IT innovation. I love it. I love the people who thought of it. I love the developers I know who have worked on it. I love that ONC has embraced it. It is awesome. OK.

That’s a good setup for the first big challenge. SMART is SUPER-broad — provider and patient apps, online and offline access, real-time and batch data, all served up with a big helping of optionality and vendor-specific noise that can make it hard to see what I believe is the much more targeted but revolutionary opportunity:  Get third-party, real-time workflow innovation for caregivers into the EHR.

Seriously, every single company trying to impact care delivery needs to start thinking about SMART as their primary user interface. Sure, you’ll probably need to have a “lowest common denominator” version that stands alone — but treat that as a backup, not your lead. I promise that if you demo your solution embedded with shared login and no separate UX, you’ll get farther faster every time.

How Can I Help?

Lack of awareness is not the only thing hampering SMART adoption. The excessive optionality that dogs pretty much every healthcare standard is an anchor around SMART’s neck as well, making it unreasonably hard to do simple things like access a moderately clean list of a patient’s current health conditions, or even their preferred name (!!!).

My goal is to eliminate as much of that complexity as I can, so that building a SMART app is trivial. “ShutdownTrials” is a complete, standalone, 99% dependency-free (it does use Gson) project that tries to do this in three acts:

  1. SmartEhr: a headless Java-based library for implementing SMART in any JVM-based application environment with minimal effort.
  2. SmartServer: building on SmartEhr, an extensible, secure web server that handles all of the HTTPS interactions required to launch and authenticate an application.  
  3. ClinicalTrialsServer: building on SmartServer, a working, turnkey demo application that embeds contextual search for clinical trials into an EHR.

These can be used independently, so you don’t have to buy into my web server to use the backend. All have been tested with the Epic and Cerner sandboxes, and the Clinical Trials search application could be installed into production EHRs today (drop me a note if you’re interested, I’d be happy to help get it running at your site as a SMART proof-of-concept).

The code is all available, license-free, on Github. Over the course of this series I’ll dive into the implementation: first the basics and auth sequence, then a deep look into the data model and how I’ve tried to make the information there easier to consume. I’ve tried to optimize for time-to-production in my specific scenario, so the code looks a little different than other libraries. There’s a lot of it; we’ll take a few rounds to cover it all!

Enough Talk – try it live!

Point your browser at https://shutdowntrials.duckdns.org:7071/ to get started. This is just a landing page with a bunch of links that launch into the experience. For now, click on the one that says “DSTU2 with patient/doc pre-selected” (or click the link in this sentence I guess, doh).  

You should see something that looks like the screenshot to the right — a “Simulated EHR” that shows potential clinical trials for imaginary patient Phillip Jones. If you want to fine-tune the search, edit the demographics or use the checkboxes, then click “refresh list”. For example, the EHR doesn’t provide an address for Phillip, so you can fill in the country “US” to see only trials in the United States.

These results come from http://clinicaltrials.gov, a public repository of clinical trials maintained by the NIH. Beware that choosing multiple conditions will often lead to empty results; just aren’t a lot of combo trials for sinusitis, ED and rosacea!

Next, try going back to the launch page and clicking on “DSTU2 with patient/doc picker”. This time, before you see results, you’ll have the option to “log in” to the EHR and select a test patient. This will land on a search experience where the demographics and conditions are appropriate for that patient. Nice!

Here’s what is going on behind the scenes:

  1. Your browser starts at the SMART App Launcher, a development sandbox maintained by the good folks at smarthealthit.org. While vendors like Epic and Cerner have quite serviceable development environments, SMART is nice because it’s completely open and has a solid selection of test patients to work with out of the box.
  2. Once the provider is logged in and the patient is selected, a negotiation sends your browser back and forth between the EHR and ShutdownTrials a couple of times. The end result is that we prove to each other that we’re legitimate, and the application receives a token that can be used to read and write EHR data.
  3. The application requests demographics and condition data for the patient, converts them into an API request at clinicaltrials.gov, and displays the results in the embedded iframe.

Tada! If it all seems very simple in practice, then at least I’ve gotten something right — because it’s anything but simple in execution. Buckle up friends, because the deep dive is going to be a great ride.

*** A last note and request (this will show up at the bottom of each article in the series). I’ve spent a lot of time in this industry, and the systemic impediments to progress and innovation can make even good folks feel hopeless sometimes. I really, truly believe that SMART is one of those rare technologies that has matured at exactly the right time to change the game. But there’s no guarantee — not enough folks know about it, and it’s too hard to use. If you swim in this pool, please help me fix that:

  1. Share these articles with folks that use and implement EHRs. Tell them to look at the “app store” for their system and add an app to their test system. Tell them to ask vendors if they have a SMART interface for their solution.
  2. Share these articles with folks that build care delivery solutions. Explain how they can use SMART to add functionality for customers without a custom login and without having to do an integration project with custom IT teams.
  3. Contact me if I can help. There’s a form here on the website, and I’m @seanno on Twitter, or use LinkedIn, or whatever. I’m happy to answer questions, make some connections, and heck I might even write some code for you if it makes a difference.