You push to staging. It works.

You deploy to prod. And suddenly, users can write to audit_logs.

The problem?

RBAC drift between environments — subtle, silent, and security-critical.

This guide walks through detecting RBAC differences between dev and prod Hasura metadata by comparing generated Role × Field matrices.


1. Assumptions

  • You export Hasura metadata from both environments:
hasura metadata export --endpoint https://dev-api.example.com --output metadata-dev/
  hasura metadata export --endpoint https://prod-api.example.com --output metadata-prod/
  • You have a matrix generator like rbac-matrix.js (see previous article) that produces:
    • rbac-dev.csv
    • rbac-prod.csv

2. Diff Goals

We want to detect:

✅ Added/removed access

✅ Role gaining new permissions

✅ Field-level permission divergence

✅ Action type mismatches (e.g. SELECT allowed in dev, denied in prod)


3. Matrix Normalization

Ensure both rbac-dev.csv and rbac-prod.csv are:

  • Sorted by Role, Table, Field
  • Have the same headers: Role,Table,Field,SELECT,INSERT,UPDATE,DELETE

4. Diff Script (TypeScript / Node.js)

import fs from 'fs';
import { parse } from 'csv-parse/sync';

const dev = parse(fs.readFileSync('rbac-dev.csv'), { columns: true });
const prod = parse(fs.readFileSync('rbac-prod.csv'), { columns: true });

const key = (row: any) => `${row.Role}::${row.Table}::${row.Field}`;

const mapDev = new Map(dev.map(row => [key(row), row]));
const mapProd = new Map(prod.map(row => [key(row), row]));

const allKeys = new Set([...mapDev.keys(), ...mapProd.keys()]);

for (const k of allKeys) {
  const d = mapDev.get(k);
  const p = mapProd.get(k);
  if (!d) {
    console.log(`🟢 Added in PROD: ${k}`);
    continue;
  }
  if (!p) {
    console.log(`🔴 Missing in PROD: ${k}`);
    continue;
  }

  ['SELECT', 'INSERT', 'UPDATE', 'DELETE'].forEach(op => {
    if ((d[op] || '') !== (p[op] || '')) {
      console.log(`⚠️  Diff in ${k} → ${op}: DEV=${d[op]} PROD=${p[op]}`);
    }
  });
}

5. CI Integration

- name: Generate RBAC Matrices
  run: |
    node rbac-matrix.js metadata-dev/ > rbac-dev.csv
    node rbac-matrix.js metadata-prod/ > rbac-prod.csv

- name: Detect RBAC Drift
  run: node diff-rbac.js

Use exitCode = 1 if drift is critical.


6. Optional Enhancements

  • Export to Markdown table for PR comments
  • Group diffs by role or table
  • Visual HTML diff viewer with color-coded deltas
  • Auto-approve deploy only if diff is "approved"

Final Thoughts

Your app changes often.

Your schema changes faster.

Your permissions? They must stay in sync — or you're inviting trouble.

With matrix diffing in place, RBAC drift becomes an explicit, testable signal — not an incident waiting to happen.

Next:

  • PR Bot to annotate diffs
  • Approval workflow with policy-as-code
  • Field-level explainers for detected mismatches

Track the access. Visualize the change. Secure the drift.