Skip to main content
Not every route a plugin adds is for signed-in users. A landing page, a contact form, a status endpoint or a webhook receiver has to answer anyone who asks. Those routes skip the authorization step, so the checks it normally provides become your responsibility.
Read the security checklist before you ship a public route. Most serious plugin vulnerabilities come from public routes that trust their input.

What makes a route public

Authentication is applied by AuthorizationMiddleware, and only the base classes for signed-in areas add it. A handler is public when neither it nor its base class carries that middleware. AbstractRequestHandler is the right starting point for almost every public route. It’s what the core’s login, signup and policy pages extend, and it adds:

A public page

Add ViewMiddleware to render a Twig template:
src/RequestHandlers/PricingView.php
The template can extend the same minimal layout the core’s public pages use, so it works with any theme:
templates/compare.twig
To build a page into the active theme’s own design instead, see Theme custom pages.

A public form or JSON endpoint

A form posts to a handler of its own. Validate everything, and add CaptchaMiddleware to anything that creates records or sends email:
src/RequestHandlers/SubmitInquiry.php
To send a captcha token, include the core snippet inside your form. It renders nothing when reCAPTCHA is off, and otherwise fills a captcha-token field for you:
Keep public endpoints outside /api and /admin, which belong to the core. Under /api and /admin/api, UserMiddleware also ignores the session cookie and only accepts an API key or a bearer token.

Errors

ExceptionMiddleware decides what a failed request returns: A public page that should show “not found” can return a ViewResponse with StatusCode::NOT_FOUND rather than throw, since a thrown NotFoundException becomes JSON.

Routes without a base class

A handler that extends no base class gets no middleware at all: no error handling, no body parsing, no user and no locale. Use it when the standard stack gets in the way, such as for a webhook receiver that must read the raw body. Then add back only what you need, and always start with ExceptionMiddleware:

Receiving webhooks

Providers post to an endpoint you publish, so verify every request:
Read the raw body for the signature, not the parsed one, and make handling idempotent: providers retry, and the same event can arrive twice. Payment gateways have a dedicated webhook route and interface. See Payment webhooks.

Identifying visitors who aren’t users

Some public features need to recognize the same visitor across requests without an Aikeedo account, such as a guest’s saved progress. Aikeedo has no built-in visitor identity for this, so issue your own token:
  • Sign it with a secret your plugin owns, scope it to your feature, and give it a short lifetime. The app ships firebase/php-jwt if you want JWTs.
  • Verify it in your own middleware, and attach what it proves to the request.
  • Only issue a token for something the caller proved. Creating a fresh identity is safe; returning a token for an identifier the caller merely supplied lets anyone impersonate anyone.

Embeds

A page meant to be framed on other sites, such as a chat or feedback widget, is a public page with a few extra requirements: a loader script, a frame-ancestors policy, postMessage between the frames, and per-site visitor tokens. The embeddable widget guide walks through all of it.

Security checklist

Validate every input. Public bodies, query strings and route parameters are attacker-controlled. Check types, lengths and formats before using them.
Verify ownership of every ID. When a route loads a record by ID, confirm the caller is allowed to see it. Loading by ID alone lets anyone read other people’s data by guessing.
Don’t rely on the optional user for access. A missing UserEntity on a public route is normal, not an error. If a route needs a user, it isn’t public: extend an authenticated base class.
Protect writes. Add CaptchaMiddleware to forms and to anything that creates records or sends email.
Limit abuse. Aikeedo has no built-in rate limiter. Cap requests per IP in your plugin or at the web server, and reject oversized payloads early.
Guard paid work. Anything that spends AI credits must name the workspace that pays and check its balance first. Never let an anonymous request spend credits without an owner.
Treat headers as hints. Origin, Referer and custom headers can be forged by any client. Enforce access with tokens, signatures and server-side allowlists.
Respond in constant shapes. Don’t reveal whether a record or email exists through different messages or timings.
Return the minimum. Never expose internal IDs, workspace details or configuration from a public route.

CORS

Browsers only need CORS headers when another site’s page calls your endpoint directly. Forms and pages on your own domain don’t. When you do need them, reflect only origins you’ve allowlisted, for example from a plugin setting:
Never reflect an arbitrary origin, and never combine Access-Control-Allow-Origin: * with credentials.