Roles & permissions
The five firm roles, their full permission matrix, and how RLS enforces tenant isolation.
Kiravy has five firm-level roles plus a separate client-portal role for SME staff on the other side. This page is the full matrix — when you're about to invite someone, come here first.
The five firm roles
In order of decreasing power:
| Role | Display label | Who it's for |
|---|---|---|
partner | Partner | Firm owners. Full admin including billing and firm deletion. |
manager | Manager | Senior staff who run the day-to-day. Same as Partner except can't delete the firm or change Partner rows. |
senior | Senior accountant | Experienced accountants doing client work. Only sees assigned clients. |
junior | Junior accountant | Trainees and junior staff. Same access as Senior today — the distinction is reserved for future approval workflows. |
viewer | View only | Trainees observing, external auditors, partners on holiday who just want to look. Read-only on assigned clients. |
The role enum lives in the database as partner / manager / senior / junior / viewer. UI shows the human-readable labels above. Earlier in some docs we use "Staff" as shorthand for Senior + Junior — that's the same group.
The visibility rule
| Role | Sees which clients |
|---|---|
| Partner | All clients in the firm |
| Manager | All clients in the firm |
| Senior | Only clients they've been assigned to |
| Junior | Only clients they've been assigned to |
| Viewer | Only clients they've been assigned to |
Assignment is a per-client decision — see Set up your firm → Assigning staff and viewers for the how.
Full permission matrix
The cells say what each role can do on the firm side. Client portal users are covered separately at the bottom — they're not firm members.
| Feature | Partner | Manager | Senior | Junior | Viewer |
|---|---|---|---|---|---|
| Clients — create, edit name / SSM / industry | ✓ | ✓ | — | — | — |
| Clients — archive / restore | ✓ | ✓ | — | — | — |
| Clients — delete permanently | ✓ | — | — | — | — |
| Clients — assign staff to client | ✓ | ✓ | — | — | — |
| Transactions — view | ✓ all | ✓ all | ✓ assigned | ✓ assigned | ✓ assigned |
| Transactions — create / edit / soft-delete | ✓ | ✓ | ✓ | ✓ | — |
| Transactions — CSV import | ✓ | ✓ | ✓ | ✓ | — |
| Customers / Suppliers — view | ✓ all | ✓ all | ✓ assigned | ✓ assigned | ✓ assigned |
| Receipts — upload + review | ✓ | ✓ | ✓ | ✓ | — |
| AR / AP — view invoices, aging | ✓ | ✓ | ✓ | ✓ | ✓ |
| AI assistant (client-level) | ✓ | ✓ | ✓ | ✓ | — |
| AI assistant (firm-level) | ✓ | ✓ | — | — | — |
| Engagements — open / close month-end | ✓ | ✓ | — | — | — |
| Engagements — review queue, categorise | ✓ | ✓ | ✓ | ✓ | view only |
| Tax deductibility — run AI check | ✓ | ✓ | — | — | — |
| Tax deductibility — override verdict | ✓ | ✓ | ✓ | ✓ | — |
| Reports — view P&L | ✓ | ✓ | ✓ | ✓ | ✓ |
| Reports — export PDF / CSV | ✓ | ✓ | ✓ | ✓ | — |
| Firm settings — profile, logo | ✓ | ✓ | — | — | — |
| Firm settings — integrations (SQL Account agents) | ✓ | ✓ | — | — | — |
| Firm settings — team (invite / change / remove) | ✓ | ✓ | — | — | — |
| Firm settings — AI usage | ✓ | ✓ | — | — | — |
| Firm settings — billing | ✓ | — | — | — | — |
| Pair SQL Account bridge agents | ✓ | ✓ | — | — | — |
| Delete firm permanently | ✓ | — | — | — | — |
Where "✓ all" appears in view rows, it means the role sees the data across every client. "✓ assigned" means they only see clients they've been explicitly assigned to.
Senior vs Junior — what's actually different today
Today: nothing functionally. Both have the same database-level permissions and pass canEdit checks identically.
The two are distinct in the enum because future features will lean on the split — for example, Junior categorises → Senior approves workflows. Until those land, treat them as interchangeable role names you can use to tag your team for HR / org-chart purposes.
What a Partner is and how you become one
A Partner is set at firm creation — the user who creates the workspace is automatically its Partner. There's no UI flow to change someone to Partner after the fact; the database policy (migration 0013) blocks it specifically.
If you need a second Partner: today, contact support. We'll add a Partner-promotion flow when there's a real reason to (e.g. someone leaves and a Manager needs to step up).
You can have multiple Partners in one firm — the database allows it. Just no self-service UI for the promotion yet.
The Client portal user
A completely separate identity. Lives in the client_portal_users table, not workspace_members. They:
- Sign in at
/portal/login(different from the firm-side/login) - See exactly one client — the one they're invited to
- Can upload receipts and view their own data
- Cannot see the firm dashboard, other clients, or any firm settings
- Cannot see staff names or firm-internal AI conversations
Portal users come in two flavours: Owner and Staff (e.g. the SME owner vs an accounts-receivable clerk on their team). Distinction is mostly cosmetic today; future features (signoff on receipts, billing visibility) will lean on it.
See Client portal for the full surface.
How Kiravy enforces this — RLS
The role checks above aren't just UI bouncers. Every read and write goes through PostgreSQL Row-Level Security policies before the application even sees the data.
Two helper functions do the heavy lifting:
current_user_workspace_role()— returns the calling user's role in the current request's workspace. Drives "is this user a Partner / Manager?" checks.is_assigned_to_client(client_id)— returns true if the user is either a Partner / Manager or explicitly assigned to that specific client. Drives "can this Senior see this client?" checks.
Every tenant table has policies like:
create policy "tenant_isolation" on customers for select
using (is_assigned_to_client(client_id));
The upshot: even a buggy application query, or a forgotten WHERE clause, can't leak data across firms. The database refuses to return rows the user shouldn't see.
This is also how the SQL Account migration agent safely uses a service-role key — its server-side code does the role check explicitly before bypassing RLS for the upsert.
Practical scenarios
Adding a junior accountant
- Firm settings → Team → Invite member, role Junior
- After they accept: open each client they should work on → Settings → Team → Add assignment
- They now see only those clients in their list
Demoting a Manager to Senior
Click their role dropdown in Team. Confirmation modal warns "this is a downgrade." Their data stays attributed to them; they just lose firm-wide access.
Replacing a departing accountant
Don't delete their account — that orphans the audit trail. Instead:
- Deactivate them (button in Team → their row)
- They lose login access immediately
- Past transactions / receipts they created stay attributed to them
- Onboard the replacement separately
- If they ever come back, Reactivate restores access
An external auditor visiting
Invite them as View only, assign to the specific clients they need. They can browse, run reports, even export PDFs — but can't change a single number.
What's coming
- Partner promotion / demotion via UI (today: support only)
- Custom roles for firms that need different splits (post-MVP)
- Per-role approval workflows — what makes Senior vs Junior actually differ
- Audit log UI for Partners to see who did what
What's next
- Set up your firm — invite flow
- Team management — deeper dive on day-to-day team admin
- Workspaces — multi-firm switching