OmniEHR implements comprehensive audit logging to meet HIPAA Security Rule requirements for tracking access to protected health information (PHI).
Overview
Every API request to FHIR resources and administrative endpoints is automatically logged with actor information, resource details, and outcome status.
Automatic Logging All API requests are automatically logged via middleware
Non-Blocking Audit logs don’t impact clinical workflow performance
Comprehensive Tracking Actor, action, resource, timestamp, outcome, and network details
HIPAA Compliant Meets HIPAA Security Rule audit trail requirements
Audit Log Data Model
const auditLogSchema = new mongoose . Schema ({
actorUserId: {
type: mongoose . Schema . Types . ObjectId ,
ref: "User"
},
actorEmail: String ,
actorRole: String ,
action: {
type: String ,
required: true
},
resourceType: String ,
resourceId: String ,
method: {
type: String ,
required: true
},
path: {
type: String ,
required: true
},
statusCode: {
type: Number ,
required: true
},
outcome: {
type: String ,
enum: [ "success" , "failure" ],
required: true
},
ipAddress: String ,
userAgent: String
}, {
timestamps: true
});
// Indexes for efficient querying
auditLogSchema . index ({ createdAt: - 1 });
auditLogSchema . index ({ actorUserId: 1 , createdAt: - 1 });
Audit Log Fields
Field Type Description actorUserIdObjectId ID of the user who performed the action actorEmailString Email of the actor actorRoleString Role of the actor (admin, practitioner, auditor) actionString Type of action (read, create, update, delete) resourceTypeString FHIR resource type or endpoint resourceIdString ID of the specific resource accessed methodString HTTP method (GET, POST, PUT, DELETE) pathString Full request path statusCodeNumber HTTP response status code outcomeString success or failure ipAddressString Client IP address userAgentString Client user agent string createdAtDate Timestamp when action occurred
Audit Middleware
The audit middleware intercepts all FHIR and admin API requests:
import AuditLog from "../models/AuditLog.js" ;
const methodToAction = {
GET: "read" ,
POST: "create" ,
PUT: "update" ,
PATCH: "update" ,
DELETE: "delete"
};
const parseRouteContext = ( urlPath ) => {
const cleanPath = ( urlPath || "" ). split ( "?" )[ 0 ];
const segments = cleanPath . split ( "/" ). filter ( Boolean );
if ( segments . length < 2 || segments [ 0 ] !== "api" ) {
return { resourceType: "Unknown" , resourceId: undefined };
}
const resourceType = segments [ 2 ] || segments [ 1 ] || "Unknown" ;
const resourceId = segments [ 3 ];
return { resourceType , resourceId };
};
export const requestAuditTrail = ( req , res , next ) => {
const watched = req . originalUrl . startsWith ( "/api/fhir" ) ||
req . originalUrl . startsWith ( "/api/admin" );
if ( ! watched ) {
return next ();
}
res . on ( "finish" , () => {
const { resourceType , resourceId } = parseRouteContext ( req . originalUrl );
const outcome = res . statusCode >= 200 && res . statusCode < 400 ?
"success" : "failure" ;
AuditLog . create ({
actorUserId: req . user ?. sub ,
actorEmail: req . user ?. email ,
actorRole: req . user ?. role ,
action: methodToAction [ req . method ] || "unknown" ,
resourceType ,
resourceId ,
method: req . method ,
path: req . originalUrl ,
statusCode: res . statusCode ,
outcome ,
ipAddress: req . ip ,
userAgent: req . get ( "user-agent" )
}). catch (() => {
// Non-blocking by design to avoid impacting clinical workflows.
});
});
return next ();
};
Non-Blocking Design : Audit log creation failures don’t impact API responses. Logs are created asynchronously after the response is sent.
Middleware Integration
The audit middleware is registered globally in the application:
import express from "express" ;
import { requestAuditTrail } from "./middleware/audit.js" ;
import fhirRoutes from "./routes/fhirRoutes.js" ;
import adminRoutes from "./routes/adminRoutes.js" ;
const app = express ();
// Apply audit middleware globally
app . use ( requestAuditTrail );
// Register routes
app . use ( "/api/fhir" , fhirRoutes );
app . use ( "/api/admin" , adminRoutes );
export default app ;
Audit Page UI
The audit page displays a paginated view of all audit log entries:
import { useEffect , useState } from "react" ;
import { adminApi } from "../api.js" ;
import { useAuth } from "../context/AuthContext.jsx" ;
const AuditPage = () => {
const { token } = useAuth ();
const [ rows , setRows ] = useState ([]);
const [ page , setPage ] = useState ( 1 );
const [ total , setTotal ] = useState ( 0 );
const [ limit , setLimit ] = useState ( 25 );
const [ error , setError ] = useState ( "" );
useEffect (() => {
const load = async () => {
try {
const query = `?page= ${ page } &limit= ${ limit } ` ;
const response = await adminApi . listAuditLogs ( token , query );
setRows ( response . data || []);
setTotal ( response . total || 0 );
setLimit ( response . limit || 25 );
} catch ( err ) {
setError ( err . message || "Unable to load audit logs" );
}
};
load ();
}, [ page , limit , token ]);
const totalPages = Math . max ( 1 , Math . ceil ( total / limit ));
return (
< section className = "stack-gap" >
< h1 > Audit logs </ h1 >
< p className = "muted-text" >
HIPAA Security Rule audit trail of access and modification activity.
</ p >
{ error ? < p className = "banner banner-error" > { error } </ p > : null }
< article className = "card" >
< div className = "table-scroll" >
< table >
< thead >
< tr >
< th > Timestamp </ th >
< th > Actor </ th >
< th > Role </ th >
< th > Action </ th >
< th > Resource </ th >
< th > Status </ th >
< th > Outcome </ th >
< th > Path </ th >
</ tr >
</ thead >
< tbody >
{ rows . map (( entry ) => (
< tr key = { entry . _id } >
< td > { entry . createdAt } </ td >
< td > { entry . actorEmail || "Unknown" } </ td >
< td > { entry . actorRole || "-" } </ td >
< td > { entry . action } </ td >
< td >
{ entry . resourceType }
{ entry . resourceId ? `/ ${ entry . resourceId } ` : "" }
</ td >
< td > { entry . statusCode } </ td >
< td > { entry . outcome } </ td >
< td > { entry . path } </ td >
</ tr >
)) }
</ tbody >
</ table >
</ div >
< div className = "pagination" >
< button
type = "button"
className = "button button-secondary"
onClick = { () => setPage (( prev ) => Math . max ( 1 , prev - 1 )) }
disabled = { page <= 1 }
>
Previous
</ button >
< span >
Page { page } of { totalPages }
</ span >
< button
type = "button"
className = "button button-secondary"
onClick = { () => setPage (( prev ) => Math . min ( totalPages , prev + 1 )) }
disabled = { page >= totalPages }
>
Next
</ button >
</ div >
</ article >
</ section >
);
};
export default AuditPage ;
The audit page includes pagination for efficient browsing:
const totalPages = Math . max ( 1 , Math . ceil ( total / limit ));
< div className = "pagination" >
< button
type = "button"
className = "button button-secondary"
onClick = { () => setPage (( prev ) => Math . max ( 1 , prev - 1 )) }
disabled = { page <= 1 }
>
Previous
</ button >
< span >
Page { page } of { totalPages }
</ span >
< button
type = "button"
className = "button button-secondary"
onClick = { () => setPage (( prev ) => Math . min ( totalPages , prev + 1 )) }
disabled = { page >= totalPages }
>
Next
</ button >
</ div >
Backend API Endpoint
import express from "express" ;
import { authenticate , authorize } from "../middleware/auth.js" ;
import { asyncHandler } from "../utils/asyncHandler.js" ;
import AuditLog from "../models/AuditLog.js" ;
const router = express . Router ();
router . use ( authenticate );
router . get (
"/audit-logs" ,
authorize ( "admin" , "auditor" ),
asyncHandler ( async ( req , res ) => {
const page = Math . max ( 1 , parseInt ( req . query . page , 10 ) || 1 );
const limit = Math . min ( 100 , Math . max ( 1 , parseInt ( req . query . limit , 10 ) || 25 ));
const skip = ( page - 1 ) * limit ;
const [ logs , total ] = await Promise . all ([
AuditLog . find ({})
. sort ({ createdAt: - 1 })
. skip ( skip )
. limit ( limit )
. lean (),
AuditLog . countDocuments ({})
]);
res . json ({
data: logs ,
total ,
page ,
limit ,
totalPages: Math . ceil ( total / limit )
});
})
);
export default router ;
Query Parameters
Parameter Type Default Description pageNumber 1 Page number (1-indexed) limitNumber 25 Results per page (max 100)
Action Types
Audit logs categorize actions based on HTTP methods:
HTTP Method Action Type Description GET readReading/viewing resources POST createCreating new resources PUT updateUpdating existing resources PATCH updatePartial updates DELETE deleteDeleting resources
Outcome Classification
const outcome = res . statusCode >= 200 && res . statusCode < 400 ?
"success" : "failure" ;
Status Code Range Outcome Description 200-399 success Successful operations 400-599 failure Client or server errors
Example Audit Logs
Successful Patient Read
{
"_id" : "507f1f77bcf86cd799439011" ,
"actorUserId" : "507f1f77bcf86cd799439020" ,
"actorEmail" : "[email protected] " ,
"actorRole" : "practitioner" ,
"action" : "read" ,
"resourceType" : "Patient" ,
"resourceId" : "507f1f77bcf86cd799439030" ,
"method" : "GET" ,
"path" : "/api/fhir/Patient/507f1f77bcf86cd799439030" ,
"statusCode" : 200 ,
"outcome" : "success" ,
"ipAddress" : "192.168.1.100" ,
"userAgent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" ,
"createdAt" : "2026-03-04T10:30:00.000Z"
}
Failed Authorization Attempt
{
"_id" : "507f1f77bcf86cd799439012" ,
"actorUserId" : "507f1f77bcf86cd799439021" ,
"actorEmail" : "[email protected] " ,
"actorRole" : "auditor" ,
"action" : "update" ,
"resourceType" : "Patient" ,
"resourceId" : "507f1f77bcf86cd799439030" ,
"method" : "PUT" ,
"path" : "/api/fhir/Patient/507f1f77bcf86cd799439030" ,
"statusCode" : 403 ,
"outcome" : "failure" ,
"ipAddress" : "192.168.1.101" ,
"userAgent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" ,
"createdAt" : "2026-03-04T10:31:00.000Z"
}
Patient $everything Operation
{
"_id" : "507f1f77bcf86cd799439013" ,
"actorUserId" : "507f1f77bcf86cd799439020" ,
"actorEmail" : "[email protected] " ,
"actorRole" : "practitioner" ,
"action" : "read" ,
"resourceType" : "Patient" ,
"resourceId" : "507f1f77bcf86cd799439030" ,
"method" : "GET" ,
"path" : "/api/fhir/Patient/507f1f77bcf86cd799439030/$everything" ,
"statusCode" : 200 ,
"outcome" : "success" ,
"ipAddress" : "192.168.1.100" ,
"userAgent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" ,
"createdAt" : "2026-03-04T10:32:00.000Z"
}
HIPAA Compliance
OmniEHR audit logs meet HIPAA Security Rule requirements:
§164.312(b) Audit Controls
Implements hardware, software, and/or procedural mechanisms that record and examine activity in information systems that contain or use electronic protected health information. Implementation : Automatic logging of all FHIR API access via middleware with comprehensive actor and resource tracking.
§164.308(a)(1)(ii)(D) Information System Activity Review
Implement procedures to regularly review records of information system activity, such as audit logs, access reports, and security incident tracking reports. Implementation : Paginated audit log interface accessible to admin and auditor roles for regular review.
§164.308(a)(5)(ii)(C) Log-in Monitoring
Procedures for monitoring log-in attempts and reporting discrepancies. Implementation : All authentication attempts and access to PHI are logged with actor identification, timestamps, and outcomes.
Watched Endpoints
The audit middleware monitors the following endpoint patterns:
FHIR Resources
Admin Endpoints
/api/fhir/Patient
/api/fhir/Patient/:id
/api/fhir/Patient/:id/$everything
/api/fhir/Observation
/api/fhir/Condition
/api/fhir/AllergyIntolerance
/api/fhir/MedicationRequest
/api/fhir/Encounter
/api/fhir/Appointment
/api/fhir/Task
/api/admin/users
/api/admin/users/:id
/api/admin/practitioners
/api/admin/audit-logs
Access Control
Audit log access is restricted to authorized roles:
router . get (
"/audit-logs" ,
authorize ( "admin" , "auditor" ),
asyncHandler ( async ( req , res ) => {
// ... audit log retrieval
})
);
Authorized Roles : admin, auditor
Audit logs contain sensitive information about system access. Only admin and auditor roles can view audit logs.
Non-Blocking Architecture
Audit log creation uses asynchronous processing:
Non-Blocking Log Creation
AuditLog . create ({
actorUserId: req . user ?. sub ,
actorEmail: req . user ?. email ,
// ... additional fields
}). catch (() => {
// Non-blocking by design to avoid impacting clinical workflows.
});
If audit log creation fails, the API response proceeds normally without error.
Database Indexes
Audit logs use indexes for efficient querying:
auditLogSchema . index ({ createdAt: - 1 }); // For chronological sorting
auditLogSchema . index ({ actorUserId: 1 , createdAt: - 1 }); // For user-specific queries
Future Enhancements
Potential enhancements to the audit system:
Advanced Filtering Filter by date range, actor, resource type, action, or outcome
Audit Report Export Export audit logs to CSV or PDF for compliance reporting
Real-Time Alerts Alert administrators to suspicious access patterns
Retention Policies Automatic archival of old audit logs per compliance requirements
API Reference
Audit Log Endpoints
Method Endpoint Description Required Role GET /api/admin/audit-logsList audit logs (paginated) admin, auditor
{
"data" : [
{
"_id" : "507f1f77bcf86cd799439011" ,
"actorUserId" : "507f1f77bcf86cd799439020" ,
"actorEmail" : "[email protected] " ,
"actorRole" : "practitioner" ,
"action" : "read" ,
"resourceType" : "Patient" ,
"resourceId" : "507f1f77bcf86cd799439030" ,
"method" : "GET" ,
"path" : "/api/fhir/Patient/507f1f77bcf86cd799439030" ,
"statusCode" : 200 ,
"outcome" : "success" ,
"ipAddress" : "192.168.1.100" ,
"userAgent" : "Mozilla/5.0..." ,
"createdAt" : "2026-03-04T10:30:00.000Z"
}
],
"total" : 1523 ,
"page" : 1 ,
"limit" : 25 ,
"totalPages" : 61
}
Next Steps
Patient Management Learn about patient registry and demographics
FHIR Resources View all supported FHIR resources
Security Explore authentication and authorization
API Reference Complete API documentation