Tracking Down a "Workspace Not Found" Bug in Dify

June 28, 2026 ยท Abhishek Mukherjee

← Back to portfolio

I spent a weekend digging into a bug in Dify, one of the larger open-source platforms for building LLM apps, and ended up sending a fix. The bug looked tiny from the outside but took a while to actually understand, which is usually the case with these things.

The Symptom

The report was simple. You remove a member from a workspace. Later you change your mind and invite them back. They get the invite email, click the link, log in, and instead of landing in the workspace they see:

"workspace not found, please contact system admin to invite you to join in a workspace."

So the invite clearly went through, the email was sent, but the user ended up with zero workspaces. That mismatch is what made it interesting. If the invite had failed loudly, nobody would have been confused. Instead it half-worked, which is worse.

Following the Data

Dify keeps accounts and workspace membership in two separate places. There is an Account record for the person, and a TenantAccountJoin row that ties that account to a specific workspace (a tenant) with a role.

When you remove an active member, the join row gets deleted but the account itself stays around. That part is intentional. The account might belong to other workspaces, and you don't want to nuke the whole user just because they left one team.

So after removal you have an account with no join row for that workspace. On login, Dify calls get_join_tenants to figure out which workspaces the user belongs to. No join row means it returns nothing, which is exactly the "workspace not found" message. The login side was behaving correctly. The bug had to be on the invite side.

The Actual Bug

In the re-invite path, the code that re-creates the workspace membership was guarded like this:

if not ta and (account.status == AccountStatus.PENDING or dify_config.RBAC_ENABLED):
    TenantService.create_tenant_member(tenant, account, session, tenant_join_role)

Read that condition carefully. It only re-adds the membership if the account is PENDING, or if RBAC happens to be enabled. A brand-new invited user is PENDING, so for them it worked fine. But a member who used to be active in the workspace and got removed is not pending anymore. Their account status is still active from their earlier life in the system.

So for a removed active member, with RBAC off, neither branch of that condition was true. The membership was never re-created. The invite email still went out because that part runs unconditionally. The user got an email promising access to a workspace they were never actually added back to.

The Fix

The status check was doing too much. The only thing that actually matters here is whether the account already has a join row for this workspace. If it doesn't, re-create it:

if not ta:
    TenantService.create_tenant_member(tenant, account, session, tenant_join_role)

The thing that made me comfortable removing the status check is that create_tenant_member is idempotent. If the account is still a member, ta is already set, so this block is skipped entirely, and an existing AccountAlreadyInTenantError guard further down still protects against double adds. So members who were never removed are completely unaffected. The change only touches the case that was broken: no join row means re-create the membership, regardless of status.

What I Took From It

The fix is one line. Finding it meant reading through how accounts, tenants, and join rows relate, and tracing the difference between a never-invited user and a removed-and-reinvited user, because the code treated them as the same thing when they aren't.

That is the part I keep coming back to with open source. The valuable work isn't writing the line. It's building enough of a mental model of someone else's system to know which line is wrong and why it's safe to change. Reading real codebases teaches that in a way tutorials never have for me.

← Back to portfolio