What makes a route public
Authentication is applied byAuthorizationMiddleware, 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
AddViewMiddleware 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
A public form or JSON endpoint
A form posts to a handler of its own. Validate everything, and addCaptchaMiddleware 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 withExceptionMiddleware:
Receiving webhooks
Providers post to an endpoint you publish, so verify every request: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-jwtif 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, aframe-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:Access-Control-Allow-Origin: * with credentials.