February 6, 2026
Healthcare HIPAA N8N Development

Building a HIPAA-Compliant Patient Portal with n8n and Airtable

Building a HIPAA-compliant patient portal with n8n and Airtable requires a self-hosted n8n deployment with encryption enabled, an Airtable Enterprise Scale plan with a signed Business Associate Agreement, and a careful architectural separation where Airtable serves as a backend data layer — not the portal itself. The system needs end-to-end TLS encryption, role-based access controls, automatic session timeouts, audit logging, and a separate frontend that patients actually interact with, while n8n orchestrates data flows between the portal, your Airtable backend, and your existing EHR system.

If you run a small practice in Central Florida — Deltona, Daytona Beach, DeLand, anywhere in Volusia County — you already know the pressure. Halifax Health and AdventHealth have enterprise-grade patient portals with all the bells and whistles. Your patients expect the same experience from you. And the commercial portal solutions? They start at $200 a month and climb fast, especially once you need custom integrations.

This guide shows you how to build a patient portal system that handles appointment requests, secure messaging, records access, and prescription refills — all HIPAA-compliant — using tools you can self-host and control. But we are going to start with something most guides skip: the uncomfortable truths about what you are actually getting into.

Table of Contents
  1. The Uncomfortable Truth About DIY Patient Portals
  2. What HIPAA Actually Requires for Patient Portals
  3. The n8n + Airtable Architecture That Actually Works
  4. Setting Up Self-Hosted n8n for Healthcare
  5. Configuring Airtable Enterprise as Your Backend
  6. Building the Patient-Facing Portal Frontend
  7. The Complete n8n Workflow
  8. Security Scripts You Need on Day One
  9. Field-Level Encryption Helper (MJS)
  10. HIPAA Audit Logger (Python)
  11. What the Custom-Built Version Looks Like
  12. When to Build vs Buy: The Honest Assessment
  13. Frequently Asked Questions
  14. Is Airtable HIPAA compliant for storing patient data?
  15. Does n8n offer a BAA for HIPAA compliance?
  16. What security features must a HIPAA-compliant patient portal have?
  17. How much does it cost to build a DIY patient portal?
  18. Should a small practice build or buy a patient portal?

The Uncomfortable Truth About DIY Patient Portals

Here is the part where most "how to build a patient portal" articles lose their credibility. They either gloss over the compliance complexity or they scare you into buying their product. We are going to do neither.

Truth number one: Airtable says you cannot use it as a patient portal. That is not a technicality. Airtable's own HIPAA documentation explicitly states that healthcare customers are not permitted to use Airtable as a patient portal. Read that again. If you build a portal where patients log into Airtable directly, you are violating Airtable's terms of service — and potentially your HIPAA compliance.

Truth number two: n8n does not offer a Business Associate Agreement. A BAA is the legal document that makes a vendor responsible for protecting health information they handle. Without one, you carry all the liability. n8n is open-source, which is beautiful for transparency and self-hosting, but it means no vendor is legally accountable for how PHI flows through it.

Truth number three: building your own portal is harder than the YouTube tutorials suggest. You are not just building a web form. You are building a system that handles protected health information under federal law, with fines up to $50,000 per violation and potential criminal charges for willful neglect.

So why build one at all? Because the architecture we are about to walk through works around these limitations. Patients never touch Airtable directly. n8n never stores PHI. And you get a system that is more flexible, more customizable, and significantly cheaper than the commercial alternatives — if you have the technical chops to maintain it.

If you followed along with our patient intake automation guide, you already have the foundation for this. That workflow handles the intake form. This one handles everything after.

What HIPAA Actually Requires for Patient Portals

Before we touch any code, you need to understand what the HIPAA Security Rule demands from a patient portal. This is not optional. This is the checklist your compliance officer — or the OCR auditor — will use to evaluate your system.

  1. End-to-end encryption: TLS 1.2 or higher for data in transit, AES-256 for data at rest
  2. Multi-factor authentication for all user accounts
  3. Role-based access controls with least-privilege principles
  4. Automatic session timeout after 30 minutes of inactivity
  5. Complete audit logging of every access to protected health information
  6. Business Associate Agreements with every vendor that touches PHI
  7. Emergency access procedures documented and tested
  8. Data backup and disaster recovery plan with tested restore procedures

Every piece of this checklist maps to a specific technical component in our architecture. If you cannot check all eight boxes, you are not HIPAA-compliant. Period. There is no "mostly compliant" — that is like being "mostly pregnant."

For practices in Volusia County, keep in mind that Florida HB 583 also strengthened data breach notification requirements. If you have a breach, you need to notify patients within 30 days. Building solid infrastructure now is cheaper than dealing with a breach later.

Here is what trips up most small practices: they focus on the technology and forget about the process. You can have perfect encryption and airtight access controls, but if your front desk staff shares login credentials, you have a HIPAA violation. If you do not have a documented procedure for what happens when a nurse leaves and you need to revoke their access immediately, you have a gap. The technology is necessary but not sufficient. You need policies that match your technical controls. Document who has access to what, how access gets granted and revoked, and what happens when something goes wrong. Print it out. Put it in a binder. Train your staff on it quarterly. That binder is what the OCR auditor wants to see — not your Docker Compose file.

The n8n + Airtable Architecture That Actually Works

Here is the architecture that respects Airtable's terms of service, works around n8n's lack of a BAA, and still gives you a fully functional HIPAA-compliant patient portal system.

The trick is separating concerns into three layers:

Layer 1: Patient-Facing Frontend. This is what your patients see and interact with. It is a standalone web application — it can be a static site with JavaScript, a React app, whatever you are comfortable deploying. This layer handles authentication, session management, MFA, and the user interface. Patients never see or touch Airtable.

Layer 2: n8n Orchestration. Self-hosted n8n sits in the middle, receiving API calls from the frontend and routing data to the right places. The critical design decision: n8n processes data but never stores it. Execution data saving is completely disabled. n8n is a pipe, not a bucket. PHI passes through, gets encrypted, and lands in Airtable — but it never persists in n8n's database.

Layer 3: Airtable Enterprise Backend. Airtable stores the structured patient data — encrypted at the field level before it ever arrives. Because patients interact with the frontend (Layer 1), not Airtable directly, you are compliant with Airtable's prohibition on portal use. Airtable is your database, not your portal.

This three-layer separation is what makes the whole thing work. Each layer does one job and does it well. The frontend handles the patient experience. n8n handles the logic. Airtable handles the storage. And the encryption layer between n8n and Airtable ensures that even if someone gained raw access to your Airtable base, they would see encrypted ciphertext — not patient names and Social Security numbers.

Why not just use one of those all-in-one portal platforms? Two reasons. First, most commercial portals lock you into their ecosystem. Want to integrate with a specialty EHR that they do not support? Tough luck. Need a custom notification workflow that sends different alerts based on appointment type? Not possible. The three-layer architecture gives you complete control over every connection point.

Second, and this matters for practices watching their budget, the infrastructure cost of this approach runs between $50 and $200 per month. Compare that to commercial portals that charge $200 to $500 per provider per month, and a five-provider practice saves $9,000 to $18,000 annually. That is a real number. That is a part-time medical assistant's salary. Of course, you need the technical skills to build and maintain it, which is where the tradeoff lives. We will get into the honest build-vs-buy analysis at the end of this guide.

One more thing about the architecture before we dive into setup: data flow direction matters. In this system, data always flows from the frontend through n8n to Airtable — never the reverse direction without going through n8n first. The patient never queries Airtable directly. Every request goes through n8n's validation and encryption layer. This creates a single chokepoint for security enforcement, which is exactly what you want. One gate to guard, not twenty.

Setting Up Self-Hosted n8n for Healthcare

You need to self-host n8n. The cloud version will not work for HIPAA because you cannot disable execution data saving on the cloud, and you have no control over where your data lives.

Here is the Docker Compose configuration. We are running n8n with PostgreSQL for reliability and Nginx for TLS termination.

yaml
version: "3.8"
 
services:
  n8n:
    image: n8nio/n8n:latest
    restart: always
    ports:
      - "5678:5678"
    environment:
      - N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_PORT=5432
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=${POSTGRES_USER}
      - DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
      - EXECUTIONS_DATA_SAVE_ON_ERROR=none
      - EXECUTIONS_DATA_SAVE_ON_SUCCESS=none
      - EXECUTIONS_DATA_SAVE_ON_PROGRESS=false
      - EXECUTIONS_DATA_SAVE_MANUAL_EXECUTIONS=false
      - N8N_DIAGNOSTICS_ENABLED=false
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      - postgres
 
  postgres:
    image: postgres:16-alpine
    restart: always
    environment:
      - POSTGRES_USER=${POSTGRES_USER}
      - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
      - POSTGRES_DB=n8n
    volumes:
      - postgres_data:/var/lib/postgresql/data
 
volumes:
  n8n_data:
  postgres_data:

The four environment variables that matter most for HIPAA are the EXECUTIONS_DATA_SAVE_* settings. Setting all of them to none or false ensures that n8n does not store the data that flows through your workflows. This is how you make n8n a pipe instead of a bucket. If n8n never stores PHI, the lack of a BAA becomes a manageable risk rather than a compliance showstopper.

The N8N_ENCRYPTION_KEY encrypts all stored credentials — your Airtable API keys, any webhook secrets, database passwords. Generate it with:

bash
openssl rand -hex 32

That gives you a 256-bit key. Store it somewhere safe. If you lose it, you lose access to every credential stored in n8n. Back it up outside of your Docker environment.

N8N_DIAGNOSTICS_ENABLED=false turns off telemetry. This is not just about privacy — it prevents any metadata about your workflows from leaving your infrastructure.

A note about hosting location: if you are running this for a practice in Deltona or anywhere in Central Florida, choose a data center in the US-East region. AWS us-east-1 in Virginia or Azure's East US region are both solid choices. Keeping your data geographically close reduces latency for your patients and keeps everything within US jurisdiction, which simplifies your compliance posture. Some practices we work with in Volusia County prefer hosting on-premises — a small server in the office closet running Docker. That works too, as long as you have reliable power, internet, and a backup strategy. Hurricane season in Florida is not a hypothetical risk for your infrastructure.

For your database passwords and encryption keys, use a proper secrets manager if you can — AWS Secrets Manager, Azure Key Vault, or HashiCorp Vault. At minimum, keep your .env file outside of version control (add it to .gitignore), store a backup in a secure location, and restrict file permissions so only the Docker service account can read it. If someone gains access to your N8N_ENCRYPTION_KEY, they can decrypt every credential stored in n8n. Treat it like the master key to your practice's digital infrastructure, because that is exactly what it is.

Configuring Airtable Enterprise as Your Backend

Airtable supports HIPAA compliance only on the Enterprise Scale plan. You need to sign their Health Information Exhibit, which includes the Business Associate Addendum. Without this, Airtable is not compliant for any healthcare data — regardless of how well you encrypt it.

Here is the schema for your Patient Portal base. Notice that every PHI field stores encrypted text, not raw data.

Patients Table:

FieldTypePurpose
patient_idAutonumberPrimary key
first_name_encryptedLong textAES-256 encrypted
last_name_encryptedLong textAES-256 encrypted
email_encryptedLong textAES-256 encrypted
phone_encryptedLong textAES-256 encrypted
dob_encryptedLong textAES-256 encrypted
consent_givenCheckboxHIPAA authorization
consent_timestampDateWhen consent recorded

Portal Requests Table:

FieldTypePurpose
request_idAutonumberPrimary key
patient_idLink to PatientsRelational
request_typeSingle selectappointment, records, message, rx_refill
request_data_encryptedLong textAES-256 encrypted JSON
statusSingle selectpending, processing, completed, failed
audit_hashSingle line textSHA-256 integrity verification

The encrypted fields look like this in practice: a1b2c3d4e5f6:9f8e7d6c5b4a:3e2d1c0b... — that is the initialization vector, authentication tag, and ciphertext, separated by colons. Without the encryption key, it is meaningless. With it, you can decrypt back to the original data.

One important limitation: do not enable Airtable AI on any workspace that contains PHI. Airtable's HIPAA documentation explicitly excludes AI features from their compliance coverage. If you accidentally enable it, you have potentially exposed PHI to an AI system without a BAA.

Building the Patient-Facing Portal Frontend

The frontend is what your patients actually see. It can be a static site with JavaScript, a React app, a Next.js application — whatever you are comfortable deploying and maintaining. The framework does not matter much. What matters is the session management layer, because that is where HIPAA compliance lives in the frontend.

Your portal frontend needs to handle four things: authentication with MFA, session management with automatic 30-minute timeouts, form submission to n8n webhooks, and response display. The first two are where most DIY portals fail their compliance audits.

Here is a session management module that enforces HIPAA's automatic logoff requirement:

javascript
// session-manager.mjs
// HIPAA-compliant session management for the patient portal frontend
// Enforces: 30-min inactivity timeout, session logging, MFA tracking
 
const THIRTY_MINUTES_MS = 30 * 60 * 1000;
const sessions = new Map();
 
export function createSession(patientId, ipAddress) {
  const sessionId = crypto.randomUUID();
  const now = Date.now();
 
  sessions.set(sessionId, {
    patientId,
    ipAddress,
    createdAt: now,
    lastActivity: now,
    mfaVerified: false,
  });
 
  console.log(
    JSON.stringify({
      event: "session_created",
      sessionId,
      patientId,
      ipAddress,
      timestamp: new Date(now).toISOString(),
    }),
  );
 
  return sessionId;
}
 
export function validateSession(sessionId, ipAddress) {
  const session = sessions.get(sessionId);
 
  if (!session) {
    return { valid: false, reason: "session_not_found" };
  }
 
  // Check IP consistency (prevent session hijacking)
  if (session.ipAddress !== ipAddress) {
    destroySession(sessionId, "ip_mismatch");
    return { valid: false, reason: "ip_changed" };
  }
 
  // Check 30-minute inactivity timeout (HIPAA requirement)
  const inactiveMs = Date.now() - session.lastActivity;
  if (inactiveMs > THIRTY_MINUTES_MS) {
    destroySession(sessionId, "inactivity_timeout");
    return { valid: false, reason: "session_expired" };
  }
 
  // Check MFA
  if (!session.mfaVerified) {
    return { valid: false, reason: "mfa_required" };
  }
 
  // Update last activity
  session.lastActivity = Date.now();
  return { valid: true, patientId: session.patientId };
}
 
export function markMfaVerified(sessionId) {
  const session = sessions.get(sessionId);
  if (session) {
    session.mfaVerified = true;
    console.log(
      JSON.stringify({
        event: "mfa_verified",
        sessionId,
        patientId: session.patientId,
        timestamp: new Date().toISOString(),
      }),
    );
  }
}
 
export function destroySession(sessionId, reason = "manual_logout") {
  const session = sessions.get(sessionId);
  if (session) {
    console.log(
      JSON.stringify({
        event: "session_destroyed",
        sessionId,
        patientId: session.patientId,
        reason,
        durationMs: Date.now() - session.createdAt,
        timestamp: new Date().toISOString(),
      }),
    );
    sessions.delete(sessionId);
  }
}
 
// Cleanup expired sessions every 5 minutes
setInterval(
  () => {
    const now = Date.now();
    for (const [id, session] of sessions) {
      if (now - session.lastActivity > THIRTY_MINUTES_MS) {
        destroySession(id, "cleanup_sweep");
      }
    }
  },
  5 * 60 * 1000,
);

Let me explain the HIPAA-critical pieces here, because they are easy to get wrong.

The 30-minute inactivity timeout is the industry standard for HIPAA session management. When validateSession() detects 30 minutes of idle time, the session is destroyed and the patient must re-authenticate. This catches the scenario where someone leaves a browser open on a shared computer or a public kiosk in a waiting room.

The IP consistency check detects potential session hijacking. If a session was created from one IP address and suddenly starts receiving requests from another, the safest response is to kill the session immediately. Yes, this will occasionally inconvenience patients on mobile networks where IP addresses change. That is better than a session hijack exposing someone's medical records.

The MFA verification flag means a session exists but is not fully authenticated until the patient completes MFA. Even if an attacker obtains a valid session token, they cannot access PHI without also completing the multi-factor challenge.

The structured JSON logging on every session lifecycle event feeds directly into your audit trail. Every session creation, MFA verification, timeout, and destruction is logged with the patient identifier, IP address, and timestamp. These logs become evidence during compliance audits.

One caveat: this uses an in-memory Map(), which is fine for a single portal instance. If you scale to multiple frontend servers behind a load balancer, you will need Redis-backed session storage to share session state across instances.

The Complete n8n Workflow

Here is the full workflow JSON you can import directly into your self-hosted n8n instance. It handles incoming portal requests from your frontend, validates them, encrypts PHI fields, stores the encrypted record in Airtable, writes an audit log entry, and returns a success response.

json
{
  "name": "HIPAA Patient Portal - Request Handler",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "patient-portal-request",
        "authentication": "headerAuth",
        "options": { "rawBody": true }
      },
      "name": "Portal Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [250, 300]
    },
    {
      "parameters": {
        "jsCode": "const body = JSON.parse($input.first().json.body);\nconst requiredFields = ['patient_id', 'request_type', 'request_data', 'auth_token'];\nconst missing = requiredFields.filter(f => !body[f]);\nif (missing.length > 0) {\n  throw new Error(`Missing required fields: ${missing.join(', ')}`);\n}\nconst validTypes = ['appointment', 'records', 'message', 'rx_refill'];\nif (!validTypes.includes(body.request_type)) {\n  throw new Error(`Invalid request_type: ${body.request_type}`);\n}\nreturn [{ json: {\n  patient_id: body.patient_id,\n  request_type: body.request_type,\n  request_data: body.request_data,\n  auth_token: body.auth_token,\n  received_at: new Date().toISOString()\n}}];"
      },
      "name": "Validate Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [470, 300]
    },
    {
      "parameters": {
        "jsCode": "const crypto = require('crypto');\nconst ENCRYPTION_KEY = $env.FIELD_ENCRYPTION_KEY;\nconst ALGORITHM = 'aes-256-gcm';\nfunction encrypt(text) {\n  const iv = crypto.randomBytes(16);\n  const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv);\n  let encrypted = cipher.update(text, 'utf8', 'hex');\n  encrypted += cipher.final('hex');\n  const authTag = cipher.getAuthTag().toString('hex');\n  return `${iv.toString('hex')}:${authTag}:${encrypted}`;\n}\nconst input = $input.first().json;\nconst encryptedData = encrypt(JSON.stringify(input.request_data));\nconst auditHash = crypto.createHash('sha256').update(JSON.stringify({ patient_id: input.patient_id, request_type: input.request_type, timestamp: input.received_at })).digest('hex');\nreturn [{ json: {\n  patient_id: input.patient_id,\n  request_type: input.request_type,\n  request_data_encrypted: encryptedData,\n  received_at: input.received_at,\n  audit_hash: auditHash,\n  status: 'pending'\n}}];"
      },
      "name": "Encrypt PHI Fields",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [690, 300]
    },
    {
      "parameters": {
        "operation": "create",
        "application": "={{ $env.AIRTABLE_BASE_ID }}",
        "table": "Portal_Requests",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "patient_id": "={{ $json.patient_id }}",
            "request_type": "={{ $json.request_type }}",
            "request_data_encrypted": "={{ $json.request_data_encrypted }}",
            "status": "={{ $json.status }}",
            "audit_hash": "={{ $json.audit_hash }}"
          }
        }
      },
      "name": "Create Airtable Record",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [910, 300]
    },
    {
      "parameters": {
        "operation": "create",
        "application": "={{ $env.AIRTABLE_BASE_ID }}",
        "table": "Audit_Log",
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "timestamp": "={{ $json.received_at }}",
            "actor": "=patient:{{ $json.patient_id }}",
            "action": "create",
            "resource": "=portal_request:{{ $json.request_type }}",
            "result": "success"
          }
        }
      },
      "name": "Write Audit Log",
      "type": "n8n-nodes-base.airtable",
      "typeVersion": 2.1,
      "position": [1130, 300]
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ success: true, request_id: $json.id, status: 'pending' }) }}"
      },
      "name": "Return Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [1350, 300]
    }
  ],
  "connections": {
    "Portal Webhook": {
      "main": [[{ "node": "Validate Request", "type": "main", "index": 0 }]]
    },
    "Validate Request": {
      "main": [[{ "node": "Encrypt PHI Fields", "type": "main", "index": 0 }]]
    },
    "Encrypt PHI Fields": {
      "main": [
        [{ "node": "Create Airtable Record", "type": "main", "index": 0 }]
      ]
    },
    "Create Airtable Record": {
      "main": [[{ "node": "Write Audit Log", "type": "main", "index": 0 }]]
    },
    "Write Audit Log": {
      "main": [[{ "node": "Return Success", "type": "main", "index": 0 }]]
    }
  },
  "settings": {
    "saveDataErrorExecution": "none",
    "saveDataSuccessExecution": "none",
    "saveManualExecutions": false,
    "saveExecutionProgress": false
  }
}

Let me walk you through what each node does.

The Portal Webhook listens for POST requests from your frontend. It uses header authentication — your frontend includes a secret token in the request header, and n8n rejects anything without it. The rawBody option ensures we get the complete request for validation.

The Validate Request node is your first line of defense. It checks that every required field exists and that the request_type is one of four valid options: appointment, records, message, or rx_refill. Anything unexpected gets rejected with an error. This is defense in depth — even if someone bypasses your frontend and hits the webhook directly, they cannot inject arbitrary data.

The Encrypt PHI Fields node is where the HIPAA magic happens. It takes the patient's request data — which contains PHI — and encrypts it using AES-256-GCM before it ever touches Airtable. GCM mode is important because it provides both encryption and authentication. If someone tampers with the ciphertext, decryption fails. You know the data is intact.

The node also generates a SHA-256 audit hash of the request metadata. This hash lets you verify that audit log entries have not been tampered with after the fact.

Create Airtable Record stores the encrypted data. Notice that the only unencrypted fields are patient_id (which you need for lookups), request_type (for routing), and status (for workflow tracking). Everything containing actual PHI is encrypted ciphertext.

Write Audit Log creates a separate record in your Audit_Log table. Every interaction with patient data gets logged — who did it, what they did, when they did it, and whether it succeeded. This is not optional under HIPAA. The audit trail needs to be complete and tamper-evident.

Return Success sends a minimal JSON response back to the frontend. Notice it only returns the Airtable record ID and status — no PHI echoed back in the response. This is a deliberate security decision. If someone intercepts the response — even over TLS, defense in depth assumes they might — they get a record ID and a status string. Nothing useful. Nothing protected.

What about error handling? In the production version, we wrap the entire flow in a try-catch pattern using n8n's error workflow feature. If the Airtable write fails, the error workflow sends a notification to the practice's IT contact without including any PHI in the error message. The raw error details get logged locally on the n8n server where they are accessible only via SSH. Never put PHI in error messages, error logs, or monitoring dashboards. It is one of the most common HIPAA mistakes in custom-built systems.

You might also wonder why we are not using n8n's built-in Airtable trigger node to handle responses back to patients. The answer is timing and security. The Airtable trigger polls for changes, which introduces latency. More importantly, it means n8n would be pulling data from Airtable — including potentially unprocessed PHI — which we want to avoid. By keeping the data flow strictly one-directional through n8n (frontend to Airtable), we minimize the surface area where PHI could be exposed or cached.

Security Scripts You Need on Day One

The n8n workflow handles the automation, but you need supporting scripts for encryption, audit logging, and session management. Here are the three scripts we deploy with every portal build.

Field-Level Encryption Helper (MJS)

javascript
// hipaa-encrypt.mjs — Field-level AES-256-GCM encryption
import {
  createCipheriv,
  createDecipheriv,
  randomBytes,
  createHash,
} from "crypto";
 
const ALGORITHM = "aes-256-gcm";
 
function getKey() {
  const key = process.env.FIELD_ENCRYPTION_KEY;
  if (!key || key.length !== 64) {
    console.error(
      "FIELD_ENCRYPTION_KEY must be 64-char hex. Generate: openssl rand -hex 32",
    );
    process.exit(1);
  }
  return Buffer.from(key, "hex");
}
 
export function encrypt(plaintext) {
  const key = getKey();
  const iv = randomBytes(16);
  const cipher = createCipheriv(ALGORITHM, key, iv);
  let enc = cipher.update(plaintext, "utf8", "hex");
  enc += cipher.final("hex");
  const tag = cipher.getAuthTag().toString("hex");
  return `${iv.toString("hex")}:${tag}:${enc}`;
}
 
export function decrypt(ciphertext) {
  const key = getKey();
  const [ivHex, tagHex, encHex] = ciphertext.split(":");
  const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(ivHex, "hex"));
  decipher.setAuthTag(Buffer.from(tagHex, "hex"));
  let dec = decipher.update(encHex, "hex", "utf8");
  dec += decipher.final("utf8");
  return dec;
}

This script handles the AES-256-GCM encryption that protects PHI in your Airtable fields. The encryption key lives in an environment variable — never in code, never in a config file, never in version control. Each encryption operation generates a fresh initialization vector, so even if you encrypt the same patient name twice, the ciphertext will be different. That is called semantic security, and it prevents attackers from identifying patterns in your encrypted data.

HIPAA Audit Logger (Python)

python
#!/usr/bin/env python3
"""hipaa_audit_logger.py — Dual-write audit logging for HIPAA compliance"""
 
import hashlib, json, os, sys
from datetime import datetime, timezone
from pathlib import Path
import requests
 
LOG_DIR = Path("./audit_logs")
LOG_DIR.mkdir(exist_ok=True)
 
def create_entry(action, actor, resource, result, ip="127.0.0.1"):
    entry = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "actor": actor, "action": action,
        "resource": resource, "ip_address": ip, "result": result
    }
    entry["integrity_hash"] = hashlib.sha256(
        json.dumps(entry, sort_keys=True).encode()
    ).hexdigest()
    return entry
 
def log_to_airtable(entry):
    api_key = os.environ["AIRTABLE_API_KEY"]
    base_id = os.environ["AIRTABLE_BASE_ID"]
    url = f"https://api.airtable.com/v0/{base_id}/Audit_Log"
    try:
        r = requests.post(url, headers={
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }, json={"records": [{"fields": entry}]}, timeout=10)
        r.raise_for_status()
        return True
    except Exception as e:
        print(f"Airtable write failed: {e}", file=sys.stderr)
        return False
 
def log_event(action, actor, resource, result, ip="127.0.0.1"):
    entry = create_entry(action, actor, resource, result, ip)
    # Always write locally first — never lose an audit event
    log_file = LOG_DIR / f"hipaa_{datetime.now().strftime('%Y-%m')}.jsonl"
    with open(log_file, "a") as f:
        f.write(json.dumps(entry) + "\n")
    # Then attempt Airtable
    log_to_airtable(entry)
    return entry

Notice the dual-write pattern here. The audit logger writes to two places: a local JSONL file and your Airtable Audit_Log table. The local file is your safety net. If Airtable is down, if your API key expires, if anything goes wrong with the remote write — you still have a complete audit trail on disk. Under HIPAA, losing audit events is a violation. This dual-write approach means you never lose one.

Each entry includes a SHA-256 integrity hash. If someone edits an audit log entry after the fact, the hash will not match. That is your tamper detection. It is not bulletproof — a sophisticated attacker could recalculate the hash — but it catches accidental or casual modifications.

For production deployments, we chain the hashes. Each audit entry includes the hash of the previous entry, creating a linked chain similar to a blockchain. If anyone modifies an entry in the middle, every subsequent hash breaks. This makes mass tampering practically impossible without detection. The version shown above is the starter implementation — the chained version is what we deploy for practices that handle more than a few hundred patients.

One thing we always tell practices: do not skimp on audit logging. When an OCR audit happens — and in Volusia County, it is a matter of when, not if — the first thing they ask for is your access logs. How many times was this patient's record accessed? By whom? When? If you cannot answer those questions in minutes, you have a problem. The audit system we built here answers all of them. Every portal request, every login, every failed authentication attempt gets recorded with a timestamp, actor, and result. That is the evidence that proves you take compliance seriously.

What the Custom-Built Version Looks Like

Everything above is the DIY version. It works. It is compliant. But it is also the minimum viable product.

When we build patient portal systems for practices across Central Florida — from Deltona to Daytona Beach — the production-grade version includes significantly more:

  • OAuth 2.0 / OIDC integration with identity providers like Auth0 or Okta, giving you enterprise-grade authentication without building it yourself
  • EHR bidirectional sync using HL7 FHIR APIs, so patient records flow automatically between your portal and your electronic health records system
  • Automated compliance monitoring that continuously checks your system against HIPAA requirements and alerts you when something drifts out of compliance
  • Rate limiting and DDoS protection at the Nginx layer, preventing abuse of your portal endpoints
  • Multi-region failover so your portal stays up even during AWS or Azure outages
  • Automated penetration testing on a quarterly schedule, with remediation workflows built into n8n
  • SOC 2 Type II audit preparation with pre-built evidence collection workflows
  • Patient consent management with versioned consent forms and automated re-consent workflows when your privacy practices change
  • Automated data retention and deletion policies that comply with both HIPAA and state law

The DIY version gets you from zero to functional. The custom-built version gets you from functional to bulletproof. If your practice handles more than a few hundred patients or if you are part of a multi-location group, the custom version is worth the investment.

Want us to build this for you? Schedule a free discovery call and we will assess your current setup, identify compliance gaps, and scope a portal system that fits your practice and budget. We work with practices across Volusia County and beyond.

When to Build vs Buy: The Honest Assessment

Here is the decision matrix we use when advising practices on patient portal strategy:

FactorBuild It YourselfBuy Commercial
Monthly cost$50–200 (infrastructure)$200–500+ (per provider)
Setup time2–4 weeks1–3 days
CustomizationUnlimitedLimited to vendor options
Technical skill neededHigh (DevOps, security)Low (admin configuration)
Compliance burdenAll on youShared with vendor
EHR integrationCustom (flexible)Pre-built (limited)
Best forTech-savvy practices, custom workflowsStandard needs, quick deployment

Build if: You have in-house technical staff (or a trusted automation and AI consulting partner), you need workflows that commercial portals cannot support, or you are integrating with non-standard EHR systems. Practices running specialty workflows — concierge medicine, clinical research, multi-location coordination — often benefit most from building.

Buy if: You are a practice with fewer than five providers, you need a portal running within days rather than weeks, and your workflows are standard. Solutions like SimplePractice, CharmHealth, and athenahealth offer solid portal features with built-in HIPAA compliance. The monthly cost is higher, but the compliance burden is dramatically lower.

The hybrid approach works too. Use a commercial portal for the patient-facing interface but add n8n automation behind the scenes for custom workflows, notifications, and integrations. You get the compliance coverage of a commercial vendor with the flexibility of custom automation.

Not sure which approach fits your practice? Take our free automation quiz to get a personalized recommendation based on your practice size, technical resources, and workflow complexity.

Frequently Asked Questions

Is Airtable HIPAA compliant for storing patient data?

Airtable supports HIPAA compliance only on its Enterprise Scale plan with a signed Business Associate Agreement. However, Airtable explicitly prohibits its use as a patient-facing portal. You can use it as a backend data layer for patient records, but the portal interface patients interact with must be built separately.

Does n8n offer a BAA for HIPAA compliance?

n8n does not currently offer a Business Associate Agreement or HIPAA compliance guarantees. For healthcare workflows, self-host n8n on your own infrastructure, disable execution data saving, enable credential encryption, and ensure n8n never becomes a repository of unsecured protected health information.

What security features must a HIPAA-compliant patient portal have?

HIPAA requires end-to-end encryption in transit and at rest, unique user identification with multi-factor authentication, role-based access controls, automatic session logoff after 30 minutes of inactivity, complete audit trails of all data access, and emergency access procedures.

How much does it cost to build a DIY patient portal?

A self-built patient portal using n8n and Airtable costs roughly $50 to $200 per month for infrastructure — Airtable Enterprise pricing, a cloud VPS for self-hosted n8n, and SSL certificates. This compares to $200 to $500 per month for commercial portal solutions, though DIY requires significantly more technical expertise to maintain.

Should a small practice build or buy a patient portal?

Most small practices with fewer than five providers should buy a commercial HIPAA-compliant portal. Building your own makes sense when you need highly customized workflows, already have technical staff, or need tight integration with non-standard systems. The compliance burden of a DIY portal is substantial and ongoing.


Building a patient portal is not trivial. The compliance requirements are real, the technical complexity is significant, and the ongoing maintenance is a commitment. But if you have the right skills — or the right partner — the n8n + Airtable approach gives you a system that is more flexible, more transparent, and more cost-effective than the commercial alternatives. The architecture we walked through keeps Airtable compliant with its own terms, works around n8n's lack of a BAA, and gives your patients a portal experience that rivals what the big hospital networks offer.

Just remember: the goal is not to build the cheapest system. The goal is to build a system that protects your patients' data, keeps your practice compliant, and scales with you as you grow. Start with the DIY version if your budget demands it. Graduate to the custom-built version when your practice is ready.

Need help implementing this?

We build automation systems like this for clients every day.