Skip to content
Merged
35 changes: 21 additions & 14 deletions docs/multiuser/user_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,13 @@ As a regular user, you can:
- ✅ View your own generation queue
- ✅ Customize your UI preferences (theme, hotkeys, etc.)
- ✅ View available models (read-only access to Model Manager)
- ✅ Access shared boards (based on permissions granted to you) (FUTURE FEATURE)
- ✅ Access workflows marked as public (FUTURE FEATURE)
- ✅ View shared and public boards created by other users
- ✅ View and use workflows marked as shared by other users

You cannot:

- ❌ Add, delete, or modify models
- ❌ View or modify other users' boards, images, or workflows
- ❌ View or modify other users' private boards, images, or workflows
- ❌ Manage user accounts
- ❌ Access system configuration
- ❌ View or cancel other users' generation tasks
Expand All @@ -173,7 +173,7 @@ Administrators have all regular user capabilities, plus:
- ✅ Full model management (add, delete, configure models)
- ✅ Create and manage user accounts
- ✅ View and manage all users' generation queues
- ✅ Create and manage shared boards (FUTURE FEATURE)
- ✅ View and manage all users' boards, images, and workflows (including system-owned legacy content)
- ✅ Access system configuration
- ✅ Grant or revoke admin privileges

Expand All @@ -183,23 +183,30 @@ Administrators have all regular user capabilities, plus:

### Image Boards

In multi-user model, Image Boards work as before. Each user can create an unlimited number of boards and organize their images and assets as they see fit. Boards are private: you cannot see a board owned by a different user.
In multi-user mode, each user can create an unlimited number of boards and organize their images and assets as they see fit. Boards have three visibility levels:

!!! tip "Shared Boards"
InvokeAI 6.13 will add support for creating public boards that are accessible to all users.
- **Private** (default): Only you (and administrators) can see and modify the board.
- **Shared**: All users can view the board and its contents, but only you (and administrators) can modify it (rename, archive, delete, or add/remove images).
- **Public**: All users can view the board. Only you (and administrators) can modify the board's structure (rename, archive, delete).

The Administrator can see all users Image Boards and their contents.
To change a board's visibility, right-click on the board and select the desired visibility option.

### Going From Multi-User to Single-User mode
Administrators can see and manage all users' image boards and their contents regardless of visibility settings.

### Going From Multi-User to Single-User Mode

If an InvokeAI instance was in multiuser mode and then restarted in single user mode (by setting `multiuser: false` in the configuration file), all users' boards will be consolidated in one place. Any images that were in "Uncategorized" will be merged together into a single Uncategorized board. If, at a later date, the server is restarted in multi-user mode, the boards and images will be separated and restored to their owners.

### Workflows

In the current released version (6.12) workflows are always shared among users. Any workflow that you create will be visible to other users and vice-versa, and there is no protection against one user modifying another user's workflow.
Each user has their own private workflow library. Workflows you create are visible only to you by default.

You can share a workflow with other users by marking it as **shared** (public). Shared workflows appear in all users' workflow libraries and can be opened by anyone, but only the owner (or an administrator) can modify or delete them.

To share a workflow, open it and use the sharing controls to toggle its public/shared status.

!!! tip "Private and Shared Workflows"
InvokeAI 6.13 will provide the ability to create private and shared workflows. A private workflow can only be viewed by the user who created it. At any time, however, the user can designate the workflow *shared*, in which case it can be opened on a read-only basis by all logged-in users.
!!! warning "Preexisting workflows after enabling multi-user mode"
When you enable multi-user mode for the first time on an existing InvokeAI installation, all workflows that were created before multi-user mode was activated will appear in the **shared workflows** section. These preexisting workflows are owned by the internal "system" account and are visible to all users. Administrators can edit or delete these shared legacy workflows. Regular users can view and use them but cannot modify them.


### The Generation Queue
Expand Down Expand Up @@ -330,11 +337,11 @@ These settings are stored per-user and won't affect other users.

### Can other users see my images?

No, unless you add them to a shared board (FUTURE FEATURE). All your personal boards and images are private.
Not unless you change your board's visibility to "shared" or "public". All personal boards and images are private by default.

### Can I share my workflows with others?

Not directly. Ask your administrator to mark workflows as public if you want to share them.
Yes. You can mark any workflow as shared (public), which makes it visible to all users. Other users can view and use shared workflows, but only you or an administrator can modify or delete them.

### How long do sessions last?

Expand Down
20 changes: 13 additions & 7 deletions invokeai/app/api/routers/workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,13 @@ async def create_workflow(
workflow: WorkflowWithoutID = Body(description="The workflow to create", embed=True),
) -> WorkflowRecordDTO:
"""Creates a workflow"""
return ApiDependencies.invoker.services.workflow_records.create(workflow=workflow, user_id=current_user.user_id)
# In single-user mode, workflows are owned by 'system' and shared by default so all legacy/single-user
# workflows remain visible. In multiuser mode, workflows are private to the creator by default.
config = ApiDependencies.invoker.services.configuration
is_public = not config.multiuser
return ApiDependencies.invoker.services.workflow_records.create(
workflow=workflow, user_id=current_user.user_id, is_public=is_public
)


@workflows_router.get(
Expand All @@ -144,10 +150,10 @@ async def list_workflows(
"""Gets a page of workflows"""
config = ApiDependencies.invoker.services.configuration

# In multiuser mode, scope user-category workflows to the current user unless fetching shared workflows
# In multiuser mode, scope user-category workflows to the current user unless fetching shared workflows.
# Admins skip the user_id filter so they can see and manage all workflows including system-owned ones.
user_id_filter: Optional[str] = None
if config.multiuser:
# Only filter 'user' category results by user_id when not explicitly listing public workflows
if config.multiuser and not current_user.is_admin:
has_user_category = not categories or WorkflowCategory.User in categories
if has_user_category and is_public is not True:
user_id_filter = current_user.user_id
Expand Down Expand Up @@ -320,7 +326,7 @@ async def get_all_tags(
"""Gets all unique tags from workflows"""
config = ApiDependencies.invoker.services.configuration
user_id_filter: Optional[str] = None
if config.multiuser:
if config.multiuser and not current_user.is_admin:
has_user_category = not categories or WorkflowCategory.User in categories
if has_user_category and is_public is not True:
user_id_filter = current_user.user_id
Expand All @@ -341,7 +347,7 @@ async def get_counts_by_tag(
"""Counts workflows by tag"""
config = ApiDependencies.invoker.services.configuration
user_id_filter: Optional[str] = None
if config.multiuser:
if config.multiuser and not current_user.is_admin:
has_user_category = not categories or WorkflowCategory.User in categories
if has_user_category and is_public is not True:
user_id_filter = current_user.user_id
Expand All @@ -361,7 +367,7 @@ async def counts_by_category(
"""Counts workflows by category"""
config = ApiDependencies.invoker.services.configuration
user_id_filter: Optional[str] = None
if config.multiuser:
if config.multiuser and not current_user.is_admin:
has_user_category = WorkflowCategory.User in categories
if has_user_category and is_public is not True:
user_id_filter = current_user.user_id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ def _update_workflow_library_table(self, cursor: sqlite3.Cursor) -> None:
if "is_public" not in columns:
cursor.execute("ALTER TABLE workflow_library ADD COLUMN is_public BOOLEAN NOT NULL DEFAULT FALSE;")
cursor.execute("CREATE INDEX IF NOT EXISTS idx_workflow_library_is_public ON workflow_library(is_public);")
cursor.execute(
"UPDATE workflow_library SET is_public = TRUE WHERE user_id = 'system';"
) # one-time fix for legacy workflows


def build_migration_28() -> Migration:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,12 @@ def get(self, workflow_id: str) -> WorkflowRecordDTO:
pass

@abstractmethod
def create(self, workflow: WorkflowWithoutID, user_id: str = WORKFLOW_LIBRARY_DEFAULT_USER_ID) -> WorkflowRecordDTO:
def create(
self,
workflow: WorkflowWithoutID,
user_id: str = WORKFLOW_LIBRARY_DEFAULT_USER_ID,
is_public: bool = False,
) -> WorkflowRecordDTO:
"""Creates a workflow."""
pass

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ def get(self, workflow_id: str) -> WorkflowRecordDTO:
raise WorkflowNotFoundError(f"Workflow with id {workflow_id} not found")
return WorkflowRecordDTO.from_dict(dict(row))

def create(self, workflow: WorkflowWithoutID, user_id: str = WORKFLOW_LIBRARY_DEFAULT_USER_ID) -> WorkflowRecordDTO:
def create(
self,
workflow: WorkflowWithoutID,
user_id: str = WORKFLOW_LIBRARY_DEFAULT_USER_ID,
is_public: bool = False,
) -> WorkflowRecordDTO:
if workflow.meta.category is WorkflowCategory.Default:
raise ValueError("Default workflows cannot be created via this method")

Expand All @@ -59,11 +64,12 @@ def create(self, workflow: WorkflowWithoutID, user_id: str = WORKFLOW_LIBRARY_DE
INSERT OR IGNORE INTO workflow_library (
workflow_id,
workflow,
user_id
user_id,
is_public
)
VALUES (?, ?, ?);
VALUES (?, ?, ?, ?);
""",
(workflow_with_id.id, workflow_with_id.model_dump_json(), user_id),
(workflow_with_id.id, workflow_with_id.model_dump_json(), user_id, is_public),
)
return self.get(workflow_with_id.id)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,22 @@ import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowCounterClockwiseBold, PiStarFill } from 'react-icons/pi';
import { useDispatch } from 'react-redux';
import { useGetSetupStatusQuery } from 'services/api/endpoints/auth';
import { useGetAllTagsQuery, useGetCountsByTagQuery } from 'services/api/endpoints/workflows';

export const WorkflowLibrarySideNav = () => {
const { t } = useTranslation();
const { data: setupStatus } = useGetSetupStatusQuery();
const multiuserEnabled = setupStatus?.multiuser_enabled ?? false;

return (
<Flex h="full" minH={0} overflow="hidden" flexDir="column" w={64} gap={0}>
<Flex flexDir="column" w="full" pb={2} gap={2}>
<WorkflowLibraryViewButton view="recent">{t('workflows.recentlyOpened')}</WorkflowLibraryViewButton>
<YourWorkflowsButton />
<WorkflowLibraryViewButton view="shared">{t('workflows.sharedWorkflows')}</WorkflowLibraryViewButton>
{multiuserEnabled && (
<WorkflowLibraryViewButton view="shared">{t('workflows.sharedWorkflows')}</WorkflowLibraryViewButton>
)}
</Flex>
<Flex h="full" minH={0} overflow="hidden" flexDir="column">
<BrowseWorkflowsButton />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import InvokeLogo from 'public/assets/images/invoke-symbol-wht-lrg.svg';
import { type ChangeEvent, memo, type MouseEvent, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiImage } from 'react-icons/pi';
import { useGetSetupStatusQuery } from 'services/api/endpoints/auth';
import { useUpdateWorkflowIsPublicMutation } from 'services/api/endpoints/workflows';
import type { WorkflowRecordListItemWithThumbnailDTO } from 'services/api/types';

Expand Down Expand Up @@ -36,6 +37,7 @@ export const WorkflowListItem = memo(({ workflow }: { workflow: WorkflowRecordLi
const dispatch = useAppDispatch();
const workflowId = useAppSelector(selectWorkflowId);
const currentUser = useAppSelector(selectCurrentUser);
const { data: setupStatus } = useGetSetupStatusQuery();
const loadWorkflowWithDialog = useLoadWorkflowWithDialog();

const isActive = useMemo(() => {
Expand All @@ -47,8 +49,12 @@ export const WorkflowListItem = memo(({ workflow }: { workflow: WorkflowRecordLi
}, [currentUser, workflow.user_id]);

const canEditOrDelete = useMemo(() => {
// In single-user (legacy) mode, all workflows are editable — no concept of ownership.
if (!setupStatus?.multiuser_enabled) {
return true;
}
return isOwner || (currentUser?.is_admin ?? false);
}, [isOwner, currentUser]);
}, [setupStatus?.multiuser_enabled, isOwner, currentUser]);

const tags = useMemo(() => {
if (!workflow.tags) {
Expand Down Expand Up @@ -113,7 +119,7 @@ export const WorkflowListItem = memo(({ workflow }: { workflow: WorkflowRecordLi
{t('workflows.opened')}
</Badge>
)}
{workflow.is_public && workflow.category !== 'default' && (
{setupStatus?.multiuser_enabled && workflow.is_public && workflow.category !== 'default' && (
<Badge
color="invokeGreen.400"
borderColor="invokeGreen.700"
Expand Down Expand Up @@ -160,7 +166,7 @@ export const WorkflowListItem = memo(({ workflow }: { workflow: WorkflowRecordLi
</Text>
)}
<Spacer />
{isOwner && <ShareWorkflowToggle workflow={workflow} />}
{setupStatus?.multiuser_enabled && canEditOrDelete && <ShareWorkflowToggle workflow={workflow} />}
{workflow.category === 'default' && <ViewWorkflow workflowId={workflow.workflow_id} />}
{workflow.category !== 'default' && (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { t } from 'i18next';
import { atom, computed } from 'nanostores';
import type { ChangeEvent, RefObject } from 'react';
import { memo, useCallback, useRef, useState } from 'react';
import { useGetSetupStatusQuery } from 'services/api/endpoints/auth';
import { useUpdateWorkflowIsPublicMutation } from 'services/api/endpoints/workflows';
import { assert } from 'tsafe';

Expand Down Expand Up @@ -90,6 +91,8 @@ const Content = memo(({ workflow, cancelRef }: { workflow: WorkflowV3; cancelRef
return '';
});
const [isPublic, setIsPublic] = useState(false);
const { data: setupStatus } = useGetSetupStatusQuery();
const multiuserEnabled = setupStatus?.multiuser_enabled ?? false;

const { createNewWorkflow } = useCreateLibraryWorkflow();
const [updateIsPublic] = useUpdateWorkflowIsPublicMutation();
Expand Down Expand Up @@ -143,10 +146,12 @@ const Content = memo(({ workflow, cancelRef }: { workflow: WorkflowV3; cancelRef
<FormLabel mt="2">{t('workflows.workflowName')}</FormLabel>
<Flex flexDir="column" width="full" gap="2">
<Input ref={inputRef} value={name} onChange={onChange} placeholder={t('workflows.workflowName')} />
<Flex alignItems="center" gap={2}>
<Checkbox isChecked={isPublic} onChange={onChangeIsPublic} />
<FormLabel mb={0}>{t('workflows.shareWorkflow')}</FormLabel>
</Flex>
{multiuserEnabled && (
<Flex alignItems="center" gap={2}>
<Checkbox isChecked={isPublic} onChange={onChangeIsPublic} />
<FormLabel mb={0}>{t('workflows.shareWorkflow')}</FormLabel>
</Flex>
)}
</Flex>
</FormControl>
</AlertDialogBody>
Expand Down
Loading
Loading