Group Management

Group Management

Groups are Logship's lightweight routing and ownership primitive.

They are intentionally simple: a group has a name, an optional description, and a set of member users. The reason they matter is what they unlock elsewhere in the system. In particular, incident ownership and incident notifications both rely on groups to route responsibility to a team instead of a single person.

If you only need the short version, here it is: create a group, add the right people, use that group as the owner for incidents, and Logship will fan notifications out to the members of that group automatically.

What a group contains

Each group stores the fields below:

FieldMeaning
groupIdStable group identifier
nameRequired display name
descriptionOptional description or purpose
createdByUserIdUser who originally created the group
createdAtCreation timestamp
updatedAtLast update timestamp

Membership is stored separately from the group itself.

Each group membership record stores:

FieldMeaning
groupIdThe group the membership belongs to
userIdThe user in the group
addedByUserIdWho added the member
addedAtWhen the member was added

What groups are for

Groups are primarily used for team-based operations:

  • Incident ownership
  • Incident notification fan-out
  • Operational routing
  • Keeping accountability at the team level while assigning execution to a person

In practice, the most common pattern is:

  • Group = accountable team
  • Incident assignee = current responder

That gives you both durable ownership and a clear execution point.

Core behavior

The group system supports four main operations:

OperationWhat it does
Create/update a groupSave the group's metadata
Delete a groupRemove the group and its memberships, if it is safe to do so
Add a memberAdd a user to the group
Remove a memberRemove a user from the group

There is no separate lifecycle state model for groups. Groups are either present or deleted.

Creating and editing groups

Required fields

The only required field is name.

If the name is empty or whitespace, the save is rejected.

Optional fields

description is optional. Empty descriptions are stored as absent rather than as required content.

Timestamps and authorship

When a group is saved:

  • createdByUserId is preserved if the caller provides one
  • otherwise the current user becomes the creator
  • createdAt is preserved if supplied
  • otherwise it defaults to the current time
  • updatedAt is always set to "now"

Upsert behavior

Group saves use an upsert model under the hood.

That means the backend uses INSERT OR REPLACE semantics rather than distinct "create" and "update" endpoints. The frontend still treats create and edit as different UX flows, but the API surface is intentionally compact.

Membership management

Membership is also intentionally simple.

Adding a member

To add a member:

  • the target group must exist
  • the target user must be a member of the same account

If either validation fails, the request is rejected.

Removing a member

Removing a member deletes that specific groupId + userId membership row.

There is no extra state transition, approval flow, or soft-delete behavior.

Idempotent add behavior

Adding a member uses INSERT OR REPLACE semantics in the data store.

Practically, that means:

  • adding the same user again does not create duplicate rows
  • the latest add operation becomes the stored membership record

Permissions

One thing worth calling out: there is not a dedicated Group.* permission family today.

Group management is tied to the incident permission model.

Read access

Users can read group data if they have any of the following:

  • Logship.Account.Admin
  • Logship.Incident.Viewer
  • Logship.Incident.Editor
  • Logship.Incident.Manager
  • global admin access

Management access

Creating, editing, deleting groups, and changing membership is more restricted.

Those write operations require one of:

  • Logship.Account.Admin
  • Logship.Incident.Manager
  • global admin access

Notably, Logship.Incident.Editor can work with incidents, but it does not have enough permission to manage groups.

Delete behavior and safety rules

Deleting a group is not just a blind remove.

Before the backend deletes a group, it checks whether that group still owns any incidents.

Delete rule

If the group still owns one or more incidents, deletion is blocked.

The backend returns an error in the shape of:

Incident group {groupId} still owns {incidentCount} incidents.

What happens on successful delete

If the group is safe to delete:

  1. All memberships for the group are deleted.
  2. The group record itself is deleted.

There is no special archival step and no automatic incident reassignment because the delete is blocked until ownership is cleared.

Relationship to incidents

This is the most important reason to document groups well: they are not isolated. They directly affect incident behavior.

Groups as incident owners

An incident can be owned by either:

  • a user
  • a group

When a group owns an incident, the group acts as the accountable team.

Groups and notifications

When the incident notification service resolves recipients for a group-owned incident, it notifies all members of the owner group.

That means group membership changes have operational impact immediately:

  • add someone to the group and they become part of future incident notifications
  • remove someone from the group and they stop receiving future group-owner incident notifications

Groups and assignment

A common and well-supported pattern is:

  • incident owner = group
  • incident assignee = a single user

This lets the team remain accountable while still making it obvious who is actively driving the response.

What groups do not currently do

It is also useful to be explicit about what the group system does not provide today:

  • No group-specific state machine
  • No dedicated group history timeline
  • No built-in group notifications when membership changes
  • No approval workflow for adds/removals
  • No role levels inside a group
  • No nested groups

That simplicity is by design. Groups are operational routing objects, not a full organizational hierarchy system.

Current UI behavior

The current UI exposes two main group screens:

Group list

The groups list page is optimized for dense operational scanning. It includes:

  • Total group count
  • Count of groups with members
  • Count of empty groups
  • Search across names, descriptions, creators, IDs, and member counts
  • Filtering by creator
  • Filtering by membership status
  • Row-level navigation into group details

Group edit page

The group edit page supports:

  • Creating a new group
  • Editing name and description
  • Viewing creator and timestamps
  • Adding members from available account users
  • Removing existing members
  • Deleting the group when allowed

The UI only enables management actions for users with the appropriate management permissions.

API surface

The backend exposes a compact REST surface for groups.

Groups

GET    /accounts/{accountId}/groups
PUT    /accounts/{accountId}/groups/{groupId}
DELETE /accounts/{accountId}/groups/{groupId}

Membership

GET    /accounts/{accountId}/groups/{groupId}/members
PUT    /accounts/{accountId}/groups/{groupId}/members/{memberUserId}
DELETE /accounts/{accountId}/groups/{groupId}/members/{memberUserId}

Example payloads

Group

{
  "accountId": "5e7bc3d8-6452-4cbf-860d-657a2c0e8b22",
  "groupId": "1bd5df10-7b83-48a7-9060-f1f3f422464c",
  "name": "Platform On-Call",
  "description": "Primary responders for platform and reliability incidents.",
  "createdByUserId": "d2fdf216-32a7-42c1-97f4-8b4f8daa95f7",
  "createdAt": "2026-03-21T10:00:00Z",
  "updatedAt": "2026-03-21T10:15:00Z"
}

Group member

{
  "accountId": "5e7bc3d8-6452-4cbf-860d-657a2c0e8b22",
  "groupId": "1bd5df10-7b83-48a7-9060-f1f3f422464c",
  "userId": "fe6f1d13-ff6d-4c99-8cb3-514b9390e40d",
  "addedByUserId": "d2fdf216-32a7-42c1-97f4-8b4f8daa95f7",
  "addedAt": "2026-03-21T10:16:00Z"
}

Pattern: team ownership for incidents

Create groups that match the teams who should own incidents operationally, not necessarily the org chart exactly.

Good examples:

  • Platform On-Call
  • Database Reliability
  • Security Response
  • Customer Escalations

These names make ownership obvious in the incident UI and in notifications.

Pattern: keep membership current

Because group membership directly affects incident notifications, stale membership turns into stale paging and stale accountability very quickly.

Treat group membership as operational configuration, not as passive documentation.

Pattern: avoid empty "placeholder" groups

The UI makes empty groups visible for a reason. An empty group can technically exist, but it is usually a sign that ownership routing is incomplete.

Pattern: clear incident ownership before deleting

If you want to retire a group:

  1. Reassign any incidents it still owns
  2. Confirm it no longer owns active work
  3. Then delete the group

This matches the backend delete guard and avoids operational surprises.

Troubleshooting and gotchas

"Group name is required."

The save request did not include a usable name.

"No permission."

The caller is authenticated but does not have enough permission for the requested group operation.

"Incident group ... does not exist."

You tried to add a member to a group that is missing or invalid.

"User ... is not a member of this account."

Only users who belong to the account can be added to a group.

"Incident group ... still owns ... incidents."

You tried to delete a group that is still the owner of one or more incidents. Reassign those incidents first.

A user was added twice

This does not create duplicate membership rows. Membership adds are effectively idempotent because the store uses replace semantics.

Why didn't a group change create a notification?

Group CRUD and membership changes do not currently publish notifications on their own. Their impact shows up indirectly through incident ownership and future incident notifications.

Implementation references

If you need to trace behavior in code, start here:

  • src\Logship\Service\Services\IncidentManager\GroupManagementWebController.cs
  • src\Logship\Host\ConsoleHost\Apis\Backend\Groups.cs
  • src\Logship\App\fe-react\ClientApp\src\services\groups\groupservice.ts
  • src\Logship\App\fe-react\ClientApp\src\routes\groups\index\groups-index.tsx
  • src\Logship\App\fe-react\ClientApp\src\routes\groups\edit\groups-edit.tsx
  • src\Logship\Service\Services\Database\DatabaseConnection\Sqlite\SqliteDatabaseConnection.cs
  • src\Logship\Service\Services\Accounts\Permissioning\AuthPermissions.cs

If you are changing how groups affect incidents, also review:

  • src\Logship\Service\Services\IncidentManager\IncidentManagerWebController.cs
  • src\Logship\Service\Services\IncidentManager\IncidentAssignmentNotificationService.cs

That pairing is what turns groups from simple records into operational routing and notification targets.