It's time to revisit everyone's two favorite topics, Row Level Security (RLS) and HIPAA compliance. I'm here to give the people what they want, so here is my take on how to create a safe and orderly place for your legally-protected patient data to live.
If you’re building a patient-focused web app and you’re not thinking about HIPAA compliance, you haven't seen the penalty structure for violations. For the rest of us, protecting patient data isn’t just a checkbox—it’s a survival skill.
What the Heck is Row Level Security, and Why Should You Care?
Row Level Security (RLS) is PostgreSQL’s way of saying, “Welcome, but stay in your assigned space.” Your users become kinda like guests in a hotel, only if door locks were as cool as SQL policies. RLS lets you centralize your access logic, so you can focus on giving your guests a great experience.
And yeah, it’s a HIPAA win: RLS helps you enforce the “minimum necessary” access rule, so you’re not handing out master keys when someone just needs access to one room.
Shared Policies Using Many-to-Many Relationships
Row Level Security in PostgreSQL is powerful enough to handle even complex relationships like many-to-many mappings between clinicians and patients. By leveraging join tables and smart policies, you can ensure HIPAA compliance while maintaining a scalable and secure database structure. We'll have 3 tables; patients
, clinicians
, and clinicians_patients
.
1. Create Policies for Clinicians
Let’s say there’s a many-to-many relationship between clinicians and patients managed through a clinicians_patients
join table. We want clinicians to only see their own patients, but not others. Here's how we can get there:
CREATE POLICY clinician_patient_access ON patients
FOR SELECT, UPDATE
USING (EXISTS (
SELECT 1
FROM clinicians_patients
WHERE clinicians_patients.patient_id = patients.id
AND clinicians_patients.clinician_id = current_setting('app.current_user')::int
));
This policy works by checking if the clinician_id
in the clinicians_patients
join table matches the current user's session variable. To make this work, your application must set the app.current_user
session variable to the clinician's ID upon authentication (more on that in a second).
2. Enable RLS on Your Tables
First, you need to tell PostgreSQL to actually care about row-level access. By default, it’s blissfully ignorant. We could enable RLS on all three tables: patients
, clinicians
, and clinicians_patients
.
ALTER TABLE patients ENABLE ROW LEVEL SECURITY;
ALTER TABLE clinicians ENABLE ROW LEVEL SECURITY;
ALTER TABLE clinicians_patients ENABLE ROW LEVEL SECURITY;
Congratulations, you’ve just hired a beefy database bouncer! But right now, he’s still letting everyone in. Let’s fix that.
One RLS Policy to Rule Them All
By default, superusers and table owners can bypass RLS, which can be risky in serverless setups where connections are shared. To lock down access, use:
ALTER TABLE patients FORCE ROW LEVEL SECURITY;
ALTER TABLE clinicians FORCE ROW LEVEL SECURITY;
ALTER TABLE clinicians_patients FORCE ROW LEVEL SECURITY;
This ensures all access follows your RLS policies, even for privileged users. In serverless environments, this step is crucial to protect sensitive data and maintain compliance. Now, not even the table owner can bypass your policies. (Take that, creepy snoopers.)
3. Serverless Gotchas
Serverless PostgreSQL is stateless, so we can’t rely on sticky sessions or nerd magic. We'll need to establish PostgreSQL session variables at the start of each connection. Our app’s authentication layer should handle this — don’t trust anyone!. But since we're cool, here's the deets:
Set the PostgreSQL Session Variable:
In your app, set the user session after successfully establishing a connection:
// Node.js example with pg library
const { Client } = require('pg');
async function setSessionVariable(userId) {
const client = new Client({ connectionString: process.env.DATABASE_URL });
// Set the session variable for the current user
await client.query('SET SESSION "app.current_user" = $1', [userId]);
}
Is All That Really Necessary?
Setting session variables at the start of each connection makes sure that user-specific context, based the current user's ID and role, is explicitly defined. This context is critical for enforcing RLS policies, which depend on session variables to determine which rows a user can access. Without session variables, the database would lack the necessary context to apply access controls, potentially leading to unauthorized data exposure or errors.
Conclusion
Row Level Security in PostgreSQL isn’t just a neat trick—it’s a practical, scalable way to remain HIPAA-compliant without losing your mind (or your patients' data). In a serverless world, it’s even more important to simplfy access logic, to prevent unforseen challenges from becoming critical failures.
With some thoughtful RLS policies, we can let PostgreSQL do the heavy lifting, while we sit back and admire what we accomplished. And if someone asks why you’re so calm about HIPAA audits, just wink and say, “It’s all in the rows, my friend.”
Further Reading:
- PostgreSQL RLS Documentation
- HIPAA Security Rule Summary (HHS.gov)
- Serverless PostgreSQL Providers: Neon, Supabase, AWS Aurora
Share This Post
If you found this post interesting, please consider sharing it to your social networks.