Skip to main content

Roles & Permissions

This page documents the role, permission, membership, sharing, billing, and AI authorization rules verified against RDS and reviewed from the backend and AI service code.

Primary code paths reviewed:

  • aiqlick-backend/prisma/schema.prisma
  • aiqlick-backend/prisma/seed-essential.ts
  • aiqlick-backend/src/common/constants/roles.ts
  • aiqlick-backend/src/user-management/authentication/guards/*.ts
  • aiqlick-backend/src/user-management/authorization/**/*.ts
  • aiqlick-backend/src/company-management/company/**/*
  • aiqlick-backend/src/ai-gateway/**/*
  • background-tasks/app/graphql/**/*
  • background-tasks/app/services/billing/**/*

RDS Verification

The canonical authorization state is the current codebase plus production RDS. The RDS checks below were read-only and queried only role, permission, and join tables.

EnvironmentDatabasePostgreSQLVerifiedRole rowsPermission rowsRole-permission links
Production RDSaiqlick_db16.62026-05-29 20:24 UTC839155
Development RDSaiqlick_dev16.102026-05-29 20:24 UTC839155
important

Production and development RDS now have identical role-permission joins by Role.name and Permission.name. Production remains the canonical environment for documenting the matrix; development was synchronized to it on 2026-05-29.

Authorization Model

The platform uses several authorization layers. They are related, but they are not the same thing.

LayerSourceMeaning
AuthenticationJWT sub loaded by JwtAuthGuardIdentifies the current user and attaches the full user record to req.user.
Platform super adminUser.isSuperAdminPlatform-level override used by SuperAdminGuard and selected services. This is separate from a company role named Super Admin.
Company membershipUserCompanyRoleLinks a user, company, and role. Most company authorization is based on this table.
Global support roleUserRole with role name supportUsed by support tooling through SupportAgentGuard; it is not a company membership role.
Current companyUser.selectedCompanyId or explicit GraphQL variablesDetermines which company-scoped role and plan checks apply.
Billing ownerCompany.billingOwnerIdDetermines which subscription controls a company when resolving plan limits and AI billing.
Share permissionsShare/collaborator tablesGrants direct object access such as view, edit, or manage, even when the user is not a member of the owning company.
AI billing contextbackend AI gateway and background-tasks billing gateDetermines whether an AI operation is billed to a company, a user, or rejected for missing context.

RDS Roles

Production and development RDS both contain these eight roles. Seven are company roles; support is a global role used by support tooling.

RoleScopeCode tierProduction assignmentsProduction permission linksDevelopment permission links
Super AdminCompanyAdmin0 company, 0 global3939
Company AdminCompanyAdmin15 company, 0 global3535
Internal SalesCompanyMember10 company, 0 global2727
External SalesCompanyViewer1 company, 0 global11
EconomyCompanyViewer0 company, 0 global1313
Recruiter/HRCompanyMember0 company, 0 global2727
Auditor/ViewerCompanyViewer0 company, 0 global1313
supportGlobalSupport0 company, 7 global00

Assignment counts are a point-in-time RDS snapshot. Role names, scopes, and code tiers are the authorization contract.

The role tier helpers live in src/common/constants/roles.ts:

  • ADMIN_ROLES: Super Admin, Company Admin
  • MEMBER_ROLES: Internal Sales, Recruiter/HR
  • VIEWER_ROLES: Auditor/Viewer, External Sales, Economy
  • MUTATION_ALLOWED_ROLES: admin + member roles
  • INVITER_ROLES: admin roles + Internal Sales
important

User.isSuperAdmin is a platform flag. Role.name === "Super Admin" is a company role. Some guards check the platform flag, some check the company role, and they are not interchangeable.

RDS Permission Matrix

Production and development RDS contain 39 permissions across 13 resources. Permission names use resource_READ, resource_WRITE, and resource_DELETE. The matrix below is the _RolePermissions state and matches seed-essential.ts.

Legend:

  • R: read
  • W: write
  • D: delete
  • -: no seeded permission
ResourceSuper AdminCompany AdminInternal SalesExternal SalesEconomyRecruiter/HRAuditor/Viewersupport
CV DatabaseR/W/DR/W/DR/W/D-RR/W/DR-
Contact ListR/W/DR/W/DR/W/D-RR/W/DR-
EconomyR/W/DR/W/D--R/W/D-R-
Grow CompanyR/W/DR/WR-RRR-
Network ExpansionR/W/DR/WR/W/D-RR/W/DR-
OpportunitiesR/W/DR/W/DR/W/DRRR/W/DR-
ProjectsR/W/DR/W/DR/W--R/WR-
Share CV ListR/W/DR/W/DR/W/D--R/W/DR-
Share Job ListR/W/DR/W/DR/W/D--R/W/DR-
StatisticsR/W/DR/WR-RRR-
SubscriptionsR/W/DR/WR-R/WRR-
Task BoardR/W/DR/W/DR/W/D-RR/W/DR-
Tracking SystemR/W/DR/W/DR-RRR-
Runtime permission status

RDS has role-permission links, but application resolvers do not currently use @HasPermission(...). Runtime authorization is mostly enforced through JwtAuthGuard, role-name checks, tier helpers, domain authorization services, sharing services, plan guards, and AI billing gates.

The eight roles are no longer the whole story

The eight roles and 39 permissions above are the platform-seeded global baseline. Company Admins can now create their own custom company roles with arbitrary tiers and permission sets through the IAM Dashboard. The global matrix is still the read-only foundation; custom roles extend it per company. See Custom Company Roles below.

Custom Company Roles

Company Admins are no longer limited to assigning the seven seeded company roles. Through the IAM Dashboard (surfaced at /userprofile/iam), an admin can author custom roles scoped to their company.

CapabilityMutationApplies to
Create a new company roleCREATE_COMPANY_ROLENew custom role
Clone an existing role into an editable copyCLONE_ROLE_AS_CUSTOM (cloneRoleAsCustom)Any role — global or custom
Edit a custom roleUPDATE_COMPANY_ROLECustom only
Delete a custom roleDELETE_COMPANY_ROLECustom only

Global roles (the eight above) are read-only in the UI — Edit/Delete are disabled with the hint to clone them into an editable copy first. A custom role carries a backend tier field (Admin / Member / Viewer) chosen in the role editor; the frontend's getRoleTier(roleName, tier?) reads that field as a fallback for names not present in the ADMIN_ROLES / MEMBER_ROLES / VIEWER_ROLES arrays. So a custom role's write/manage gating follows the tier the admin selected.

Assignable-role whitelist

Not every role is assignable by a company admin — platform-only roles must never be handed out from the company UI.

  • GET_COMPANY_ASSIGNABLE_ROLES (companyAssignableRoles) filters platform-only roles (super admin, support) server-side, returning only the roles a company admin may assign.
  • The frontend intersects role IDs against that result and, while the query is still loading, defensively hides super admin / support so a privileged role can never appear in an assignable dropdown.

Role reassignment and audit

  • Reassignment — Changing a member's role in the IAM Members tab issues UPDATE_USER_COMPANY_ROLE (updateUserCompanyRole). The billing owner's row is locked; ownership moves via Company Ownership Transfer, not a role edit.
  • Audit trailROLE_CHANGE_AUDIT (roleChangeAudit) records each reassignment (actor, target, old role → new role, reason, timestamp). The IAM dashboard renders the last 8 entries as a "Recent role changes" feed.

Per-member navigation gating (Phase 1)

Each jobSeekerNavItems / teamNavItems entry (lib/config/navigation.ts) carries a requiredPermission key (e.g. jobseeker.dashboard.view, pipeline.view, team.view). The Sidebar hides an item when the member lacks the effective permission. This defaults to show-all until the backend ships effectivePermissions on whoAmI; the keys are wired now so per-member visibility becomes a data change later. See RBAC Frontend → requiredPermission nav gating.

Backend Guards

GuardDecorator or inputRule
JwtAuthGuardAuthorization: Bearer <jwt>Verifies the JWT with JWT_SECRET, loads the user with roles, and attaches it to req.user.
SseAuthGuard?token= or bearer headerSame basic JWT authentication for SSE. Query tokens work but can leak in infrastructure logs.
RoleGuard@HasRole(...)Allows if the user has at least one required role on the resolved company. If no @HasRole metadata exists, the guard is a no-op.
PermissionGuard@HasPermission(...)Allows if any role on the resolved company has one of the required permission names. If no metadata exists, the guard is a no-op.
SuperAdminGuard@RequireSuperAdmin()Requires req.user.isSuperAdmin === true.
CompanyWriteGuardcompany context from GraphQL variables or selected companyBlocks viewer-tier company roles from state-changing company mutations. Platform super admins bypass this guard.
SupportAgentGuardsupport role or platform super adminAllows platform super admins or users assigned the global support role.
PlanLimitGuard@CheckPlanLimit(feature)Checks the company plan and usage for a specific feature limit. If no feature or no selected company exists, it does not block.

Company Context Rules

RoleGuard resolves the company from:

  1. variables.companyId
  2. variables.id
  3. user.selectedCompanyId

CompanyWriteGuard resolves the company from:

  1. variables.companyId
  2. variables.input.companyId
  3. user.selectedCompanyId

CompanyWriteGuard deliberately does not use a bare id variable because many mutations use id for a resource ID, not a company ID.

CompanyWriteGuard Viewer Blocker

CompanyWriteGuard blocks company-scoped write mutations when the caller's role on the resolved company is in VIEWER_ROLES:

  • External Sales
  • Economy
  • Auditor/Viewer

Platform super admins bypass this guard through User.isSuperAdmin === true.

Canonical error messages:

ConditionMessage
Unauthenticated callerUser is not authenticated
Missing company contextCompany context is required for this action.
No role on resolved companyYou do not have a role on this company.
Viewer-tier roleYour role is read-only on this company. Ask a Company Admin to perform this action.

Company And Team Rules

Company Creation

An authenticated user can create a company, subject to the maxCompanies plan limit. The service sets Company.billingOwnerId to the creator and creates company billing configuration.

The reviewed CompanyService.create(...) path does not create a UserCompanyRole for the creator. If a flow relies on the creator immediately having a company role, confirm the onboarding path creates that membership elsewhere.

Updating And Deleting Companies

Company update and delete mutations use RoleGuard with:

  • Company Admin
  • Super Admin

The delete service also checks that the caller has an admin-tier company role through isAdminRole(...).

Inviting Members

The new-invitation mutation requires:

  • authenticated user
  • company role in Company Admin, Super Admin, or Internal Sales
  • maxUsersPerCompany plan capacity
  • a target role that exists
  • no duplicate pending invitation for the same company and email
  • the invitee is not already a company member

The service repeats the inviter role check using INVITER_ROLES, so the resolver guard is not the only protection.

Resending Invitations

Resend requires the caller to have an inviter role on the invitation's company:

  • Super Admin
  • Company Admin
  • Internal Sales

Resend does not consume another maxUsersPerCompany slot. It refreshes the token, sets the invitation back to PENDING, and sends a new email. Accepted invitations cannot be resent.

Accepting Invitations

Invitation acceptance is token-based and public. The service:

  1. finds the invitation by token
  2. rejects already accepted invitations
  3. finds or creates the user
  4. creates the UserCompanyRole
  5. sets selectedCompanyId if the user does not have one
  6. marks the invitation ACCEPTED

If the accepted role is Company Admin and the company has no billing owner, the accepting user becomes Company.billingOwnerId.

Removing Members

User-facing member removal requires:

  • authenticated caller
  • the caller is not removing themself
  • platform super admin, or admin-tier role on the target member's company
  • non-super-admin callers cannot remove the billingOwnerId member role

If the removed membership was linked to an invitation, the invitation is set to CANCELLED in the same transaction.

Transferring Ownership

Ownership transfer is allowed only for:

  • the current Company.billingOwnerId
  • a platform super admin

The service promotes or creates the new owner's membership as Company Admin, demotes other company admins to Internal Sales, and updates Company.billingOwnerId.

Resource Sharing Rules

The platform uses share and collaborator tables for object-level authorization in addition to company roles.

Common permission enums:

EnumValuesTypical use
AccessPermissionVIEW, EDIT, MANAGEShare records such as talent pool, pipeline, and user/company shares.
PermissionLevelREAD_ONLY, EDIT, ADMINCollaborators and some talent/CV sharing paths.

Talent And CV Access

A user can access talent data when at least one of these is true:

  • they own the talent
  • they are a member of the owning talent-pool company
  • the talent is shared with the user or one of their companies
  • they have collaborator access
  • the talent is included in a shared talent list they can access

Talent write access is narrower:

  • talent owner
  • company role Company Admin, Super Admin, or Recruiter/HR on the owning company
  • explicit talent share above read-only

CV access follows associated talent access. Direct CV write access is blocked for application snapshots and restricted when the active CV owner is another user.

Talent Pool Access

Talent pool access can come from:

  • pool creator
  • company membership for company pools
  • public pool visibility
  • user, company, or collaborator share

Talent pool write access can come from:

  • creator
  • company role Company Admin, Super Admin, or Recruiter/HR
  • user/company/collaborator share with EDIT or MANAGE

Public pools are treated as readable by broad list filters, but non-creators do not get write access just because a pool is public.

Pipeline Access

Pipeline access can come from:

  • membership in the pipeline company
  • collaborator access through shared talents
  • pipeline company share
  • pipeline user share

Pipeline write access can come from:

  • pipeline owner
  • company role Company Admin, Super Admin, or Recruiter/HR
  • pipeline user or company share with EDIT or MANAGE

Public And External CV Views

Public or external CV access masks personal data. The helper removes or masks fields including email, phone, date of birth, gender, street address, city, state, zip code, profile image URL, and salary preference fields.

Plan And Billing Rules

Plan limits are company-scoped unless the operation is explicitly personal. The backend resolves company subscription control through the company billing owner.

Company subscription resolution uses:

  1. Company.billingOwnerId
  2. fallback to the earliest Company Admin membership when no billing owner exists

Active subscription statuses are:

  • ACTIVE
  • TRIALING

Postpaid enterprise plans can be blocked when the subscription status is UNPAID.

Backend Plan Limits

PlanLimitGuard and PlanLimitsService enforce feature limits such as:

  • maxJobs
  • maxTalentPools
  • maxContacts
  • maxPipelines
  • maxInterviewsPerMonth
  • maxMeetingsPerMonth
  • maxCvCollections
  • maxSharedTalentLists
  • maxCollaborationInvites
  • maxConnectionRequestsPerMonth
  • maxUsersPerCompany
  • maxNewsPerMonth
  • maxContactNotes
  • maxCvParsesPerMonth
  • maxAgentMessagesPerMonth
  • maxTextRewritesPerMonth

Monthly limits are counted from the start of the current UTC month. A limit of -1 means unlimited. A limit of 0 disables the feature.

Postpaid plans are generally unlimited, but CompanyBillingConfig.allocated* overrides can cap individual features.

Credit Balance Routing

Credit balance routing depends on the user's view mode:

  • EMPLOYER + selectedCompanyId: use the company credit balance
  • otherwise: use the user's personal credit balance

AI Authorization And Billing Rules

The backend AI gateway is the first gate for AI operations. The background-tasks service is the final billing and execution gate.

Backend AI Gateway

The following GraphQL entry points require JwtAuthGuard:

  • subscribeAi
  • queryAi
  • mutateAi

Each operation must be present in the backend operation registry. Unknown operations are rejected before reaching background-tasks.

The gateway resolves an actor company ID with this order:

  1. if user.viewMode === "JOB_SEEKER", no company context is used
  2. user.selectedCompanyId
  3. active company subscription owned by the user
  4. company where the user is billingOwnerId
  5. null

For an AI operation, AiPolicyService requires an active subscription when either of these registry fields is set:

  • stampBillingContext
  • planLimitKey

If a planLimitKey exists and a company context exists, the gateway also calls PlanLimitsService.checkLimit(...). If no company context exists, subscription and plan-limit checks are skipped at the gateway and the background-tasks billing gate becomes the final authority.

Current Subscription Operations

OperationStamps billing contextBackend plan limitGateway subscription precheck
extractCvYesmaxCvParsesPerMonthYes
parseJobYes-Yes
faceExtractionYes-Yes
agentChatNomaxAgentMessagesPerMonthYes
textRewriteYesmaxTextRewritesPerMonthYes
generateMeetingInsightNo-No
addCandidateNo-No
userJobMatchingNo-No
bulkCvImportNo-No
documentProcessingNo-No

Current HTTP AI operations are registry-allowlisted but do not stamp billing context and do not declare backend plan limits.

Background-Tasks Authentication

The AI service accepts internal service JWTs by default:

  • signed with INTERNAL_SERVICE_SECRET or INTERNAL_SERVICE_SECRET_PREVIOUS
  • audience aiqlick-background-tasks
  • issuer aiqlick-backend
  • requires actorUserId
  • normalizes actorCompanyId to companyId

Legacy user JWTs are disabled by default and only accepted when ALLOW_USER_JWT=true.

The GraphQL context extracts tokens lazily from WebSocket connection params, HTTP headers, or ?token=, but resolvers or extensions must call ensure_auth() to enforce authentication.

Background-Tasks Billing Gate

BillingGate requires billing context when billing is enabled:

  • company context for employer/company operations
  • user context for personal operations

If both company ID and user ID are missing and billing is enabled, the operation is rejected. If billing is disabled, the gate can skip credit deduction, but it still enforces unpaid postpaid blocking.

Billing mode rules:

  • active company subscription: company mode
  • active personal subscription: user mode
  • postpaid plan: log usage, do not deduct credits
  • prepaid plan: pre-check and consume credits
  • unpaid postpaid: block

Background-tasks plan-limit mapping currently includes:

AI operation typePlan field
CV_EXTRACTIONmaxCvParsesPerMonth
JOB_PARSINGmaxJobs
MEETING_INSIGHTmaxMeetingsPerMonth
AGENT_CHATmaxAgentMessagesPerMonth
TEXT_REWRITEmaxTextRewritesPerMonth

Unknown AI operation types have no background-tasks plan limit configured.

Review Findings And Gaps

The following findings come from code review and should be treated as implementation notes for future hardening.

AreaFinding
Permission guard usagePermissionGuard exists, but application resolvers are not currently using @HasPermission(...). Seeded permissions are therefore not the primary runtime enforcement mechanism.
Legacy role/permission CRUDThe older role, permission, and role-permission CRUD resolvers appear to expose mutations without the same JwtAuthGuard + SuperAdminGuard protection used by the admin role-management API. Prefer the guarded admin role-management API or lock down the legacy resolvers.
RoleGuard no-op behaviorRoleGuard does nothing unless @HasRole(...) metadata is present. Resolver classes that only add @UseGuards(RoleGuard) do not get a role check from that alone.
Company creator membershipCompany creation sets the creator as billing owner, but the reviewed service path does not create a company membership role. Confirm onboarding creates the membership before relying on role-based access for new company creators.
Billing-owner removal protectionUser-facing member removal protects the billing-owner membership. The super-admin role revocation path should be checked before using it to remove a billing owner.
AI gateway versus background billingGateway prechecks only apply to operations with stampBillingContext or planLimitKey, and only when a company context is resolved. Background-tasks remains the final billing gate.

Implementation Rules Of Thumb

For new company-scoped write mutations:

  1. require JwtAuthGuard
  2. use CompanyWriteGuard for viewer-tier blocking
  3. add PlanLimitGuard when the mutation consumes a plan-limited feature
  4. use a domain authorization service for object-specific ownership/share checks
  5. pass an explicit companyId or input.companyId instead of relying on a bare id

For new admin-only platform operations:

  1. require JwtAuthGuard
  2. require SuperAdminGuard
  3. add @RequireSuperAdmin()

For new AI operations:

  1. add the operation to the backend registry
  2. set stampBillingContext when the operation should be billed to the actor company or user
  3. set planLimitKey when the operation consumes a plan-limited feature
  4. ensure the background-tasks resolver calls ensure_auth()
  5. call BillingGate.pre_check(...) and BillingGate.check_and_consume(...) around billable execution
  6. log failures through the billing gate when execution fails after pre-check