Skip to main content
CoreSDK
Guides

HIPAA-Ready Healthcare API

Build MediRecord — a patient record API with role-based field masking, break-glass access, audit trails, and fail-closed enforcement. Go SDK + Gin.

HIPAA-Ready Healthcare API

What you'll build: MediRecord — a patient record REST API for a telehealth platform. Three user types access the same records with different visibility:

RoleCan see
patientTheir own records only, no SSN/DOB in responses
clinicianAny patient in their practice, full clinical data
adminEverything including audit logs — requires MFA claim
break_glassEmergency override — all fields, always audited with reason

CoreSDK enforces role gating, PII masking per role, break-glass audit, and fail-closed mode (no sidecar = no access — healthcare data is too sensitive for fail-open).

The Story

MedCore Health runs a multi-practice telehealth platform. HIPAA requires:

  • Minimum necessary access — patients see only their own data
  • Audit log for every access to PHI (Protected Health Information)
  • Break-glass procedure — emergency access with mandatory reason, always flagged
  • Fail-closed — any auth failure denies access, never leaks

Without CoreSDK: field masking, audit, and fail-closed logic scattered across models, middleware, and ad-hoc code. With CoreSDK: one middleware, one policy file.

Architecture

Clinician / Patient / Admin
         │  JWT (contains role + practice_id + mfa_verified)

┌─────────────────────────────────┐
│          Gin HTTP Server        │
│                                 │
│  CoreSDK Auth Middleware        │  ← fail-CLOSED (no sidecar = 401)
│       │                         │
│       ▼                         │
│  Role extraction from claims    │
│       │                         │
│  ┌────┴──────────────────────┐  │
│  │ Field masking per role    │  │  ← patient sees masked SSN
│  │ patient → mask SSN, DOB   │  │
│  │ clinician → full record   │  │
│  │ break_glass → log reason  │  │
│  └───────────────────────────┘  │
│       │                         │
│  Audit every PHI access         │
└─────────────────────────────────┘
         │  gRPC :50051  (fail-CLOSED)

┌─────────────────┐
│  CoreSDK Sidecar│
└─────────────────┘

Prerequisites

go get github.com/coresdk-dev/sdk-go
go get github.com/gin-gonic/gin

Quickstart

git clone https://github.com/coresdk-dev/examples
cd examples/go/medirecord
CORESDK_FAIL_MODE=closed go run main.go

Code Walkthrough

Step 1 — Fail-closed SDK (healthcare default)

package main

import (
    "context"
    "os"

    coresdk "github.com/coresdk-dev/sdk-go"
    "github.com/gin-gonic/gin"
)

func main() {
    // FAIL-CLOSED: sidecar unreachable = deny all requests
    // Never use fail-open for healthcare data
    os.Setenv("CORESDK_FAIL_MODE", "closed")

    sdk, err := coresdk.FromEnv()
    if err != nil {
        // In fail-closed mode, this error means sidecar is unreachable.
        // Do NOT start the server — returning 500 is safer than serving unauthed.
        panic("sidecar unreachable at startup (fail-closed): " + err.Error())
    }

    r := gin.New()
    r.Use(AuthMiddleware(sdk))
    r.GET("/patients/:id", GetPatient)
    r.GET("/patients/:id/audit", RequireRole("admin"), GetAuditLog)
    r.Run(":8080")
}

Step 2 — Auth middleware with break-glass detection

func AuthMiddleware(sdk *coresdk.SDK) gin.HandlerFunc {
    return func(c *gin.Context) {
        token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")

        // Fail-closed: any error denies access
        claims, err := sdk.Authorize(context.Background(), token)
        if err != nil {
            c.AbortWithStatusJSON(401, gin.H{
                "type":   "https://medirecord.example/errors/unauthorized",
                "title":  "Unauthorized",
                "status": 401,
                "detail": "Valid JWT required. Sidecar validation failed.",
            })
            return
        }

        c.Set("claims", claims)
        c.Set("roles", claims.Roles)
        c.Set("practice_id", claims.TenantID)

        // Detect break-glass: role present but requires mandatory reason header
        if contains(claims.Roles, "break_glass") {
            reason := c.GetHeader("X-Break-Glass-Reason")
            if reason == "" {
                c.AbortWithStatusJSON(403, gin.H{
                    "type":   "https://medirecord.example/errors/break-glass-reason-required",
                    "title":  "Forbidden",
                    "status": 403,
                    "detail": "Break-glass access requires X-Break-Glass-Reason header.",
                })
                return
            }
            c.Set("break_glass_reason", reason)
            AuditBreakGlass(claims, c.Request.URL.Path, reason)
        }

        c.Next()
    }
}

Step 3 — Field masking per role

type PatientRecord struct {
    ID          string `json:"id"`
    Name        string `json:"name"`
    SSN         string `json:"ssn,omitempty"`
    DOB         string `json:"dob,omitempty"`
    Diagnosis   string `json:"diagnosis,omitempty"`
    PracticeID  string `json:"practice_id"`
}

// MaskForRole removes fields the caller's role is not allowed to see.
func MaskForRole(p PatientRecord, roles []string) PatientRecord {
    masked := p

    isPatient   := contains(roles, "patient")
    isClinician := contains(roles, "clinician")
    isBreakGlass := contains(roles, "break_glass")

    if isBreakGlass {
        // break_glass sees everything — already audited in middleware
        return masked
    }

    if isPatient && !isClinician {
        masked.SSN = mask(p.SSN)      // "***-**-6789"
        masked.DOB = mask(p.DOB)      // "****-**-15"
        masked.Diagnosis = ""         // not visible to patient
    }

    return masked
}

func mask(s string) string {
    if len(s) <= 4 {
        return strings.Repeat("*", len(s))
    }
    return strings.Repeat("*", len(s)-4) + s[len(s)-4:]
}

Step 4 — Tenant-scoped record access

func GetPatient(c *gin.Context) {
    claims := c.MustGet("claims").(*coresdk.Claims)
    roles := c.MustGet("roles").([]string)
    practiceID := c.MustGet("practice_id").(string)
    patientID := c.Param("id")

    record, ok := db[patientID]
    if !ok {
        c.JSON(404, gin.H{"type": "...not-found", "status": 404})
        return
    }

    // Tenant isolation: patients can only see their own practice's records
    if record.PracticeID != practiceID && !contains(roles, "break_glass") {
        c.JSON(403, gin.H{
            "type":   "https://medirecord.example/errors/forbidden",
            "title":  "Forbidden",
            "status": 403,
            "detail": "Patient record belongs to a different practice.",
        })
        return
    }

    // Patient self-access check
    if contains(roles, "patient") && !contains(roles, "clinician") {
        if record.ID != claims.Subject {
            c.JSON(403, gin.H{
                "type":   "https://medirecord.example/errors/forbidden",
                "title":  "Forbidden",
                "status": 403,
                "detail": "Patients may only access their own records.",
            })
            return
        }
    }

    // Audit PHI access
    AuditPHIAccess(claims, patientID, "read")

    // Mask fields based on caller's role
    c.JSON(200, MaskForRole(record, roles))
}

Step 5 — Audit log

type AuditEvent struct {
    Timestamp  string `json:"timestamp"`
    Actor      string `json:"actor"`
    Action     string `json:"action"`
    Resource   string `json:"resource"`
    Outcome    string `json:"outcome"`
    Reason     string `json:"reason,omitempty"`
    BreakGlass bool   `json:"break_glass,omitempty"`
}

var auditLog []AuditEvent

func AuditPHIAccess(claims *coresdk.Claims, patientID, action string) {
    auditLog = append(auditLog, AuditEvent{
        Timestamp: time.Now().UTC().Format(time.RFC3339),
        Actor:     claims.Subject,
        Action:    action,
        Resource:  "patient/" + patientID,
        Outcome:   "success",
    })
}

func AuditBreakGlass(claims *coresdk.Claims, path, reason string) {
    auditLog = append(auditLog, AuditEvent{
        Timestamp:  time.Now().UTC().Format(time.RFC3339),
        Actor:      claims.Subject,
        Action:     "break_glass",
        Resource:   path,
        Outcome:    "access_granted",
        Reason:     reason,
        BreakGlass: true,
    })
    // In production: page the on-call security team immediately
    log.Printf("BREAK GLASS: actor=%s path=%s reason=%q", claims.Subject, path, reason)
}

Testing Break-Glass Access

# Normal clinician access
curl http://localhost:8080/patients/p-001 \
  -H "Authorization: Bearer clinician-token"
# {"id":"p-001","name":"Alice Smith","ssn":"123-45-6789","diagnosis":"..."}

# Patient self-access — SSN masked
curl http://localhost:8080/patients/p-001 \
  -H "Authorization: Bearer patient-token"
# {"id":"p-001","name":"Alice Smith","ssn":"***-**-6789","diagnosis":""}

# Break-glass — requires reason header
curl http://localhost:8080/patients/p-001 \
  -H "Authorization: Bearer break-glass-token" \
  -H "X-Break-Glass-Reason: Patient in ER, consent unreachable"
# Full record + audit event written + on-call paged

# Break-glass without reason → 403
curl http://localhost:8080/patients/p-001 \
  -H "Authorization: Bearer break-glass-token"
# {"status":403,"detail":"Break-glass access requires X-Break-Glass-Reason header."}

HIPAA Compliance Notes

RequirementImplementation
Minimum necessaryField masking per role in MaskForRole()
Access auditAuditPHIAccess() on every PHI read
Break-glass procedureX-Break-Glass-Reason header + immediate audit
Fail-closedCORESDK_FAIL_MODE=closed — sidecar down = no access
Tenant isolationpractice_id from JWT, validated on every record access

Environment Variables

VariableRequiredDescription
CORESDK_SIDECAR_ADDRYesSidecar gRPC — must be reachable at startup
CORESDK_FAIL_MODEYesMust be closed for PHI workloads
CORESDK_JWKS_URIYesYour IdP JWKS — validates role claims cryptographically
CORESDK_TENANT_IDYesPractice ID for default tenant scope

Full Source

examples/go/medirecord/

cd examples/go/medirecord
CORESDK_FAIL_MODE=closed go run main.go

On this page