Main conceptsUsers, Roles & Permissions
Main concepts

Users, Roles & Permissions in Jules

How Jules manages user identities, organizational structure, access control, and authorization for recyclable materials trading teams.

Product documentation — How Jules models user identities, assigns roles, structures organizations, and enforces fine-grained access control across every action in the platform.


Table of Contents

  1. Overview

  2. User Model

  3. Authentication

  4. Roles

  5. Organizational Structure — Departments & Divisions

  6. Manager Hierarchy

  7. User ACL Scoping — Companies & Sites

  8. Geographic Scoping — Subregions

  9. Billing Entities & Legal Entities

  10. Billing Entity ↔ User Assignment

  11. External Users & Portal Access

  12. Licenses

  13. Permit.io ABAC — Fine-Grained Authorization

  14. Relationships with Other Modules

  15. Key Business Rules

  16. Glossary


Overview

Jules uses a layered access model. At the innermost layer, a User has an identity and a set of Roles that determine what pages and actions they can access. Surrounding that, a set of ACL scoping tables (companies, sites, subregions) narrows which records a user can see. At the outermost layer, Permit.io ABAC enforces fine-grained permission checks on every GraphQL query and mutation.

Everything in Jules operates inside a tenant (called an organizationId). The public PostgreSQL schema holds user identity records; every other table lives in a per-tenant schema and is always scoped with withSchema(organizationId).


User Model

The User type is the central identity object in Jules. User records live in the public.users table, shared across all tenants.

Core fields

FieldTypeDescription
idIDInternal auto-increment primary key
uidStringExternal identifier (used in JWT, integrations)
emailStringPrimary login credential and Permit.io identity key
erpEmailStringAlternative email used for ERP synchronization
firstName / lastNameStringDisplay name
languageStringUI language preference (e.g., en, fr)
timezoneStringUser's local timezone for notifications
organizationIdStringThe tenant this user belongs to
roles[String]Array of role keys assigned to the user
phoneNumberStringOptional contact number
signatureUrlStringURL of the user's scanned signature image (used on PO/SO PDFs)
emailSignatureStringHTML email signature block
shouldReceiveKnockNotificationsBooleanOpt-in for in-app push notifications
notificationPropertiesJSONGranular notification preferences per trigger type
isExternalBooleanMarks a portal user (supplier/customer-facing); see External Users
companyCompanyThe company linked to this user (relevant for external users)

System-level fields (not exposed via GraphQL)

These fields exist in the database and affect runtime behavior:

FieldDescription
passwordBcrypt-hashed password (legacy login path)
isBlockedPrevents login when true — used by admins to suspend accounts
enablePermitioPer-user feature flag that activates Permit.io ABAC checks
viewGroupOptional grouping key included in the JWT, used for view-level access segmentation
dailyEmailScheduleTime-of-day for scheduled digest emails
shouldDisconnectOnceFromWeb / shouldDisconnectOnceFromMobileAdmin-triggered forced logout flags
apiUrlOverride API endpoint returned on login (used for multi-region routing)
erpIdReference to the matching record in an external ERP system

Authentication

Jules supports three login methods, all converging on a JWT issued by the API.

Login flow

  1. The client calls login (email/password) or loginWithToken (Magic Link or Google)

  2. The API locates the user record — it checks both internal users and external (portal) users

  3. If isBlocked = true, the login is rejected immediately

  4. The user's roles are loaded from the per-tenant roles table

  5. A JWT is signed containing: id, email, organizationId, roles, uid, language, viewGroup, enablePermitio, isExternal, and (for portal users) companyId and entity

  6. The response also includes defaultPage — the landing page the frontend should route to based on the user's roles — and shouldUseAwsServer

Default page by role

RoleDefault landing page
FIELD_MANAGERWarehouse Inbounds
SHIPMENT_TRACKERShipment Tracker
All othersConfigured default page for the organization (from Config)

Session management

  • JWTs are stateless — no server-side session is stored

  • Forced logout is implemented via shouldDisconnectOnceFromWeb / shouldDisconnectOnceFromMobile flags in users_administration, combined with a Redis key (shouldDisconnect:{userId})

  • On successful login, these flags are cleared and the Redis key is deleted


Roles

Every user has one or more roles stored in the per-tenant roles table. Roles are additive — a user can hold multiple roles simultaneously.

Available roles

RoleDescription
ADMINFull administrative access to the organization's configuration, user management, and all data
MANAGERManages a team of users (managees); sees only data scoped to their managees
BUYERCreates and manages purchase operations
SELLERCreates and manages sale operations
TRADER_BUYPurchase-focused trader variant
ALLOCATORCreates and manages allocations between buy and sell operations
LOGISTICIANManages freight bookings, containers, and shipments
LOGISTICS_MANAGERExtended logistician with managerial oversight
ACCOUNTANTAccess to invoices, bills, and financial reports
VALIDATORAuthorized to approve or reject operations in the approval workflow
VIEWERRead-only access across the platform
FIELD_MANAGERManages warehouse inbound operations on-site; defaults to the Warehouse Inbounds page
SHIPMENT_TRACKERFocused on tracking shipments; defaults to the Shipment Tracker page
WM_BUYWarehouse management buy variant

How roles are stored

Roles are stored in a per-tenant junction table {organizationId}.roles with a composite of (userId, role). The getByUserId method aggregates all roles for a user into an array:

SELECT ARRAY_AGG(role) as roles FROM {organizationId}.roles WHERE userId = {id}

This array is embedded in the JWT and re-evaluated on every authenticated request.

Role-based page access

The PagesEnum in the GraphQL schema enumerates all navigable pages in Jules (e.g., PURCHASES, SALES, INVOICES, LOGISTICS_EXPLORER, WAREHOUSE_INBOUNDS). The Permit.io configuration maps roles to allowed pages and actions.


Organizational Structure — Departments & Divisions

Jules provides two lightweight organizational classification dimensions for users and operations: Departments and Divisions.

Department

A Department is a named grouping within the organization (e.g., "Trading", "Logistics", "Finance"). Departments are simple name-value pairs used to categorize users and associate them with operations.

FieldTypeDescription
idIDUnique identifier
nameStringDisplay name of the department

Division

A Division is a higher-level organizational unit, typically representing a business line or geographic division (e.g., "Ferrous", "Non-Ferrous", "APAC").

FieldTypeDescription
idIDUnique identifier
valueStringDisplay name of the division

Both entities are read via the @permit(resource: "DEPARTMENTS", action: "VIEW") and @permit(resource: "DIVISIONS", action: "VIEW") directives respectively, meaning only users with the appropriate Permit.io permission can list them.


Manager Hierarchy

Jules implements a manager-to-managee relationship that drives both organizational visibility and data access scoping.

How it works

The managers_to_managees table stores directed pairs of (managerId, manageeId) — both referencing public.users. One manager can have many managees.

A user with no managees is treated as having global visibility — they can see all users and all data.

A user with at least one managee has their data access restricted to records where the assigneeId or creatorId of an operation matches one of their managees. This is enforced through a PostgreSQL Row-Level Security (RLS) policy function CHECK_TABLE_USERS_POLICY and CHECK_TABLE_OPERATIONS_POLICY.

RLS policy logic (simplified)

IF user has no managees → full access
IF user has managees:
  - Can only see users who are in their managee list
  - Can only see operations assigned to or created by their managees
  - Combined with company ACL check (see below)

The system explicitly excludes the edge case where a user is listed as their own manager (whereNot({ managerId: manageeId })).


User ACL Scoping — Companies & Sites

Beyond roles and manager hierarchy, Jules provides two ACL scoping tables that restrict which counterparties and physical locations a user can interact with.

UsersToCompanies

The users_to_companies table assigns specific trading companies to a user. When a user has entries in this table, they can only see operations, contracts, and related records associated with those companies.

ColumnDescription
userIdReference to public.users.id
companyIdReference to {org}.companies.id
societyOptional sub-classification of the assignment

Empty = global: If a user has no rows in users_to_companies, they have access to all companies within their organization. This mirrors the manager-to-managee pattern — absence of restriction means full access.

UsersToSites

The users_to_sites table restricts a user to specific physical sites (warehouses, collection points, etc.).

ColumnDescription
userIdReference to public.users.id
siteIdReference to {org}.sites.id

As with companies: no rows = access to all sites within the organization.

Combined ACL policy

Both restrictions are evaluated together in the CHECK_TABLE_OPERATIONS_POLICY PostgreSQL function:

RETURN (manager_to_managee_permission AND company_permission)

This means a user must satisfy both the manager hierarchy check and the company scope check to access a given operation record.


Geographic Scoping — Subregions

The users_to_subregions table assigns users to one or more geographic subregions (e.g., "West Africa", "Southeast Asia"). This is used for notification routing and intelligent user suggestions — for example, when Jules needs to suggest which trader to assign to an operation based on its origin subregion.

ColumnDescription
userIdReference to public.users.id
subregionReference to {org}.subregions.value

Subregions combine with other user attributes (see User Attributes below) to enable the smart notification system.

User Attributes

The user_attributes table stores additional routing dimensions for a user. These are used exclusively for notification filtering and user-matching queries — not for RLS access control.

AttributeDescription
qualityIdSpecific material quality the user specializes in
qualityFamilyBroader material family (e.g., "Ferrous", "Plastics")
countryOfOriginCountry from which the user handles material sourcing
countryOfDestinationCountry the user handles for deliveries
regionOfOriginBroader geographic region for origin
subregionOfOriginNarrower subregion for origin
shipmentModePreferred or specialized shipment mode (CONTAINER, BULK_CARGO, TRUCK_RAIL_BARGE)

LegalEntity interface

LegalEntity is a GraphQL interface implemented by all legal party types in Jules. It represents any registered legal entity — whether it is your own company, a trading counterparty, a shipping line, or a customs agent.

LegalEntityTypeEnum

TypeDescription
BILLING_ENTITYYour organization's own legal entity (for issuing/receiving invoices)
COMPANYA trading counterparty (supplier or customer)
AGENTA trade agent or broker
LOGISTIC_FORWARDERA freight forwarding company
SHIPPING_LINEA maritime carrier
SHIP_OWNEROwner of a vessel
CUSTOMS_AGENCYA customs clearance service provider
PRE_CARRIAGE_LINEA local transport provider for pre-carriage legs
INSPECTORA third-party quality/quantity inspection company
BROKERA commodities broker
FINANCIERA bank or financial institution

BillingEntity

A Billing Entity is the legal entity that your organization uses to issue or receive commercial documents (POs, SOs, invoices). In a multi-entity organization, you may have several billing entities — for example, one per country of incorporation.

BillingEntity fields

CategoryKey fields
Identityvalue (display name), legalName, legalForm, identificationCode, registrationCode, taxCode, taxRate
Addressaddress, city, country, region, state, zipCode, isEU
Contactemail, phoneNumber, fax, logisticsEmail, accountingEmail, contact, contacts
BankingbankAccounts (structured), taxCode, creditCoverage, creditCoverageProvider
DocumentslogoUrl (used in PDF headers), signatureUrl (used on PO/SO documents)
ERPerpId (reference to external ERP)
Other codeslicenseNumber, industryCode, locationIdentificationCode, otherCode1/2/3

Soft deletion

Billing entities are never hard-deleted. The delete operation sets isDeleted = true and appends (id) (deleted) to the name, preserving referential integrity for historical documents.


Billing Entity ↔ User Assignment

The billing_entities_to_users junction table links users to billing entities. This relationship has two purposes:

  1. Signatories — users who are authorized to sign documents issued by a billing entity. The filteredSignatories query returns all users linked to a specific billing entity, and this list populates the signatory selector on PO/SO documents.

  2. Default user — one user can be marked as isDefault = true for a billing entity, making them the default contact for that entity's documents.

ColumnDescription
billingEntityReferences {org}.billing_entities.value
userIdReferences public.users.id
isDefaultBoolean — marks the default signatory for this billing entity

External Users & Portal Access

Jules supports external users — contacts at supplier or customer companies who are granted limited access to a portal view of Jules. External users are identified by isExternal = true on their user record.

How external users work

  • External users are linked to a specific company via the external_users_to_companies table (columns: userId, organizationId, companyId)

  • Their JWT includes companyId and entity (an enum indicating whether they represent a supplier, customer, or other portal entity)

  • The @isAuthenticated({ isExternalAllowed: true }) decorator on model methods explicitly permits external users to call those methods

  • The @portalPermit GraphQL directive further restricts which fields are visible to external users (e.g., prices, margins, and internal costs can be hidden per field based on Redis-cached portal configuration)

  • External users queried via externalUsers are returned with their companyId enriched from the join

Portal permission configuration

A per-organization portal:permit:{organizationId} Redis key stores a JSON map of field-level visibility rules in the form "TypeName.fieldName": true/false. This allows operations teams to configure exactly which fields of which types are exposed to portal users without a code deployment.


Licenses

A License in Jules represents a regulatory permit associated with the import or export of specific materials. Licenses are used at the site level to authorize the handling of particular material qualities.

FieldDescription
idUnique identifier
nameLicense reference number or name
typeCUSTOMER (import permit) or SUPPLIER (export permit)
qualityThe specific material quality this license covers
countryThe country in which the license is valid
quotaFrequencyHow often the license quota resets
commentFree-text notes

Licenses can be filtered by isNotUsedBySiteId to find licenses not yet assigned to a given site, supporting the license assignment workflow.


Permit.io ABAC — Fine-Grained Authorization

Jules uses Permit.io (Attribute-Based Access Control) as a centralized policy decision point layered on top of role-based logic.

How it works

The @permit directive

Every sensitive GraphQL query and mutation is annotated with @permit(resource: "RESOURCE_NAME", action: "ACTION"). For example:

DirectiveMeaning
@permit(resource: "USERS", action: "VIEW")Must have VIEW permission on the USERS resource
@permit(resource: "BILLING_ENTITIES", action: "VIEW")Must have VIEW on BILLING_ENTITIES
@permit(resource: "LICENSES", action: "CREATE")Must have CREATE on LICENSES
@permit(resource: "DEPARTMENTS", action: "VIEW")Must have VIEW on DEPARTMENTS

Variant permissions

Some directives use variant: true with either inputPath or returnPath to check permissions against a specific instance rather than the resource type. For example, a user might have CREATE permission on operations in general, but only VIEW permission on operations belonging to a specific billing entity. The path parameters allow the directive to pass a dynamic resource key (e.g., "OPERATIONS-{billingEntityId}") to Permit.io.

Per-user activation

The enablePermitio flag on the user record is a migration escape hatch — it allows Permit.io checks to be rolled out incrementally, user by user, without forcing an all-or-nothing cutover. Users without this flag bypass Permit.io and rely solely on the @isAuthenticated decorator.

Resource synchronization

The syncPermitResources utility automatically keeps the Permit.io resource/action catalog in sync with the local GraphQL schema by diffing local resources against the remote Permit.io API and creating or updating as needed.


Relationships with Other Modules

Users are referenced throughout Jules. The diagram below shows key cross-module relationships:

Related moduleRelationship
OperationsUsers are assigned as trader, admin, account rep, signatory, and watchers on every operation
ContractsUsers are assigned as the responsible trader
GoalsUsers are listed as contributors/owners of commercial goals
TasksUsers are assigned as task owners and can be added as watchers
OffersUsers own and manage offer records
Billing EntitiesUsers are assigned as authorized signatories
NotificationsUsers receive in-app and email notifications based on their notificationProperties and user_attributes
BudgetsUsers are associated with budget planning cycles
ApprovalsUsers with the VALIDATOR role act as approvers in the approval workflow

Key Business Rules

1. Single-tenant isolation

All per-tenant data (roles, ACLs, billing entities, departments, divisions) lives in a PostgreSQL schema named after the organizationId. User identity is global (public.users), but authorization context is always tenant-scoped.

2. Roles are additive

A user can hold multiple roles simultaneously. Permissions from all roles are unioned — there is no role priority or conflict resolution. Assign the minimum set of roles needed.

3. ACL scoping is opt-in by restriction

For both users_to_companies and users_to_sites: having no rows grants access to all companies/sites. Access is only restricted when you explicitly add scope entries. This means new users automatically have broad access until an admin narrows it.

4. Manager hierarchy drives data visibility

Managers see only records assigned to or created by their direct managees. If you need a user to have global visibility, ensure they have no entries in managers_to_managees as a manager. The hierarchy is flat (one level) — there is no recursive manager lookup.

5. Blocking a user

Setting isBlocked = true on a user record prevents all future logins. Existing JWTs remain valid until they expire; forced logout requires also setting the shouldDisconnectOnce* flags, which trigger a Redis check on the next API call.

6. External users are scoped to a company

Every external (portal) user must be linked to exactly one company via external_users_to_companies. Their companyId is embedded in the JWT and enforced by the isExternalAllowed decorator — they can only see records related to their company.

7. Billing entity signatories

The filteredSignatories query is the designated way to populate signatory dropdowns on POs and SOs. It accepts an optional billingEntity filter to return only users authorized to sign for that specific entity. The isDefault flag on billing_entities_to_users surfaces the recommended signatory.

8. ERP identity bridge

Users can carry both a Jules id and an erpId. When Jules pushes data to an external ERP, the erpId and erpEmail fields allow the ERP to match Jules user records to its own user table.

9. Permit.io is per-user opt-in

The enablePermitio flag allows a staged rollout of ABAC policies. Production organizations can enable Permit.io enforcement selectively, user by user, before enforcing it organization-wide.

10. Soft delete for billing entities

Billing entities cannot be hard-deleted because they are referenced by historical invoices, operations, and PDF documents. The soft-delete mechanism preserves the record while visually marking it as deleted by appending (deleted) to the name.


Glossary

TermDefinition
ABACAttribute-Based Access Control — authorization model that evaluates user attributes, resource attributes, and environmental context
ACLAccess Control List — a set of permissions attached to a resource specifying which users can access it
Billing EntityA legal entity belonging to your organization used to issue and receive invoices and commercial documents
DepartmentAn organizational subdivision (e.g., Trading, Finance) used to classify users within a tenant
DivisionA higher-level business unit (e.g., Ferrous, Non-Ferrous) grouping departments and operations
enablePermitioPer-user flag that activates Permit.io ABAC enforcement for that user
External UserA portal user representing a supplier or customer; isExternal = true; scoped to a single company
isBlockedFlag that prevents a user from logging in
JWTJSON Web Token — a signed, self-contained authentication token carrying user identity and roles
Legal EntityThe GraphQL interface implemented by all registered parties: billing entities, companies, shipping lines, agents, etc.
LegalEntityTypeEnumEnumeration of all recognized legal party types (BILLING_ENTITY, COMPANY, AGENT, etc.)
LicenseA regulatory import/export permit associated with a material quality and country
Magic LinkPasswordless authentication via a one-time email link (Magic.link SDK)
manageeA user supervised by a manager in the managers_to_managees hierarchy
ManagerA user who has one or more managees; their data visibility is restricted to their managees' records
organizationIdThe tenant identifier; also the PostgreSQL schema name for all per-tenant tables
Permit.ioThird-party ABAC policy service used for fine-grained permission checks
@permit directiveGraphQL schema directive that triggers a Permit.io check before resolving a field
PortalThe external-facing interface for supplier/customer users with restricted data visibility
RoleA named permission level assigned to a user (e.g., ADMIN, BUYER, VALIDATOR)
RLSRow-Level Security — PostgreSQL feature enforcing per-row access control at the database level
SignatoryA user authorized to sign documents on behalf of a billing entity
SubregionA geographic sub-classification used for notification routing and user matching
user_attributesPer-user routing metadata (quality, country, region, shipment mode) used for notification targeting
UsersToCompaniesACL junction table restricting which trading companies a user can access
UsersToSitesACL junction table restricting which physical sites a user can access
VALIDATORA role that grants authorization to approve or reject operations in the approval workflow
viewGroupAn optional JWT claim used for view-level access segmentation within a tenant