HIPAA-07: Database Row-Level Security — Compliance Finding & Remediation Guide
HIPAA-07: Database Row-Level Security
Compliance Control: HIPAA-07 | Framework: HIPAA | Version Identified: v0.1.156
Overview
This document describes a defence-in-depth compliance gap identified under HIPAA control HIPAA-07: the absence of Postgres Row-Level Security (RLS) policies on the Neon database.
The platform currently enforces multi-tenant data isolation at the application layer using userId predicates in Drizzle ORM queries. While this provides functional isolation today, it lacks a database-layer enforcement mechanism. A single query regression that omits a userId WHERE clause could expose data across tenants.
No active vulnerability or data exposure has been identified. This is a defence-in-depth gap.
What Is Row-Level Security?
Postgres Row-Level Security (RLS) is a database-native access control mechanism that restricts which rows a session can read or write, independent of the application layer. When RLS is enabled on a table, every query against that table is automatically filtered by the active policy — even if the application query omits a WHERE clause.
This provides a critical second layer of protection in multi-tenant SaaS applications.
Current Architecture
| Layer | Mechanism | Status |
|---|---|---|
| Application | Drizzle ORM userId predicates on all queries | ✅ In place |
| Database | Postgres Row-Level Security policies on Neon | ❌ Not configured |
Affected Tables
The following user-scoped tables require RLS policies:
peoplematchesmatchReviews- All other tables containing user-scoped data
Recommended Remediation
Step 1 — Enable RLS on each affected table
Run the following SQL on your Neon Postgres instance for each user-scoped table:
ALTER TABLE people ENABLE ROW LEVEL SECURITY;
ALTER TABLE matches ENABLE ROW LEVEL SECURITY;
ALTER TABLE matchReviews ENABLE ROW LEVEL SECURITY;
Step 2 — Create isolation policies
Create a USING policy on each table that restricts row access to the current user. The policy reads the user identity from a Postgres session configuration variable (app.current_user_id):
-- people
CREATE POLICY user_isolation ON people
USING (user_id = current_setting('app.current_user_id')::text);
-- matches
CREATE POLICY user_isolation ON matches
USING (user_id = current_setting('app.current_user_id')::text);
-- matchReviews
CREATE POLICY user_isolation ON matchReviews
USING (user_id = current_setting('app.current_user_id')::text);
Repeat for any additional user-scoped tables.
Step 3 — Set user context at session start
At the beginning of each database session (before executing any queries), set the app.current_user_id configuration parameter to the authenticated user's ID:
SELECT set_config('app.current_user_id', '<userId>', true);
In a Node.js / Drizzle ORM context, this should be executed as the first statement in each request's database transaction or connection:
await db.execute(
sql`SELECT set_config('app.current_user_id', ${userId}, true)`
);
Note: The third argument to
set_config(true) scopes the setting to the current transaction. Usefalseif you are working at the connection level rather than the transaction level.
Risk Assessment
| Property | Detail |
|---|---|
| Current risk | Low — application-layer isolation is functioning |
| Residual risk without remediation | High — any query bug omitting userId would expose cross-tenant data |
| Remediation complexity | Low — SQL migrations only, no application code changes required for the database layer |
| Compliance impact | Satisfies database-layer isolation requirement under HIPAA-07 |
References
- Postgres Row-Level Security documentation
- Neon Postgres RLS guide
- HIPAA Security Rule — Technical Safeguards, § 164.312(a)(1): Access Control