Repository navigation
Description
Summary
On self-hosted makeplane/plane v1.4.2, the REST API exposes a path called POST /api/v1/workspaces/{slug}/projects/{project_id}/project-views/, but the handler only persists per-user UI state into ProjectMember.view_props (and friends). It does not create a named, project-wide saved view, and it is not reachable with API-key auth.
This is the same underlying product gap as #8816 (Public API support for creating and managing Views), but the angle here is the misleadingly-named /project-views/ endpoint in the current self-host master / v1.4.2: the path looks like a public REST endpoint for project views, but the implementation is per-user state, not a saved view, and it is not part of the documented /api/v1/ public surface.
Repro on a running self-host instance (HTTP against http://localhost/)
The following curl probes all return HTTP 404 against a healthy self-host v1.4.2 stack with valid auth cookies/API keys. They cover the natural guesses for a "create named project-wide saved view" endpoint:
# Natural guesses that all return 404 in self-host v1.4.2:
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
"http://localhost/api/v1/workspaces/test-slug/projects/00000000-0000-0000-0000-000000000001/project-views/" \
-X POST
# -> HTTP 404
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
"http://localhost/api/v1/workspaces/test-slug/projects/00000000-0000-0000-0000-000000000001/views/" \
-X POST
# -> HTTP 404
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
"http://localhost/api/v1/workspaces/test-slug/projects/00000000-0000-0000-0000-000000000001/saved-views/" \
-X POST
# -> HTTP 404
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
"http://localhost/api/v1/workspaces/test-slug/projects/00000000-0000-0000-0000-000000000001/project-saved-views/" \
-X POST
# -> HTTP 404
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
"http://localhost/api/v1/workspaces/test-slug/projects/00000000-0000-0000-0000-000000000001/named-views/" \
-X POST
# -> HTTP 404
curl -sS -o /dev/null -w 'HTTP %{http_code}\n' \
"http://localhost/api/v1/workspaces/test-slug/projects/00000000-0000-0000-0000-000000000001/project-named-views/" \
-X POST
# -> HTTP 404(404 here is the auth-wall + no-such-route response; the request is well-formed but the routes are not registered, or they are registered but gated to session-cookie auth and not API-key.)
What does exist in self-host v1.4.2 source (read from the running plane-app-api image, tag v1.4.2)
plane/app/views/project/base.py, class ProjectUserViewsEndpoint:
class ProjectUserViewsEndpoint(BaseAPIView):
def post(self, request, slug, project_id):
project = Project.objects.get(pk=project_id, workspace__slug=slug)
project_member = ProjectMember.objects.filter(
member=request.user, project=project, is_active=True
).first()
if project_member is None:
return Response({"error": "Forbidden"}, status=status.HTTP_403_FORBIDDEN)
view_props = project_member.view_props
default_props = project_member.default_props
preferences = project_member.preferences
sort_order = project_member.sort_order
project_member.view_props = request.data.get("view_props", view_props)
project_member.default_props = request.data.get("default_props", default_props)
project_member.preferences = request.data.get("preferences", preferences)
project_member.sort_order = request.data.get("sort_order", sort_order)
project_member.save()
return Response(status=status.HTTP_204_NO_CONTENT)This handler accepts view_props, default_props, preferences, sort_order. It does not accept name, description, access, query, filters, display_filters, display_properties. It writes to ProjectMember (per-user state), not to IssueView. The ProjectMember columns are JSONField defaults like {view_id, list, kanban, calendar, gantt, spreadsheet, …} — UI column visibility per layout, not a saved filter.
Routing — plane/app/urls/project.py:
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/project-views/",
ProjectUserViewsEndpoint.as_view(),
name="project-view",
),This is the only /project-views/ route in the entire plane codebase. The name in the URL is project-view, but the semantics are ProjectMember.view_props, not a saved view. There is no symmetric GET.
Comparison with the SaaS feature surface
Plane SaaS documents a public POST /api/v2/workspaces/{slug}/projects/{project_id}/views/ endpoint under scope projects.views:write. Body fields include name (≤255 chars), access, display_filters, display_properties, filters, description, is_locked, logo_props, pql_filters, sort_order. Response fields include id, name, description, access, query, filters, display_filters, display_properties, owned_by_id, created_at, updated_at, etc. The backing model is IssueView (db_table issue_views) with columns name, description, query, filters, display_filters, display_properties, rich_filters, access, sort_order, logo_props, owned_by, is_locked, archived_at.
Self-host v1.4.2 ships the same IssueView model in plane/db/models/view.py. The data layer is therefore present. The problem is the public-API surface:
- self-host:
/api/v1/.../project-views/→ per-userProjectMember.view_propssave (no name, no description, no query/filters, no public route for theIssueViewmodel). - SaaS:
/api/v2/.../views/→IssueViewCRUD under API-key auth, with full filter/layout payload.
So self-host users on /api/v1/ cannot script the creation of a named, project-wide saved view (IssueView) through the public API. The only ways to create a saved view in self-host v1.4.2 are:
- Click through the React UI (which calls internal session-cookie endpoints).
- Hit the internal session-cookie
IssueViewViewSet.createdirectly (not documented as public, not API-key-scoped, not stable).
Why this matters for self-host users
The parent feature request #8816 captures the general ask. The specific concrete problem this surfaces is for ops automations on a self-host install:
- Teams want to script recurring views (e.g. "waiting on", "due this week", "needs review") from CI, repo automation, or other Plane-adjacent tooling.
- The only
/api/v1/endpoint whose name suggests this —/project-views/— is a footgun: it does not create a saved view, it just persists per-user UI state. There is no public-route way to create aIssueViewrecord via API key.
Suggested fix
Promote the existing IssueViewViewSet (already present in plane/app/views/view/base.py and wired in plane/app/urls/views.py) to the /api/v1/ public surface (or expose a /api/v1/.../views/ POST under API-key auth), with the same payload shape documented for SaaS v2. Until that lands, the misleadingly-named /project-views/ POST should either:
- be moved/renamed to make its per-user semantics explicit (e.g.
/project-view-state/or/project-member-view-props/), or - be documented as per-user state in the public API reference.
Related
- Public API support for creating and managing Views #8816 — Public API support for creating and managing Views (broader feature request)
- Pages API not exposed in public REST API on self-hosted instances #8986 — Pages API not exposed in public REST API on self-hosted instances (sibling gap in the same area)
Activity
feat(api): expose project and workspace saved views in public REST API (