Skip to content

Customer Portal

The Customer Portal gives end-users in your managed organizations a self-service interface for common IT tasks. Portal users can view their organization’s devices, submit and track support tickets, check out and return shared assets, and manage their own profile — all without needing access to the full Breeze admin dashboard.

Each organization gets its own portal instance with customizable branding, a dedicated user directory, and granular feature toggles. Portal users authenticate with their own credentials, separate from admin accounts, and sessions are scoped to their organization.


Each organization can enable or disable portal features independently through the branding configuration.

| Feature | Default | Description | |---------|---------|-------------| | enableTickets | true | Allow portal users to submit and track support tickets | | enableAssetCheckout | true | Allow portal users to check out and return shared devices | | enableSelfService | true | Allow portal users to view organization devices | | enablePasswordReset | true | Allow portal users to reset their password via email |

| Status | Meaning | |--------|---------| | new | Ticket just submitted, not yet triaged | | open | Ticket acknowledged and being worked on | | pending | Waiting for additional information from the submitter | | on_hold | Work paused, awaiting external dependency | | resolved | Issue fixed, awaiting confirmation | | closed | Ticket fully closed |

| Priority | Description | |----------|-------------| | low | Non-urgent issue, can be addressed during normal operations | | normal | Standard priority (default for new tickets) | | high | Important issue requiring prompt attention | | urgent | Critical issue requiring immediate response |

| Status | Behavior | |--------|----------| | active | User can log in and access portal features | | Any other value | Login is blocked with a 403 response |


Portal authentication is separate from the main Breeze admin auth system. Portal users have their own credentials stored in the portal_users table and sessions are managed through either Redis (production) or in-memory storage (development).

Sessions use 48-character tokens generated by nanoid. Each authenticated request slides the session expiry forward, keeping active users logged in.

| Setting | Value | |---------|-------| | Session TTL | 24 hours (sliding) | | Reset token TTL | 1 hour | | Cookie name | breeze_portal_session | | CSRF cookie name | breeze_portal_csrf_token | | Auth methods | Bearer token, session cookie |

The portal applies per-IP and per-account rate limiting to authentication endpoints to prevent brute-force attacks.

| Endpoint | Window | Max Attempts | Block Duration | |----------|--------|--------------|----------------| | Login | 5 minutes | 10 | 15 minutes | | Forgot password | 15 minutes | 5 | 30 minutes | | Reset password | 15 minutes | 10 | 30 minutes |


Each organization can customize the portal appearance and contact information. Branding is resolved by custom domain — when a portal user visits their organization’s custom domain, Breeze serves the matching branding configuration.

| Field | Type | Description | |-------|------|-------------| | logoUrl | URL | Organization logo displayed in the portal header | | faviconUrl | URL | Browser tab icon | | primaryColor | CSS color | Main brand color | | secondaryColor | CSS color | Secondary accent color | | accentColor | CSS color | Highlight color for interactive elements | | customDomain | String | Custom domain for the portal (e.g., support.acme.com) | | domainVerified | Boolean | Whether the custom domain has been verified | | welcomeMessage | Text | Welcome text displayed on the portal login page | | supportEmail | Email | Support contact email shown in the portal | | supportPhone | Phone | Support phone number shown in the portal | | footerText | Text | Custom footer text | | customCss | CSS | Custom CSS injected into the portal pages |


Portal users can submit support tickets, track their status, and add comments to ongoing conversations.

  1. Log in to the customer portal.
  2. Navigate to the Tickets section.
  3. Click New Ticket and fill in the subject, description, and priority.
  4. Submit the ticket. A unique ticket number is auto-generated.

Tickets are automatically associated with the submitter’s portal user account and organization. Each ticket receives a unique alphanumeric ticket number for easy reference.

Both portal users and admin technicians can add comments to tickets. Portal users only see public comments (isPublic: true). Internal notes added by technicians with isPublic: false are hidden from the portal view.

Tickets support linking to external ticketing systems through the externalTicketId and externalTicketUrl fields, enabling integration with tools like ConnectWise, Autotask, or Zendesk.


The asset checkout system lets portal users borrow shared devices from their organization’s inventory and return them when finished.

  1. Navigate to Assets in the portal.
  2. Browse the list of available devices (devices not currently checked out).
  3. Select a device and click Check Out.
  4. Optionally provide an expected return date, condition notes, and checkout notes.
  5. Confirm the checkout.
  1. Navigate to Assets in the portal.
  2. Find your checked-out asset and click Check In.
  3. Optionally provide return condition notes and check-in notes.
  4. Confirm the check-in.

| Field | Required | Description | |-------|----------|-------------| | expectedReturnAt | No | ISO 8601 datetime for expected return | | checkoutNotes | No | Notes about the checkout (max 2000 chars) | | condition | No | Condition of the device at checkout (max 100 chars) |

| Field | Required | Description | |-------|----------|-------------| | checkinNotes | No | Notes about the return (max 2000 chars) | | condition | No | Condition of the device at return (max 100 chars) |


Portal users can view and update their profile, change their password, and manage notification preferences.

Send a PATCH to the profile endpoint with any combination of:

| Field | Type | Description | |-------|------|-------------| | name | String (1-255 chars) | Display name | | receiveNotifications | Boolean | Whether to receive email notifications | | password | String (min 8 chars) | New password (replaces current) |

The dedicated password change endpoint requires the current password for verification before accepting a new password. After a successful password change, all existing sessions for the user are invalidated.


All portal endpoints are mounted under /api/v1/portal. Public routes (branding, auth) do not require authentication. Protected routes require a valid portal session token.

| Method | Path | Auth | Description | |--------|------|------|-------------| | POST | /auth/login | No | Log in with email and password | | POST | /auth/forgot-password | No | Request a password reset email | | POST | /auth/reset-password | No | Reset password with a reset token | | POST | /auth/logout | Yes | Log out and invalidate the current session |

| Method | Path | Auth | Description | |--------|------|------|-------------| | GET | /branding | No | Get branding for the current domain (resolved from Host header) | | GET | /branding/:domain | No | Get branding for a specific domain |

| Method | Path | Auth | Description | |--------|------|------|-------------| | GET | /devices | Yes | List devices in the portal user’s organization |

| Method | Path | Auth | Description | |--------|------|------|-------------| | GET | /tickets | Yes | List tickets submitted by the current portal user | | POST | /tickets | Yes | Create a new support ticket | | GET | /tickets/:id | Yes | Get ticket details with public comments | | POST | /tickets/:id/comments | Yes | Add a comment to a ticket |

| Method | Path | Auth | Description | |--------|------|------|-------------| | GET | /assets | Yes | List available devices for checkout | | POST | /assets/:id/checkout | Yes | Check out a device | | POST | /assets/:id/checkin | Yes | Check in a device |

| Method | Path | Auth | Description | |--------|------|------|-------------| | GET | /profile | Yes | Get current portal user profile | | PATCH | /profile | Yes | Update profile (name, notifications, password) | | POST | /profile/password | Yes | Change password (requires current password) |


Portal API responses use HTTP cache headers for optimal performance.

| Route Type | Cache Scope | Browser Max-Age | Stale-While-Revalidate | |------------|-------------|-----------------|------------------------| | Branding | public | 5 minutes | 24 hours | | Devices, Tickets, Assets | private | 15 seconds | 90 seconds | | Profile | private | 15 seconds | 60 seconds |

All authenticated endpoints also return ETag headers. Clients that send If-None-Match receive a 304 Not Modified response when the data has not changed, saving bandwidth.


All mutating portal actions are recorded in the audit log with an actorType of user and actions prefixed with portal.:

| Action | Trigger | |--------|---------| | portal.ticket.create | New ticket submitted | | portal.ticket.comment.create | Comment added to a ticket | | portal.asset.checkout | Device checked out | | portal.asset.checkin | Device checked in | | portal.profile.update | Profile updated | | portal.profile.password.change | Password changed |


Portal login returns “Multiple portal accounts found”

Section titled “Portal login returns “Multiple portal accounts found””

This error occurs when the same email address exists in multiple organizations and no orgId was provided in the login request. The portal frontend should prompt the user to select their organization or include the orgId field in the login payload.

Verify that the domainVerified field is set to true for the organization’s branding record. Branding is only served for verified domains.

When using cookie-based authentication, ensure your frontend reads the breeze_portal_csrf_token cookie and sends its value in the x-breeze-csrf request header on every POST, PATCH, and DELETE request.

”Service temporarily unavailable” (503) on login

Section titled “”Service temporarily unavailable” (503) on login”

In production mode, portal sessions require Redis. If Redis is unreachable, the portal returns 503. Verify your Redis connection is healthy and the PORTAL_STATE_BACKEND environment variable is set correctly.

Rate limits apply per IP address and per account. After being blocked, wait for the duration specified in the Retry-After response header. Rate limit state is automatically cleared on successful login.