Build on Rails
Build an EHR/EMR app on Rails
Run patient records from your own Rails app: patients, encounters, medications, and documents, behind role-based access and an audit trail, with the data never leaving your infrastructure.
250+ paying companies · 2.7M downloads of the avo gem
01 · the hand-built version
What building an EHR/EMR app by hand actually costs
Clinical software is where a hand-built admin is riskiest. The data is sensitive, access has to be provable, and the internal tool is the part with the keys to everything.
-
01
The admin is the softest target in a clinical stack
Your internal tool can read every patient record and change any of them, often behind auth someone wrote in a hurry. In a regulated domain that is the riskiest code you own, and the easiest to get subtly wrong.
-
02
Access has to be role-based and provable
A billing clerk, a nurse, and an attending physician should see different slices of a chart. Everyone with a login sees everything is a finding waiting to happen, and building a correct per-role, per-field access model by hand is slow, exacting work.
-
03
Every view of a record is an event you may need to show
In clinical systems, who looked at which chart and when is not a nice-to-have. Bolting an audit log onto a bespoke admin after the fact, and trusting it to be complete, is its own project.
-
04
Sensitive data leaks through the tool you trust most
Documents, medication lists, and encounter notes are exactly what you cannot afford to expose. A DIY panel that quietly renders a field to the wrong role is how that happens.
02 · the Avo version
Your models are already the hard part. Avo is the rest.
Avo runs inside your Rails app, so patient data never leaves your infrastructure the way it would with a hosted admin tool. You model Patient, Encounter, Medication, and Document as resources on your own tables, and every access goes through the Pundit policies and audit hooks you control. Owning the code and the data is a better starting point for a regulated system than putting a third-party service in the path.
Access decided by policies, enforced everywhere
A PatientPolicy and per-field authorization decide what a nurse, a biller, or a physician can see and change, down to individual fields like diagnosis or medication. Because Avo reads your Pundit policies, a role that should not see a field simply does not render it.
It lives in your infrastructure, not a vendor's
Avo is a Rails engine mounted in your app, so encounters, documents, and medication records stay in your database and never transit a third-party service. There is no external platform between your clinicians and their patients' data.
Encounters, medications, and documents as related records
A Patient has_many encounters, each encounter has_many medications and documents. Avo renders that chart structure as linked panels, so a clinician navigates a patient's history without you hand-building the UI.
Log access, not just changes, with auditing you own
Pair Avo with your model-level auditing so each view and edit of a record is logged with the acting user. Because access flows through policies you control, you decide what gets recorded and can produce it later.
Sensitive documents handled through Active Storage
Attach scans, referrals, and lab PDFs with a files field, with upload and download gated per role by attachment authorization, so a document is only reachable by someone allowed to see it.
A Patient resource with encounters, documents, and field-level access:
class Avo::Resources::Patient < Avo::BaseResource
self.title = :full_name
self.includes = [:encounters, :documents]
def fields
field :mrn, as: :text, name: "Medical record number", required: true
field :full_name, as: :text, required: true
field :date_of_birth, as: :date
field :ssn, as: :text, visible: -> { current_user.can_view_phi? }
field :encounters, as: :has_many
field :documents, as: :files
end
end
# app/policies/patient_policy.rb (Avo reads your Pundit policies)
class PatientPolicy < ApplicationPolicy
def show?
user.clinician? || user.billing?
end
def download_documents? # attachment authorization
user.clinician?
end
end
03 · the timeline
From first resource to the team running on it
Generate resources for your existing Patient, Encounter, and Medication models and you have a navigable chart on real data, with each patient's encounters and documents linked from their record.
Put it behind your existing authentication and add Pundit policies so a nurse, a biller, and a physician each see only their slice. Turn on model auditing so every access is recorded with the acting user.
Field-level access, document authorization, and an audit trail are in place, and clinical and billing staff are working from patient records in a tool that never sends data to anyone else's servers.
Prefer to prompt?
Hand this to Claude Code, Cursor, or whatever agent you build with, and review what it ships like any other pull request.
Build a brand-new EHR/EMR application from scratch with Ruby on Rails and Avo 4 as the management UI (https://avohq.io, docs at https://docs.avohq.io/4.0/). Nothing has been built yet. Everything must live inside my own infrastructure; patient data never leaves the app.
1. Scaffold the application with rails new using sensible defaults, then set up sign-in with Rails' built-in authentication generator so every clinician and staff member logs in. Add the avo gem, bundle, run bin/rails generate avo:install, and mount Avo at /avo behind that authentication so the back office is never reachable unauthenticated.
2. Work out the data model from first principles before touching a migration. Think through what running patient records genuinely requires: the core entities a clinical record turns on, the lifecycle each one moves through (model an encounter's status or a medication order's state as an enum, or as its own table when the workflow is richer than a few values), and how they associate into a navigable chart. The domain will surface patients and their identifiers, clinical encounters or visits, medications and orders, and attached documents such as scans and referrals, so treat those as ground your design has to cover, not a schema to transcribe. Sketch the tables and relationships, then write the migrations.
3. Generate an Avo resource per model with bin/rails generate avo:resource, then make each one concrete (https://docs.avohq.io/4.0/resources.html). Set self.title to a human label like the patient's name, list self.includes to eager-load the associations a chart renders so it stays fast, and set self.search over the columns staff look records up by. Match field types to the columns you chose: a date for a date of birth, a select backed by your status enum for an encounter, a files field over Active Storage for documents, and a visible lambda checking the current user on any protected field so it renders only for a role allowed to see it.
4. Turn the clinical verbs into Avo Actions rather than raw edits: admitting or discharging an encounter, prescribing or discontinuing a medication, flagging a chart for review. Since the app is new, put the real behavior in model methods or small service objects and have each action call into them, so a status change runs one code path whether it fires from Avo or elsewhere (https://docs.avohq.io/4.0/actions.html).
5. Add filters and scopes so staff can narrow to open encounters, a given clinician's patients, or active medications, and build a dashboard that surfaces what the page promises: active caseload, recent encounters, and documents awaiting review (https://docs.avohq.io/4.0/dashboards.html).
6. Make authorization the spine of the build with Pundit policies, the same ones the app uses (https://docs.avohq.io/4.0/authorization.html). Give a nurse, a billing clerk, and a physician distinct reach: write a policy for the patient record and one for each sensitive model deciding who views and edits what down to individual fields, plus attachment authorization so a document opens only for a role permitted to see it.
7. Seed realistic sample data so no screen is empty: a handful of patients, each with encounters across different statuses, medications, and a document or two, plus one user per role to sign in as.
8. Boot the app and open /avo. Sign in as the billing clerk and confirm protected clinical fields do not render and a document download is denied; then sign in as a physician and walk a patient into their encounters, run the discharge action on an open encounter, and confirm its status changes.
04 · start today
Stand up an EHR/EMR app this week
Avo is per app with unlimited users, so the whole team works from it without the bill scaling with headcount. When you hit an edge Avo doesn't cover, you drop to plain Rails instead of filing a feature request.
More things teams run from their Rails app
Frequently asked questions
- Can I build an EHR or EMR with Ruby on Rails?
- Yes. Patients, encounters, medications, and documents are ordinary Rails models with associations, and Avo turns them into a navigable chart with role-based access on top of your existing tables. Because Avo runs inside your Rails app, the records stay in your own database rather than a third-party service.
- Is Avo HIPAA compliant?
- Compliance is a property of your entire system and processes, not of any single library, so no gem can be HIPAA compliant on its own, and we do not claim Avo is. What Avo gives you is a better starting point: the tool runs inside your Rails app on infrastructure you control, access goes through policies you write, and the data never leaves your database for a vendor's servers. The compliance work, a BAA where applicable, encryption, retention, and access reviews, remains yours to do, on code you own.
- How does role-based access work for clinical versus billing staff?
- Access is enforced with Pundit policies, the same ones your Rails app uses. A PatientPolicy decides who can view or edit a record, and per-field authorization means a billing clerk sees what they need for invoicing while diagnosis or medication fields render only for clinicians. See the authorization docs.
- Can I audit who viewed a patient record?
- Yes. Because every access flows through policies and controllers you own, you can pair Avo with model-level auditing to record who viewed or changed a record and when. You decide what is logged, and the log lives in your own database where you can query and produce it.
- Where does the patient data live?
- In your database, inside your own infrastructure. Avo is a Rails engine mounted in your application, so unlike a hosted admin tool there is no external platform in the path between your clinicians and their patients' data, and nothing is synced out to a third party.
- Is a Rails admin safe enough for clinical data?
- Your admin is the part of the stack with the keys to everything, so the honest answer is that safety comes from getting authorization and hardening right, whoever builds it. Avo's advantage is that the auth model is Pundit, which your app already uses, access is enforced per action and per field, and it all stays in code you can review, rather than a black-box service you cannot.