In our previous blog, we covered why and how we adopted Row-Level Security (RLS) at Rocketlane to enhance data security, streamline permissions handling, and simplify query logic.
You can check it out here if you haven’t already. Now, let's dive deeper into the practical aspects of implementing RLS, sharing critical learnings and challenges along the way.
Our transition to RLS required careful planning and execution. Here's how we did it:
Initially, each table had complex permission logic with multiple JOIN operations across various permission-related tables. With RLS, our objective was clear: replace these JOIN-intensive queries with simpler direct SELECT queries on RLS-enabled tables.
To smoothly migrate each table, we:
Our controlled rollout followed a meticulous testing strategy:
This gradual rollout approach significantly reduced risk, providing us peace of mind as we enhanced security.
One significant advantage of RLS was simplifying authorization logic within our API filter layer. Previously, verifying user permissions required additional permission checks before executing API calls. Post-RLS, the filter layer streamlined this:
We soon noticed performance challenges as our session variables grew increasingly complex. Initially, these variables—stored as JSONB maps—contained extensive permissions data, much of it unnecessary for immediate query execution. A heavy query that originally took ~30 seconds dropped to ~9 seconds once we optimized these variables, stripping away irrelevant data.
The key takeaway here is that RLS session variables should ideally stay small and predictable in size. The amount of data going into them should be mostly fixed and not grow unbounded over time.
In our case, as the computation of these session variables itself became expensive, we introduced Redis-based caching. This ensured stable, predictable response times for authorization checks, even as the underlying data grew. Caching proved especially useful when growth in session variable complexity was unavoidable.
Additionally, by flattening critical permission details into columns within RLS-enabled tables, we eliminated the need for nested inner queries in RLS policies, further boosting query performance.
One insightful discovery emerged from scenarios where a resource (e.g., a task) inherently linked to another (e.g., a project): Initially, our task RLS policies redundantly included checks that duplicated project-level RLS permission checks.
Later, we realized we could simplify this significantly:
SELECT * FROM project WHERE project.project_id = task.project_id;
While RLS handled row-level permissions efficiently, we faced limitations when dealing with granular permission checks within JSONB column data. Specifically, scenarios required us to grant or deny permissions on individual key-value pairs inside a JSONB column, not just entire rows.
To handle this elegantly, we adopted a creative approach:
This strategy allowed us to achieve a developer-friendly experience akin to RLS, automating complex permission scenarios with minimal manual overhead.
Implementing RLS was a transformative journey for Rocketlane, drastically simplifying permission handling, improving query performance, and significantly enhancing data security. These key learnings and strategies underline how careful planning, incremental rollout, and targeted optimizations can make RLS implementation seamless and highly beneficial.
Stay tuned for more insights into our continuous journey towards enhanced security and better performance!