plandex-ai/plandex · error

no access to plan

Error message

no access to plan

What it means

This 401 is returned by authorizePlan when ValidatePlanAccess succeeds but returns a nil plan, meaning the user is not a member of (or has no access to) the plan within their organization. Unlike 814, the lookup worked; the result was 'no access'. Plans are scoped by membership plus org, so cross-org or non-member access is rejected.

Source

Thrown at app/server/handlers/auth_helpers.go:682

	}

	return true
}

func authorizePlan(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {
	log.Println("authorizing plan")

	plan, err := db.ValidatePlanAccess(planId, auth.User.Id, auth.OrgId)

	if err != nil {
		log.Printf("error validating plan membership: %v\n", err)
		http.Error(w, "error validating plan membership", http.StatusInternalServerError)
		return nil
	}

	if plan == nil {
		log.Println("user doesn't have access the plan")
		http.Error(w, "no access to plan", http.StatusUnauthorized)
		return nil
	}

	return plan
}

func authorizePlanUpdate(w http.ResponseWriter, planId string, auth *types.ServerAuth) *db.Plan {
	plan := authorizePlan(w, planId, auth)

	if plan == nil {
		return nil
	}

	if plan.OwnerId != auth.User.Id && !auth.HasPermission(shared.PermissionUpdateAnyPlan) {
		log.Println("User does not have permission to update plan")
		http.Error(w, "User does not have permission to update plan", http.StatusForbidden)
		return nil
	}

View on GitHub (pinned to e2d772072e)

Solutions

  1. Confirm the authenticated user has been added as a member of the plan
  2. Verify the planId belongs to the same organization as the auth token
  3. Have the plan owner add the user to the plan, or use an account with access
  4. Re-fetch the plan list for the current org and use a valid planId

Example fix

// before
await api.createBranch({ planId: urlPlanId });
// after
const plans = await api.listPlans();
if (!plans.some(p => p.id === urlPlanId)) {
  throw new Error('You do not have access to this plan; ask the owner to add you as a member');
}
await api.createBranch({ planId: urlPlanId });
Defensive patterns

Strategy: validation

Validate before calling

async function ensurePlanAccess(planId) {
  const plans = await api.listPlans();
  if (!plans.some(p => p.id === planId)) {
    throw new Error(`No access to plan ${planId}; ask the owner to add you as a member`);
  }
}

Type guard

function isNoPlanAccess(res) {
  return res.status === 401;
}

Try / catch

try {
  return await api.getPlan(planId);
} catch (e) {
  if (e.status === 401 && /no access to plan/.test(e.body)) {
    notifyUser('You are not a member of this plan');
    redirect('/plans');
    return null;
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling plan-scoped endpoints (update/delete/rename/archive via the authorizePlan* wrappers, ListBranchesHandler, CreateBranchHandler) with a planId the user was never added to, a plan in another org, or a plan removed while membership rows were deleted.

Common situations: Sharing a plan URL with a teammate who has no membership; switching orgs and reusing an old plan ID; a plan membership revoked but a stale client session still shows the plan; guessing plan IDs in API scripts.

Understand the failure class

Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.

Related errors


AI-assisted analysis of plandex-ai/plandex@e2d772072e (2026-09-05). Data as JSON: /api/errors/d0cd37a0e217a4f8. Report an issue: GitHub.