Skip to content

Security and deployment ​

Island updates use your application's authorization, middleware, and CSRF protection. Signed snapshots protect state integrity; they do not grant access or hide data.

Application middleware remains authoritative ​

The plugin's default POST route is /_interact/update, named interact:update. It does not rerun the controller action that initially rendered an island.

Apply CakePHP's body parser, CSRF, and appropriate authentication middleware to update requests. The demo uses a test-only session identity; production applications must use their own authentication integration.

For separate administrative/public scopes, disable the plugin's default routes and connect the update controller within your existing route scopes:

php
use Interact\InteractPlugin;

$this->addPlugin(InteractPlugin::class, ['routes' => false]);
php
use Cake\Routing\RouteBuilder;

$routes->scope('/admin', function (RouteBuilder $routes): void {
    // Apply your application's existing administrative middleware here.
    $routes->post('/_interact/update', [
        'plugin' => 'Interact',
        'controller' => 'Update',
        'action' => 'update',
        'interactScope' => 'admin',
    ], 'admin:interact:update');
});

Configure that view's Interact helper with scope: 'admin' and the URL generated by CakePHP's routing API for the named administrative route. The initial scope and update route's interactScope must match. Updating a snapshot through another scope is rejected.

Override authorize(IslandContext $context) to check current resource access on every request. Treat action arguments as untrusted resource identifiers and authorize their use. Signing a record ID does not permanently authorize it.

Custom endpoints ​

Use the shared Interact\Http\UpdateHandler service when an island endpoint should belong to your application controller. CakePHP injects it into the action; returning its Response avoids automatic template rendering.

php
use Cake\Http\Response;
use Cake\Routing\Attribute\Post;
use Interact\Http\UpdateHandler;

#[Post(
    '/islands/update',
    name: 'app:interact',
    defaults: ['interactScope' => 'default'],
)]
public function update(UpdateHandler $interact): Response
{
    return $interact->handle(
        $this->getRequest(),
        $this->getResponse(),
    );
}

The interactScope route default marks the endpoint early enough for middleware exceptions to receive Interact's safe JSON errors. Set it in server route configuration. The handler processes ordinary updates and file transfers through the same lifecycle and preserves the supplied response's application headers and cookies. Expected failures become JSON; unexpected failures retain normal CakePHP logging.

Point the rendering controller's helper at the named route:

php
// Import Cake\Routing\Router in this controller.
$this->viewBuilder()->addHelper('Interact', [
    'className' => 'Interact.Interact',
    'endpoint' => Router::url(['_name' => 'app:interact']),
    'scope' => 'default',
]);

The helper and route must declare the same scope. Apply the application's normal middleware to the custom action. You can disable the plugin's default route when you only use application-owned endpoints; the service remains available.

Tested custom endpoint implementation
php
<?php
declare(strict_types=1);

namespace TestApp\Controller;

use Cake\Http\Response;
use Cake\Routing\Attribute\Get;
use Cake\Routing\Attribute\Post;
use Cake\Routing\Router;
use Interact\Http\UpdateHandler;

/**
 * Hosts the upload example and demonstrates an application-owned island endpoint.
 *
 * The shared handler processes both updates and transfers. Route defaults declare
 * the scope early enough for middleware exceptions to retain island JSON feedback.
 */
class UploadsController extends AppController
{
    /**
     * Select this controller's named endpoint once for all mounted examples.
     */
    public function initialize(): void
    {
        parent::initialize();
        $this->viewBuilder()->addHelper('Interact', [
            'className' => 'Interact.Interact',
            'endpoint' => Router::url(['_name' => 'demo:uploads:update']),
            'scope' => 'uploads-demo',
        ]);
    }

    /**
     * Render the public document-upload demonstration through a normal action template.
     */
    #[Get('/uploads', name: 'demo:uploads')]
    public function index(): void
    {
    }

    /**
     * Delegate JSON and multipart handling through CakePHP's action dependency injection.
     */
    #[Post('/uploads/update', name: 'demo:uploads:update', defaults: ['interactScope' => 'uploads-demo'])]
    public function update(UpdateHandler $interact): Response
    {
        return $interact->handle($this->getRequest(), $this->getResponse());
    }
}

Snapshots ​

Snapshots are signed, session-bound, scope-bound JSON with a 30-minute default lifetime. The default signing service uses CakePHP's security salt with domain separation. Replace SnapshotCodec through the application container to configure a dedicated key or lifetime. Use a strong secret and keep it out of source control.

Snapshots are visible to the browser. More transported props and state mean larger requests; computed viewData() does not add its results to the snapshot. Keep large lists and entities on the server. Snapshots provide integrity rather than encryption or single-use guarantees. Replaying a valid snapshot is possible until expiry. An application requiring idempotent payments or other durable operations must implement that guarantee in its business layer.

A failed connection does not establish whether the server performed an action. Interact does not automatically retry actions. Disposing an island or aborting fetch does not cancel server execution.

Do not cache personalized island HTML across sessions. Update responses use Cache-Control: no-store.

Markup and CSP ​

Escape application text and attribute values with CakePHP's helpers/functions. Returned HTML comes from trusted application templates; the plugin is not an HTML sanitizer.

The standalone runtime uses Alpine's CSP build and requires no unsafe-eval. Load scripts according to the application's CSP and nonce policy. Do not load the standalone bundle alongside an existing Alpine instance; use npm integration instead.

Binding, nesting, and transport payload sizes/depths are bounded. Prototype-related state keys and arbitrary PHP object restoration are rejected. Expired snapshots require a fresh mount, not an automatic replay of an action.

For safe error responses and middleware integration, see failures and recovery.

Redirect destinations must be validated by the application when they originate from user input. HTTP(S) external redirects are supported. Download URLs are same-origin and their endpoints must independently authorize file access; an island response is not an access token. See redirects and downloads.

Enhanced navigation ​

Page visits use ordinary CakePHP GET routes and run their normal middleware and authorization. Compatible document metadata guides browser replacement; it is not an authorization capability. Every destination island mounts and authorizes, and retained islands still authorize every update.

Navigation does not replace the original document's HTTP security policy. Changed session, CSP, or runtime compatibility triggers native navigation. Nonces are not rewritten. Keep the document middleware outside security-header middleware and account for headers added by other plugins or reverse proxies. See navigation.