Company Collaboration
Secure, auditable company-to-company collaboration system for establishing business relationships, sharing resources, and managing cross-company permissions.
Core Capabilities
- Connection Management -- Formal establishment of business relationships between companies
- Partner Onboarding -- Collaborating partner invitations with default role assignment
- Invitation Type Discrimination -- Clear distinction between collaborator invitations and connection requests
- Bidirectional Relationships -- Company connections visible to both parties
- Audit Compliance -- Complete traceability of all resource sharing activities
- Communication Control -- Granular notification preferences for collaboration events
For the detailed source-reviewed rulebook, see Collaborators, Partners, and Other Collaborations.
Connection Request Workflow
Connection requests use token-based acceptance for email-based workflows:
Company A sends request --> Token generated + email sent to Company B
|
Company B clicks accept link --> Token validated --> Relationship created
| (bidirectional)
(or) Token expired / declined
Only company administrators (Company Admin or Super Admin roles) can accept connection requests. Non-admin users who click the accept link receive a ForbiddenException. This ensures connection approval follows proper authorization channels.
Database Model
model CompanyConnectionRequest {
id String @id @default(uuid())
fromCompanyId String @db.Uuid
toCompanyEmail String
toCompanyId String? @db.Uuid
relationshipType RelationshipType
requestedById String @db.Uuid
approvedById String? @db.Uuid
token String @unique
status ConnectionRequestStatus @default(PENDING)
message String?
responseMessage String?
expiresAt DateTime
// ... timestamps
@@unique([fromCompanyId, toCompanyEmail, relationshipType])
}
The @@unique constraint on [fromCompanyId, toCompanyEmail, relationshipType] prevents duplicate requests of the same type to the same recipient.
Connection Requests as CRM Shell Links
CompanyConnectionRequest does double duty. Besides the email-based connection flow above, a PENDING request is also how a company shell is linked to the company that created it:
- When
addInactiveCompanycreates or finds a shell, it upserts a PENDINGCompanyConnectionRequestwithtoCompanyId = shell.idand a ~100-year expiry. This pending request is the per-org CRM link — it's idempotent, so re-adding the same shell doesn't duplicate it. - When the shell has no email, a synthetic address
shell-<id>@unregistered.aiqlick.localis used astoCompanyEmailso the@@uniquemodel still holds. - When the shell later onboards and is claimed in place, these pre-existing PENDING links surface in the claimed company's Incoming tab — the org can then accept the relationships that were staged against its shell.
addInactiveCompany refuses to shadow a live company. If a company with the same normalized org number is already active (has a billingOwnerId), the mutation throws ConflictException: "A company with this organization number is already active on the platform. Send a connection request instead." This steers shell-adds toward the real connection-request flow when the counterparty is already on the platform.
Key GraphQL Operations
# Send a connection request
mutation CreateConnectionRequest($input: CreateConnectionRequestInput!) {
createCompanyConnectionRequest(input: $input) {
id
status
token
expiresAt
fromCompany { id name }
}
}
# Accept via token (requires authentication and target-company admin role)
mutation AcceptConnection($token: String!) {
acceptCompanyConnectionRequest(token: $token) {
id
status
approvedAt
}
}
# Accept from dashboard (by request ID)
mutation AcceptById($id: ID!) {
acceptCompanyConnectionRequestById(id: $id) {
id
status
approvedAt
}
}
Bidirectional Relationships
When a connection request is accepted, CompanyRelationship records are created in both directions (A→B and B→A), ensuring both companies see the relationship in their connections list.
Query Connected Companies
query {
companyRelationshipsByCompany(
companyId: "company-uuid"
filter: { relationshipType: PARTNERSHIP }
) {
id
fromCompanyId
toCompanyId
relationshipType
fromCompany { id companyName }
toCompany { id companyName }
createdAt
}
}
Collaborating Partner Invitations
When inviting a contact as a collaborating partner, the system:
- Creates the collaboration invitation
- Defaults the role to "External Sales" if no role is specified
- Sends an invitation email to the partner
When the partner accepts the invitation:
- A collaborator record is created with the assigned role
- A contact record is automatically created in the inviting company's contact directory (via the
ContactAutoCreateListener)
Accepting Without a Company
The acceptCollaborationInvitation mutation treats selectedCompanyId as
optional — invitees who don't yet have a company on AIQLick (e.g. a
brand-new user invited as a Client/Partner contact) can accept without
linking a company. The collaborator record is created with
selectedCompanyId: null and status: ACTIVE, and the invitee can link
a company later from their dashboard once they create one.
input AcceptCollaborationInvitationInput {
token: String!
selectedCompanyId: ID # nullable — omit if the invitee has no company yet
}
The /message/accept page in the frontend shows a friendly note when
the invitee has no companies, instead of blocking acceptance:
You don't have a company on AiQlick yet. You can still accept this invitation now and link a company later from your dashboard.
Invitation Type Discrimination
All invitation and connection request outputs include an invitationType field to help the frontend route to the correct UI:
| Type | Description |
|---|---|
COLLABORATOR | Collaborating partner invitation |
CONNECTION_REQUEST | Company-to-company connection request |
This field is also included in notification metadata for invitation events.
Job Sharing Permissions
When companies share jobs, three permission levels control access:
| Permission | View Job | View Candidates | Add Candidates | Manage Pipeline |
|---|---|---|---|---|
READ_ONLY | Yes | -- | -- | -- |
CANDIDATE_ONLY | Yes | Yes | Yes | -- |
FULL_ACCESS | Yes | Yes | Yes | Yes |
Jobs have three company role fields that define relationships:
- ownerCompanyId -- The company that created the job
- hiringCompanyId -- The company that is hiring (may differ from owner)
- supplierCompanyId -- An external recruiter or staffing agency
Audit Logging
Every share operation is logged through CompanyShareAuditService. The base class pattern ensures all sharing services inherit consistent audit behavior:
abstract class CompanyShareBaseService {
protected async logShareAction(params: {
resourceType: ResourceType; // JOB, TALENT, PIPELINE, etc.
resourceId: string;
actionType: ShareActionType; // SHARED, REVOKED, PERMISSION_CHANGED
fromCompanyId: string;
toCompanyId: string;
performedById: string;
permissionLevel?: AccessPermission;
previousPermission?: AccessPermission;
metadata?: any;
}) {
return this.auditService.logShare(params);
}
}
Querying Audit Logs
query GetShareAuditLogs($filter: CompanyShareAuditFilterInput) {
companyShareAuditLogs(filter: $filter) {
id
resourceType
actionType
permissionLevel
previousPermission
createdAt
fromCompany { id name }
toCompany { id name }
performedBy { id firstName lastName }
}
}
query GetAuditStatistics($companyId: ID!) {
companyShareAuditStatistics(companyId: $companyId) {
totalShares
activeShares
revokedShares
byResourceType { resourceType count }
}
}
Collaboration Events
Nine collaboration events trigger notifications via CollaborationNotificationListener:
| Event | Trigger | Notification |
|---|---|---|
talent.shared | Talent(s) shared with user or company | Recipient notified with sharer name and talent details |
talent.unshared | Talent share revoked | Affected users notified |
invitation.received | Company invitation sent | Invitee receives email + in-app notification |
invitation.accepted | Invitation accepted | Inviter notified; contact auto-created |
invitation.declined | Invitation declined | Inviter notified |
connection.request.received | Company connection request sent | Target company admins notified |
connection.request.accepted | Connection request accepted | Requester company notified |
permission.changed | User role/permission changed | Affected user notified with old and new role |
resource.shared | Generic resource shared | Recipients notified |
// Example: emitting a connection request event
this.eventEmitter.emit('connection.request.received', {
requestId: request.id,
requesterCompanyId: fromCompany.id,
targetCompanyId: toCompany.id,
requestMessage: input.message,
});
Security
All collaboration resolvers require authentication via @UseGuards(JwtAuthGuard). Key security measures:
- Admin-only acceptance -- Connection request acceptance requires Company Admin or Super Admin role
- Token-based acceptance -- Connection requests use cryptographically secure tokens for email-based workflows
- Company context validation -- Guards verify the user belongs to the relevant company before allowing operations
- Plan limit enforcement --
@CheckPlanLimit('maxCollaborationInvites')restricts invitations based on subscription tier - Bidirectional duplicate checking -- Connection requests check both directions (A→B and B→A) before allowing new requests
- Audit trail -- All share and permission change operations are logged with performer identity and timestamps
Connection request tokens have an expiration date. Expired tokens cannot be used to accept requests. The unique constraint [fromCompanyId, toCompanyEmail, relationshipType] prevents duplicate pending requests.
Related entities (fromCompany, toCompany, performedBy) on audit logs and connection requests are loaded via GraphQL field resolvers, keeping queries efficient by only loading relations when explicitly requested by the client.
Job Seeker Profile Invitations
Job seekers can invite employers to view their profile. The flow:
When an employer accepts:
- A Talent record is created with
source: INVITED - The talent is assigned to the company's Default talent pool (created automatically if it doesn't exist)
- The job seeker's active CV is linked to the talent record
- A confirmation email is sent to the job seeker
Invited talents are immediately visible on the company's talents page. When they become candidates, they receive automatic email notifications for pipeline status changes and interview events — non-invited candidates do not.
Related
- Collaborators, Partners, and Other Collaborations -- Detailed rules for collaborator invitations, partner connections, shares, audit logs, and job seeker profile invitations
- Contact Management — Contact directory with notes, import, merge, and sharing
- Interview Scheduling — Interview prep notifications for candidates
- Talent Management — Talent sources and pool management
- Candidate Management — Candidate notification rules and manual notifications