KW Knowledge base

Contractors / Vendors

The platform supports external **contractors and vendors** (subcontractors, freelancers, suppliers) who get their own dedicated portal at `/vendors`. They sign in via a separate `/vendor-login` URL, see only what's been explicitly assigned

Mitch Wigham
Updated 21 July 2026 · 46 views

29 · Contractors / Vendors

The platform supports external contractors and vendors (subcontractors, freelancers, suppliers) who get their own dedicated portal at /vendors. They sign in via a separate /vendor-login URL, see only what's been explicitly assigned to them, and update their own status, time, and ticket replies.

The internal role code is VENDOR for both — the label "Contractor / Vendor" is purely the user-facing display string.

Two distinct surfaces

Audience URL Chrome
Staff /login/ Full PortalShell, sidebar, all modules
Vendor /vendor-login/vendors Stripped-down VendorShell (5 tabs)

The main staff login screen has a Vendor sign in link in its footer (alongside Status and Knowledge base) that goes to /vendor-login. Vendors arriving via either login flow are auto-redirected to /vendors after authenticating; staff arriving via /vendor-login are refused with the error "This is the contractor / vendor portal login. Use the staff sign-in instead."

Creating a vendor account

/admin/users/invite — pick VENDOR from the role dropdown.

You can use either tab:

  • ✉️ Send invite — vendor sets their own password.
  • ⚡ Create now — admin sets the password directly. Useful when onboarding a sub-contractor on a kickoff call.

⚠️ Caution. Vendor accounts are scoped to your tenant. If you operate multiple tenants, create the vendor in the tenant whose projects they'll work on.

Linking a vendor to a project

Once the account exists, open the project (/projects/<id>) — the Vendors card on the detail page lets admins:

  • Pick a vendor from the dropdown (only VENDOR-role users in the current tenant appear)
  • Add an optional role label ("Cabling", "Racking", …)
  • Click Link vendor to grant access
  • Click Remove to revoke

The vendor sees the project under /vendors/projects immediately on their next refresh.

📷 Screenshot placeholder: screenshots/vendor-link-project.png

What a vendor sees

Five tabs on the vendor portal:

Tab Scope
🏠 Dashboard Counts of their projects + open tickets, recent rows
📋 Projects Projects where they have a ProjectVendor link
🎫 Tickets Tickets where assigneeId is their user ID
⏱️ Timesheets Their own time entries (Project + Manual kinds)
👤 Profile Read-only profile view

What a vendor does not see:

  • Other vendors' rows (even on the same project)
  • Staff-assigned tickets, even if linked to a project they're on
  • Any module not in the five tabs (no CRM, no RMM, no admin, no customers)
  • The tenant switcher (it's hidden for non-admin-tenancy users)

What a vendor can update

Surface What they can change
Project detail Status notes (free text) — visible to staff. Nothing else (status, billable, invoice fields are locked)
Project detail Log time against the project (PROJECT kind), against projects they're linked to
Ticket detail Status — set the ticket's typed status (statusId) to any status
Ticket detail Add a comment — any comment a vendor posts is created as public (isInternal: false). Note that the vendor ticket view does still show existing internal notes — internal notes are not hidden from vendors.
Timesheets Log PROJECT or GENERAL entries, and delete their own (no TICKET kind). The vendor time API also supports editing an entry, but the vendor portal UI exposes only logging and delete.
Profile Read-only for now

Access boundary

Every /api/vendor/* route runs requireVendor() first:

  1. Verify a valid JWT
  2. Verify role === 'VENDOR'
  3. Filter every query by userId === jwt.sub

A staff token hitting any vendor API gets 403 "Contractor / Vendor account required". A vendor token hitting any staff API hits the existing auth-service guards (which check role for admin-only endpoints).

The (vendor) route group's client-side layout also redirects:

  • No JWT → /vendor-login
  • JWT with role !== 'VENDOR'/

This is defence in depth — the API checks are the real boundary. The client checks are UX so non-vendors don't get a confusing empty shell.

End-to-end test (verified)

1. Admin creates vendor (e2e-vendor@example.test, role=VENDOR, password set)
2. Vendor logs in via POST /api/auth/login → 200 + JWT(role=VENDOR)
3. GET /api/vendor/me → 200 with vendor's record, role=VENDOR
4. GET /api/vendor/projects → 200, [] (no project links)
5. Admin token GET /api/vendor/me → 403 "Contractor / Vendor account required" ✓

Common workflows

Onboard a subcontractor for a SNAG project

  1. Admin → Users → Invite → role=VENDOR. Either send invite or set password directly.
  2. Open the SNAG project → Vendors card → pick the new vendor → role "Cabling" → Link.
  3. Tell the subbie to go to portal.kwgroup.org.uk/vendor-login.
  4. They log in, see only the SNAG project on /vendors, log time against it as they work.

Assign a ticket to a vendor for triage

  1. On the ticket: Assignee dropdown → pick the vendor user.
  2. Ticket immediately appears at /vendors/tickets for them.
  3. They reply with a public comment + change status to "In progress".

Offboard a vendor

  1. Project → Vendors → Remove for each project they're on.
  2. Admin → Users → suspend the account.
  3. Their existing time entries stay (audit trail).

See also

Still need help?

Log a support ticket and the team will pick it up from this page.