All Docs
FeaturesCalmony Sanctions MonitorUpdated March 12, 2026

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

LayerMechanismStatus
ApplicationDrizzle ORM userId predicates on all queries✅ In place
DatabasePostgres Row-Level Security policies on Neon❌ Not configured

Affected Tables

The following user-scoped tables require RLS policies:

  • people
  • matches
  • matchReviews
  • 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. Use false if you are working at the connection level rather than the transaction level.


Risk Assessment

PropertyDetail
Current riskLow — application-layer isolation is functioning
Residual risk without remediationHigh — any query bug omitting userId would expose cross-tenant data
Remediation complexityLow — SQL migrations only, no application code changes required for the database layer
Compliance impactSatisfies database-layer isolation requirement under HIPAA-07

References