Skip to content

Users & Roles

BRIDGEPORT uses a three-tier role system (admin, operator, viewer) with JWT sessions and API tokens to control who can view, operate, and administer the platform.

After your first admin account exists (see Initial Admin Setup), create additional users in under a minute:

  1. Navigate to Admin > Users (/admin/users).
  2. Click Add User.
  3. Enter email, password (8+ characters), optional name, and role.
  4. Click Create.

The new user can log in immediately and receives a welcome notification.


BRIDGEPORT supports two authentication methods: JWT sessions for interactive browser use and API tokens for programmatic access. Both carry the user’s role and grant the same permissions.

sequenceDiagram
    participant Client
    participant BRIDGEPORT
    participant DB

    alt Browser login
        Client->>BRIDGEPORT: POST /api/auth/login {email, password}
        BRIDGEPORT->>DB: Validate credentials (bcrypt)
        BRIDGEPORT-->>Client: JWT token (7-day expiry)
        Client->>BRIDGEPORT: GET /api/... (Authorization: Bearer <JWT>)
        BRIDGEPORT->>DB: Verify JWT, load user role
        Note over BRIDGEPORT,DB: lastActiveAt updated in background
    else API token
        Client->>BRIDGEPORT: GET /api/... (Authorization: Bearer <api-token>)
        BRIDGEPORT->>DB: Hash token, look up ApiToken record
        BRIDGEPORT->>DB: Check expiry, load user role
        Note over BRIDGEPORT,DB: lastUsedAt updated on token record
    end
    BRIDGEPORT->>BRIDGEPORT: Check role against route requirement
    BRIDGEPORT-->>Client: 200 OK / 403 Forbidden

Authentication flow details:

  1. The authenticate plugin tries the Authorization: Bearer value as an API token first (hash lookup in the ApiToken table).
  2. If no API token matches, it attempts JWT verification.
  3. If neither succeeds, the request gets 401 Unauthorized.
  4. After authentication, route-level middleware (requireAdmin, requireOperator) checks the user’s role.

JWTs expire after 7 days. API tokens have optional expiry set at creation time.


BRIDGEPORT has three roles in strict hierarchy: admin > operator > viewer. Higher roles inherit all permissions of lower roles.

RolePurposeTypical User
viewerRead-only access to all resources in every environmentStakeholders, on-call engineers, auditors
operatorViewer permissions + operational actions (deploy, manage secrets, trigger backups)Day-to-day platform engineers
adminFull access including user management, environments, and system settingsPlatform owners, team leads
ActionAdminOperatorViewer
View all resources (servers, services, metrics, logs)YesYesYes
View audit logsYesYesYes
View deployment historyYesYesYes
Reveal secret valuesYesYes*Yes*
Deploy servicesYesYesNo
Restart / stop / start containersYesYesNo
Run predefined commands (shell, migrate)YesYesNo
Manage secrets (create, update, delete)YesYesNo
Manage config files & syncYesYesNo
Manage databases & backupsYesYesNo
Trigger health checksYesYesNo
Create / delete environmentsYesNoNo
Edit environment settingsYesNoNo
Manage users (create, edit roles, delete)YesNoNo
Manage service types / database typesYesNoNo
System settings (SSH timeouts, webhook config)YesNoNo
SMTP / Slack / webhook channel configYesNoNo
Delete serversYesNoNo

* Secret reveal is subject to the per-environment allowSecretReveal setting and the per-secret neverReveal flag, both configured by admins.

An admin cannot change their own role. This prevents accidental self-demotion. Another admin must make the change.


All user management is under Admin > Users (/admin/users). These actions require the admin role.

POST /api/users
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"email": "alice@example.com",
"password": "securepassword",
"name": "Alice",
"role": "operator"
}

Response (200):

{
"user": {
"id": "clxyz...",
"email": "alice@example.com",
"name": "Alice",
"role": "operator",
"createdAt": "2026-02-25T10:00:00.000Z",
"updatedAt": "2026-02-25T10:00:00.000Z"
}
}

Validation: password must be 8+ characters, email must be unique (returns 409 Conflict if taken). The new user receives a welcome notification.

GET /api/users
Authorization: Bearer <admin-token>

Returns all users ordered by creation date (newest first). Each record includes id, email, name, role, lastActiveAt, createdAt, and updatedAt. Password hashes are never returned.

Admins can update name and role. Email is immutable.

PATCH /api/users/:id
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"role": "admin"
}

When a role changes, the affected user receives a notification: “Your role has been changed from operator to admin.”

Admins can reset any user’s password without knowing the current one:

POST /api/users/:id/change-password
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"newPassword": "newSecurePassword"
}

The affected user receives a notification: “Your password was changed by an administrator.”

DELETE /api/users/:id
Authorization: Bearer <admin-token>

Returns 400 Bad Request if you attempt to delete your own account. All related records (deployments, audit logs, API tokens, notifications) are cascade-deleted.

Deletion is permanent. There is no deactivation mechanism. If you need to revoke access without losing audit history, consider rotating the user’s password and revoking all their API tokens instead.


Every user, regardless of role, can manage their own profile without admin involvement.

Click the user icon at the bottom of the left sidebar. The My Account modal opens with two sections:

  • Profile — update your display name (email and role are read-only)
  • Change Password — update your own password
  1. Enter your Current Password.
  2. Enter your New Password (8+ characters).
  3. Click Change Password.
POST /api/users/:id/change-password
Authorization: Bearer <token>
Content-Type: application/json
{
"currentPassword": "oldPassword",
"newPassword": "newPassword"
}

API tokens let scripts, CI/CD pipelines, and external tools authenticate without exposing user passwords. Tokens are admin-managed and live under Admin > Integrations (/admin/integrations).

Every token has four properties beyond its name:

  • Owner — either a user or a service account. Service-account ownership is preferred for tools, since SA tokens survive when individual admins leave.
  • Roleadmin, operator, or viewer. The token’s role is capped at the owner’s role: a viewer-owned token cannot be operator-grade.
  • Environment scope — either “all environments” or a specific allowlist. Env-scoped tokens cannot reach environments outside their allowlist and are denied on global routes (users, system settings, audit log, etc.).
  • Expiry — mandatory, max 365 days. Forces rotation.

Tokens are prefixed bport_pat_ so they’re trivially detectable in logs and secret scanners.

UI: Admin > Integrations > New Token. Fill in the owner, role, scope, and expiry.

API:

POST /api/admin/tokens
Authorization: Bearer <admin-token-or-jwt>
Content-Type: application/json
{
"name": "github-actions-staging",
"ownerServiceAccountId": "clxyz...",
"role": "operator",
"allEnvironments": false,
"environmentIds": ["clenv-staging..."],
"expiresInDays": 90
}

Either ownerUserId or ownerServiceAccountId is required (exactly one). When allEnvironments is false, environmentIds must contain at least one ID.

Response:

{
"token": "bport_pat_abc123...",
"tokenRecord": {
"id": "cltok...",
"name": "github-actions-staging",
"tokenPrefix": "bport_pat_abc1",
"role": "operator",
"allEnvironments": false,
"expiresAt": "2026-08-18T10:00:00.000Z",
"createdAt": "2026-05-20T10:00:00.000Z",
"userId": null,
"serviceAccountId": "clxyz..."
}
}

The full token value is returned only once at creation time. BRIDGEPORT stores only a SHA-256 hash. Copy it immediately and store it in your secrets manager. If lost, revoke the token and mint a new one.

GET /api/admin/tokens
Authorization: Bearer <admin-token>

Optional query: ?ownerUserId=<id> or ?ownerServiceAccountId=<id> to filter. Records include scope and ownership but never the token hash.

DELETE /api/admin/tokens/:tokenId
Authorization: Bearer <admin-token>

Revocation is immediate. Any request using the token will get 401 Unauthorized after this call returns.

HTTP header (most requests):

Authorization: Bearer bport_pat_abc123...

Query parameter (SSE connections):

GET /api/events?token=bport_pat_abc123...

The SSE query parameter approach exists because EventSource clients cannot set custom headers.

When the token authenticates a request, BRIDGEPORT computes effective role = min(token role, owner role). If an owner gets demoted after a token was minted, the token’s permissions drop automatically — no need to re-issue.

Use CaseRecommended Setup
CI/CD deploy to stagingService account, operator, env-scoped to staging, 90-day expiry
Read-only dashboardsService account, viewer, all environments
Personal CLIUser-owned, role matches the user, all environments
Webhook integrationsService account, role matches the action, env-scoped
AI agent via MCPService account; viewer for read-only, operator to deploy/restart/backup; all environments for the full tool surface

Service accounts are machine identities — they own tokens but never log in. Use them for any tool that talks to BRIDGEPORT (CI/CD, monitoring scrapers, deploy bots) so the credential is decoupled from any one person’s user account.

If a CI pipeline uses a token owned by alice@company.com and Alice leaves, her account is deactivated and the pipeline breaks. With a service account, the credential is attached to a named identity (ci-deploy-staging) that outlives any admin.

UI: Admin > Integrations > New Service Account.

POST /api/admin/service-accounts
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"name": "ci-deploy-staging",
"description": "GitHub Actions deployer for staging",
"role": "operator"
}

Names follow the pattern [a-z0-9][a-z0-9_-]* (lowercase, max 64 chars). Each SA has a role that caps all tokens minted against it.

Set disabled: true via PATCH /api/admin/service-accounts/:id to immediately invalidate every token belonging to that SA without revoking them individually. Re-enable later to restore.

DELETE /api/admin/service-accounts/:id cascades to all of the SA’s tokens. Audit log entries created by the SA are preserved (the serviceAccountId link nulls out via ON DELETE SET NULL).


On first boot, if no users exist, BRIDGEPORT creates an admin account from environment variables:

Terminal window
ADMIN_EMAIL=admin@example.com
ADMIN_PASSWORD=your-secure-password

The bootstrapAdminUser() function in src/services/auth.ts checks prisma.user.count() and only proceeds if zero users exist, making it safe to leave these variables set permanently.

If neither variable is set, use the one-time registration endpoint instead:

POST /api/auth/register
Content-Type: application/json
{
"email": "admin@example.com",
"password": "securepassword",
"name": "Admin"
}

This creates the user with the admin role and returns a JWT. It returns 403 Forbidden if any user already exists, effectively disabling open registration after bootstrap.


BRIDGEPORT tracks when users are actively using the application. On every JWT-authenticated request, the lastActiveAt field is updated in the background (fire-and-forget, no added latency).

In Admin > Users, the page header shows an active users summary. Individual user cards show a green “Online” badge for currently active users.

GET /api/users/active
Authorization: Bearer <admin-token>

Returns users whose lastActiveAt is within the configured active window.

The window is controlled by activeUserWindowMin in System Settings (default: 15 minutes):

PATCH /api/settings/system
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"activeUserWindowMin": 30
}

SettingLocationDefaultDescription
ADMIN_EMAILEnvironment variableEmail for auto-created admin on first boot
ADMIN_PASSWORDEnvironment variablePassword for auto-created admin on first boot
activeUserWindowMinSystem Settings15Minutes of inactivity before a user is no longer “active”
allowSecretRevealEnvironment Settings > ConfigurationtrueWhether non-admin users can reveal secret values
JWT expiryHardcoded7 daysJWT token lifetime

“Email already in use” when creating a user Email must be unique. Check existing users with GET /api/users.

“Cannot delete your own account” Intentional safeguard. Log in as a different admin to delete the account.

“Current password is incorrect” when changing password The submitted current password does not match. Admins can bypass this by calling POST /api/users/:id/change-password for another user without providing currentPassword.

“Registration disabled” on POST /api/auth/register At least one user exists. Use POST /api/users with an admin token to create additional accounts.

API token returns 401 after rotation Any system using the old token value will fail. Update the token in all dependent systems before revoking the old one. Consider creating the replacement token first, updating integrations, then deleting the old token.

User not showing as “Online” in admin panel lastActiveAt is only updated on JWT requests, not API token requests. Users authenticating exclusively via API tokens (e.g., service accounts) will not appear online.

Admin cannot change their own role in the UI The role dropdown is intentionally disabled when editing your own account. Ask another admin to make the change.