Skip to main content
Plugins can extend the part of Aikeedo that users see: pages under /app and JSON endpoints under /api. Both run inside a workspace, so every read and write has to be scoped to it.

A page under /app

src/RequestHandlers/NotesView.php
AppView prefixes /app, requires an authenticated user, enforces the email verification policy and renders ViewResponse through Twig. The example above is served at GET /app/notes.

The template

templates/notes.twig
See Frontend integration for the Alpine and asset side.

A workspace-scoped JSON endpoint

src/RequestHandlers/Api/ListNotesRequestHandler.php
With a shared base class for the group:
src/RequestHandlers/Api/NotesApi.php
Endpoints are then served under /api/notes, and they accept the same authentication as the rest of the User API: a session token from the app, or an API key. See the REST API overview.

The user and the workspace

UserMiddleware attaches both entities to the request:
  • The workspace is the caller’s current workspace, or the one named by the X-Workspace-Id header when the caller is a member of it.
  • The workspace attribute is set only when the user belongs to it, so treat a missing workspace as an error rather than a reason to fall back.

Access control

Scope every query by workspace, and verify ownership whenever an ID comes from the request.
src/AccessControls/NoteAccessControl.php
The core uses the same shape in Presentation\AccessControls\*, with Presentation\AccessControls\Permission for finer-grained checks. Two conventions worth copying:
  • A resource the caller may not see is a 404, not a 403.
  • A resource that exists but is withheld by the user’s plan is a 403.
Checking the parent resource isn’t enough for nested routes. On a route such as /api/notes/{noteId}/comments/{commentId}, verify that the comment belongs to that note and that the note belongs to the caller’s workspace. Skipping the second check lets anyone read another workspace’s data by guessing IDs.

Feature flags and plan limits

Gate your feature behind an option so administrators can turn it off:
Enforce per-plan limits yourself, reading the plan configuration from the workspace’s subscription. To expose your own per-plan settings in the admin panel, see Plan config extensions.

Write endpoints

  • Validate with Presentation\Validation\Validator::validateRequest().
  • Return StatusCode::CREATED with the created resource, or EmptyResponse with StatusCode::NO_CONTENT for deletes.
  • Changes are flushed at the end of the request, so persist through a repository and return.