Most teams building an internal tool or client portal treat permissions as a step-two problem: get the app working first, lock it down later. The trouble is that 'later' usually means after someone has already seen a salary field they shouldn't have, or a client has stumbled into another client's invoice. Retrofitting permissions onto a live app is far more painful than designing them in from the start, because by then real users have workflows built around whatever access they already have. If you're building anything that touches HR data, financials, or CRM records with external visibility, treat permission structure as a design decision, not a cleanup task.
Almost every business app can be modeled with three roles, even if you eventually split them further: employees who do the work, managers who oversee and approve it, and external clients who need visibility into their own slice of the data and nothing else. In ViibeStack, each role maps to a set of view, edit, and approve permissions applied at the table, field, and record level -- not just 'can this person open the app.' View-only access is the default you should reach for first. An employee viewing a project dashboard doesn't need edit rights on every field, just the ones tied to their actual tasks. Edit access should be scoped to the records someone owns or is assigned to -- a support rep can update their own tickets in Helpdesk & Support but shouldn't be able to reassign tickets belonging to another queue. Approve access is its own tier, separate from edit, because the person who submits an expense report or a marketing budget request should almost never be the person who signs off on it. Collapsing edit and approve into the same permission is one of the most common mistakes we see -- it defeats the purpose of having an approval step at all.
Page-level permissions -- can this role see this screen -- are the easy part. The harder and more important layer is filtering what shows up inside that screen. A sales manager and a rep might both open the same deals table in CRM, but the manager sees every deal on the team while the rep sees only their own. That's a row-level rule, usually built by filtering records against the logged-in user's ID or team. Field-level rules solve a different problem: hiding specific columns from a role that can otherwise see the record. A client portal might show project status and deliverables but hide your internal cost notes and margin. This is especially relevant if you're running a customer onboarding portal where clients and staff work off the same underlying records but need very different views of them. Get row and field rules right and page-level access almost takes care of itself.
Client-facing roles deserve extra caution because they sit outside your organization's trust boundary. A good pattern is to give clients a dedicated portal view built on the same data model as your internal app, rather than a stripped-down version of the internal screens. That way you're not relying on remembering to hide fields on every screen a client might reach -- the portal only ever queries the fields it's built to show. This is the same logic behind the self-serve quote calculator pattern: external users get a purpose-built surface, not a locked-down copy of the internal tool. Also decide early whether clients can edit anything at all, or only approve/reject items you push to them (a proposal, an invoice, a deliverable). Approve-only access for clients is usually safer and simpler to reason about than giving them edit rights on shared records.
Before you invite real employees or clients into the app, run through this list: - List every role in the app and write down, in plain language, what each one should and shouldn't see -- don't skip this because it feels obvious. - Log in as each role (not just admin) and click through every screen. Permissions bugs are almost always found by actually looking, not by reading a settings page. - Check that edit and approve are separate permissions wherever money, contracts, or HR actions are involved. - Confirm row-level filters are applied to every table a non-admin role can access, not just the main dashboard. - Test field-level visibility on any table shared between internal and external roles. - Verify that removing someone from a role actually revokes access immediately, including anything cached or embedded in reports. - Check integrations and automations too -- a workflow that emails a report or posts to Slack can leak data a role otherwise can't see directly. Our Integrations page covers where these connections typically live. - Document who owns permission changes going forward, so access doesn't drift as the team grows. This kind of audit takes an afternoon and it's far cheaper than explaining after the fact why a client saw another client's data. For more on what a security-first build process looks like end to end, see our Security page and Trust Center.
Permission structures that work for ten users rarely survive fifty unchanged. New roles get added, contractors come and go, and clients sometimes need temporary access to a project they wouldn't normally see. Build the habit of reviewing roles on a schedule -- quarterly is reasonable for most teams -- rather than only when something breaks. If you're doing a broader post-launch review, it's worth pairing this with a 60-day retrospective on whether the app is actually being used the way you designed it, permissions included.