---
url: "https://vueda.dev/v3/guides/manage-groups.md"
title: "Manage Groups and Generate Group Migrations"
---

# Manage Groups and Generate Group Migrations

Use the [Group Management Page](/v3/reference/glossary.md#group-management-page) to add groups to permissions, rename groups, and remove permissions from groups. VUEDA records each change. Then run [`makegroupmigrations`](/v3/reference/api/py/vueda.user.management.commands.makegroupmigrations/Command.md) to write a [Group Permission Migration](/v3/reference/glossary.md#group-permission-migration), which applies the same changes in your other environments.

A group's permissions feed each member's [Baseline Permission](/v3/reference/glossary.md#baseline-permission). To audit which groups hold each permission without editing anything, use the [Permissions and Workflow Overview](/v3/reference/glossary.md#permissions-and-workflow-overview), described in [Use the Permissions and Workflow Overview](/v3/guides/permissions-workflow-overview.md).

## Before You Begin

- The server runs with [Django: DEBUG](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-DEBUG) on. The group management page and the URLs that it saves through exist only in debug mode.
- `vueda.user` is in [Django: INSTALLED\_APPS](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-INSTALLED_APPS). [`get_defaults`](/v3/reference/api/py/vueda.core.default_settings.md#get_defaults) includes it.
- The project includes the [`vueda.user` URLs](/v3/reference/api/py/vueda.user.urls.md) under `routes/`, as the template app does. The page's script sends its save and remove requests to paths under `/routes/`.
- Your account holds these permissions, directly or through a group:
    - `auth.list_permission` to open the page.
    - `auth.create_group` and `auth.update_group` to add or rename a group.
    - `auth.delete_permission` to remove a permission from a group.

Grant these permissions outside the page, for example in `python manage.py shell`. You need `auth.list_permission` before you can open the page, and the page hides the other three permissions.

## Group Management UI

### Open the Page

1. Sign in to the server. In debug mode, `/routes/vueda.user/dev-login/` serves a Django login form. Add `?next=/routes/vueda.user/permissions/overview/` to that URL to land on the page after you sign in.
2. Open `/routes/vueda.user/permissions/overview/`. On a local template app this is `http://localhost:8000/routes/vueda.user/permissions/overview/`.

An anonymous request redirects to the [Django: LOGIN\_URL](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-LOGIN_URL) setting. A signed-in user without `auth.list_permission` gets `403`.

The page groups permissions by app, then by model. Apps in `LOCAL_APPS` appear under **My App Permissions**, and the rest under **Other App Permissions**. Historical models stay hidden until you select **Toggle historical**. Each permission shows its codename, its name, and the groups that hold it.

The page hides some permissions. For `auth.Group`, `auth.Permission`, `contenttypes.ContentType`, and the VDQ queue models, it hides the create, update, and delete permissions and shows read and list. For models such as `sessions.Session` and `vueda_user.GroupChange`, it hides every permission. The [`vueda.user.globals`](/v3/reference/api/py/vueda.user.globals.md) module lists each hidden model.

### Add a Group to a Permission

1. Find the permission and select **Add**. A name field appears with **Save** and **X** buttons.
2. Type the group name, then select **Save** or press Enter.

When no group has that name, VUEDA creates the group, adds the permission, and records an `added` change. When the group exists, VUEDA adds the permission to it and records an `associated` change. When the group already holds the permission, the page shows an error and records nothing.

**Save** changes color while its name field has unsaved edits.

### Rename a Group

1. Find any permission that the group holds.
2. Edit the group name, then select **Save**.

VUEDA renames the group and keeps its permissions and members. The page shows the new name on every row for that group. VUEDA records a `changed` change with the old and new names, so the migration can run forward and backward.

### Remove a Permission from a Group

1. Find the permission.
2. Select **X** next to the group name.

The page removes the permission at once, with no confirmation step. VUEDA records an `unassociated` change. The group and its members stay, even when the group holds no permissions.

::: warning

The group management page has no action that deletes a group; [#423](https://github.com/arrai-innovations/vueda/issues/423) tracks one. Do not delete a group through the Django admin or a shell. VUEDA records no change for that deletion, so every other environment keeps the group.

:::

## Generating Group Migrations

1. Write the migration:

    ```console
    python manage.py makegroupmigrations
    ```

    The command writes the migration into the app of [Django: AUTH\_USER\_MODEL](https://docs.djangoproject.com/en/6.1/ref/settings/#std-setting-AUTH_USER_MODEL), with a name like `0005_group_permission_migrations_2026_04_21`. When there is nothing new to write, it prints `No group changes detected.`

2. Fake the migration in your own database, which already has the changes. The command prints a note that tells you to fake it.

    ```console
    python manage.py migrate myapp 0005_group_permission_migrations_2026_04_21 --fake
    ```

3. Ship the migration with your code. Other environments apply it with `python manage.py migrate`.

To preview the migration name without writing a file, add `--dry-run`. The command passes `--dry-run` to Django's `makemigrations`, and still prints the note to fake the migration.

```console
python manage.py makegroupmigrations --dry-run
```

### Which Changes a Migration Includes

VUEDA stores each recorded change as a [`GroupChange`](/v3/reference/api/py/vueda.user.models/GroupChange.md) row. `makegroupmigrations` skips the rows that an existing group migration in the same app already holds. It writes the rest in the order that VUEDA recorded them.

A change names its group by name and its permission by codename, app label, and model name. It holds no primary keys, because keys differ between databases.

When a group migration runs forward, it adds a `GroupChange` row for each of its changes that has none. A later `makegroupmigrations` run in that environment then skips those changes.

### Change Types

Each change has one of five types, listed in [`GroupChangeTypes`](/v3/reference/api/py/vueda.user.management.commands.makegroupmigrations/GroupChangeTypes.md). Rolling back a migration applies its changes in reverse order, each with the backward effect below.

| Type           | Recorded when                                                | Forward                                                    | Backward                                                   |
| -------------- | ------------------------------------------------------------ | ---------------------------------------------------------- | ---------------------------------------------------------- |
| `added`        | You save a name that no group has                            | Creates the group if it is missing and adds the permission | Removes the permission and keeps the group                 |
| `associated`   | You save the name of an existing group                       | Adds the permission                                        | Removes the permission                                     |
| `changed`      | You rename a group                                           | Renames the group                                          | Renames the group back                                     |
| `unassociated` | You remove a permission from a group                         | Removes the permission and keeps the group                 | Adds the permission                                        |
| `deleted`      | Earlier releases, when you removed a group's last permission | Removes the permission and keeps the group                 | Creates the group if it is missing and adds the permission |

No change type deletes a group, in either direction.

Each group migration carries its own copy of the code that applies these changes. A group migration generated by an earlier VUEDA release still deletes a group when it replays a `deleted` change or rolls back an `added` change. Review those migrations before you run them in another environment. The `updategroupmigrations` command, described in [Updating Existing Group Migrations](/v3/guides/manage-groups.md#updating-existing-group-migrations), replaces their copy with the current code.

### What the Migration File Contains

A group migration is a standard Django migration. `makegroupmigrations` adds:

- `changed_data`, the list of changes that the migration applies. [How `changed_data` Is Formatted](/v3/guides/manage-groups.md#how-changed-data-is-formatted) describes its layout.
- Copies of [`migrate_step`](/v3/reference/api/py/vueda.user.management.commands.makegroupmigrations.md#migrate_step), [`forwards_migrate_groups`](/v3/reference/api/py/vueda.user.management.commands.makegroupmigrations.md#forwards_migrate_groups), [`backwards_migrate_groups`](/v3/reference/api/py/vueda.user.management.commands.makegroupmigrations.md#backwards_migrate_groups), [`make_sure_permissions_exist`](/v3/reference/api/py/vueda.user.management.commands.makegroupmigrations.md#make_sure_permissions_exist), `GroupChangeTypes`, and their helpers. The migration runs `make_sure_permissions_exist` first, so every permission it names exists.
- A comment near the top that marks the file as a group migration. VUEDA's group migration commands find group migrations by this comment, so keep it.

### How `changed_data` Is Formatted

`makegroupmigrations` formats `changed_data` with the Python formatter that your project uses, [black](https://black.readthedocs.io/) or [ruff](https://docs.astral.sh/ruff/), when one is installed. Projects usually install these as development dependencies. `makeworkflowmigrations` and `updateworkflowmigrations` format their `changed_data` the same way, as [How `changed_data` Is Formatted](/v3/guides/manage-workflows.md#how-changed-data-is-formatted) in the workflow guide describes.

The command uses the first formatter in this list that applies:

1. black, when the `pyproject.toml` at your project root has a `[tool.black]` section, even an empty one. A project can install ruff only to lint, so black configuration comes first. A black configuration in your home directory, such as `~/.config/black`, does not count. One developer's file therefore cannot choose the formatter for the whole project.
2. ruff, when it is installed.
3. black with its default settings, when it is installed but not configured.
4. Python's `pprint` with a narrow width, when neither is installed. Its output uses single quotes and ignores your settings.

If the chosen formatter fails, the command does not switch to another formatter that would ignore your settings. A formatter fails, for example, when your `pyproject.toml` cannot be read or a setting has the wrong type. The command prints the formatter's error and writes `changed_data` with `pprint`. It then asks you to fix the problem and format that migration yourself.

With black or ruff, the output has these properties:

- Your project's settings apply. black reads `[tool.black]` from the `pyproject.toml` at your project root. ruff reads the configuration that covers the migration file, such as your `pyproject.toml` or `ruff.toml`, even when that configuration excludes migrations. The list follows your line length and quote style.
- Every change and every key gets its own line. Each dictionary and list ends in a trailing comma, which keeps the formatter from joining the items onto one line. The command turns the magic trailing comma on even when your configuration turns it off.
- Keys are sorted, as earlier versions of the command wrote them.

The command formats only `changed_data` this way. It writes the rest of the migration, including the copied functions, in its own layout.

### Fill In Missing Change Records

[`sync_group_changes`](/v3/reference/api/py/vueda.user.management.commands.sync_group_changes/Command.md) reads `changed_data` from the group migrations in your project's apps. It creates a `GroupChange` row for each change that has none, and prints how many it created. The `vueda_user` migration `0005_sync_group_changes` runs it once. You can run it again at any time:

```console
python manage.py sync_group_changes
```

## Updating Existing Group Migrations

A group migration runs without updates, because it carries its own copy of the code. Run [`updategroupmigrations`](/v3/reference/api/py/vueda.user.management.commands.updategroupmigrations/Command.md) when a VUEDA release changes that code and you want existing group migrations to use the new version.

```console
python manage.py updategroupmigrations
```

Add `--dry-run` to list the files that the command would update without writing them.

```console
python manage.py updategroupmigrations --dry-run
```

The command finds group migrations in every installed app by their marker comment. In each file it:

- Replaces each copied function, and `GroupChangeTypes`, with the current version, matched by name. It adds any that the file lacks.
- Updates the import lines that it recognizes, and leaves other imports in place.
- Keeps `changed_data` and the `Migration` class. In the class's `operations` list, it renames references to functions that earlier releases named differently.

Code outside the copied functions stays as it is. The command overwrites edits inside a copied function the next time it runs. To add behavior to a group migration, write a separate function and add it to the `operations` list.

An environment that already applied a migration does not run it again. An update affects only rollbacks and environments that have not applied the migration. Running the command when nothing has changed rewrites the same code.
