Skip to article
← All articles

SaaS permissions beyond the login screen

How to think through workspace ownership, roles, row-level security, and the less obvious places where an application can expose customer data.

Separated glass workspaces with a user token selecting records within its own compartment

Application Architecture / For small saas teams adding collaboration

The first version of a product often has one account and a few records. Permissions become more interesting when that account invites a teammate, a contractor joins two workspaces, or someone leaves the company.

Now, “is this person signed in?” is only the beginning of the decision. The application also needs to know which organisation owns the record, what the person can do there, and whether that access is still current.

My work on Dysc includes authenticated sessions, multiple collaborators, and role-based access with Supabase. That makes permissions a practical product concern for me. A content workspace is only useful if collaboration is predictable and access stays within the right boundaries.

Start with people, actions, and records

Before writing policies, list the actions the product supports. Reading a record, editing it, inviting a member, exporting data, and changing billing are separate permissions even when they appear on nearby screens.

Then describe the relationship that allows each action. A workspace member might read shared content. An editor might change assigned content. An owner might manage membership. Some records might remain private to their creator.

For a hypothetical content platform, I would begin with a short permission matrix:

  • A viewer can read the workspace's shared content.
  • An editor can create content and edit the records their role allows.
  • An owner can manage members and workspace settings.
  • A signed-in person with no membership has no access to that workspace.

The last case deserves its own place in the plan. It is easy to test three useful roles and forget the person who belongs to a different customer.

Avoid assuming that a role name explains every decision. “Admin” can mean a customer administrator or a trusted internal operator. Those are different responsibilities and should be modelled clearly.

Enforce the decision where data is accessed

Hiding an edit button helps explain the interface. The server must also reject an unauthorised edit request. The same applies to downloads, search results, exports, and background actions.

OWASP's Authorization Cheat Sheet recommends least privilege, denying access by default, and checking permissions on every request. Those principles are useful when reviewing paths that do not pass through the normal screen flow.

For example, a detail page might load a record by its identifier. The application should check access to that record, rather than assume that anyone who knows the identifier is allowed to read it. A difficult-to-guess identifier is useful for other reasons, but it does not express the user's relationship to the record.

I would make the access decision easy to find in the code. If each endpoint has a slightly different interpretation of workspace membership, changes become difficult to review and harder to test.

Put ownership into the data model

A multi-workspace product needs a reliable way to connect records to their workspace. That relationship should survive beyond the screen that created the record.

An example model could include workspaces, memberships, and content records. A membership connects a user to a workspace with a role and status. A content record carries its workspace identifier. An access decision can then consider the authenticated user, an active membership, and the requested operation.

Be careful about which fields come from the browser. The client can ask to create content in a workspace, but the server should verify that the person is allowed to do so. A workspace identifier submitted in a form is a request, not proof of membership.

Membership changes also need a product decision. What happens to assigned content when a person leaves? Can the last owner remove themselves? Does an invitation become active immediately or only after acceptance? Write those rules down before they become scattered conditions.

Use row-level security with a clear scope

Row-level security can enforce row access in the database. Supabase's RLS guide explains how policies work with authenticated requests and why exposed tables need appropriate policies. It also warns that privileged service credentials must not be exposed in the browser.

I would use policies to express the same ownership and membership rules as the application. Then I would check that the relevant reads and writes actually run under the intended database role and user context.

PostgreSQL's row-security documentation describes how policies filter accessible rows and constrain changes. It also explains that owners and roles with bypass privileges can behave differently. A passing test with an administrative connection does not demonstrate how an ordinary user's request behaves.

Keep privileged credentials on the server and make privileged operations narrow. A trusted background job may need broader access, but its inputs and purpose still deserve validation. Moving an operation behind an administrative key should be a deliberate choice.

Follow the record beyond its main page

A permissions review can miss the secondary ways data leaves the application. An export may use a different query. A search index may contain a copy. An uploaded file may have a separate access policy. A background notification may reveal content in its subject line.

For each important type of record, I would trace where it appears and which system decides access in that location. This is especially useful when adding an AI assistant. Retrieval should not make private workspace content available to another customer before the answer is generated.

Caching needs the same attention. If a response depends on membership, its cache behaviour must preserve that boundary. A cached result should not outlive the access rules the product promises without a conscious design for that situation.

Test with more than one workspace

A useful test setup has at least two separate workspaces and accounts with different relationships to them. Use it to check ordinary access and attempts that should fail.

Try reading another workspace's record by identifier, changing the workspace field in a request, editing with a viewer account, and using an invitation before it is accepted. Remove a member and check the relevant paths again. Include exports and files if the feature exposes them.

These tests should use the same kinds of credentials as the real client. Administrative test access can hide the very failures you are trying to find.

Finally, make access understandable to the person using the product. Explain why an action is unavailable when that is appropriate, and give owners a clear way to manage membership. Good permissions support collaboration by making the boundaries predictable.

For a growing SaaS, I would rather agree on a small, explicit permission model early than keep adding exceptions to a broad admin flag. It gives the interface, API, and database one set of decisions to follow as the product grows.

I’m Amminadab Elias, an AI Product Engineer based in Addis Ababa, Ethiopia. Explore my Full-Stack SaaS Development work, or tell me what you are building.