Configuration
The package ships with Maniaba\CodeIgniterSse\Config\Sse. Create an
application config class only when defaults need to be changed:
<?php
declare(strict_types=1);
namespace Config;
use Maniaba\CodeIgniterSse\Config\Sse as BaseSse;
final class Sse extends BaseSse
{
public string $channelPrefix = 'storefront:sse:';
public int $heartbeatInterval = 15;
public int $maxConnectionSeconds = 300;
public array $redis = [
'host' => '127.0.0.1',
'port' => 6379,
'database' => 2,
];
}
HTTP route
| Property | Default | Purpose |
|---|---|---|
route['enabled'] |
true |
Register the package route through CI4 discovery. |
route['path'] |
sse |
Route path without a leading slash. |
route['name'] |
sse.stream |
Name used by CI4 route lookup. |
route['controller'] |
SseController::class |
Controller class used by the route. |
route['method'] |
stream |
Controller method used by the route. |
route['filters'] |
[] |
CI4 filter aliases applied to the route. |
route['options'] |
[] |
Additional CI4 route options. |
requireAcceptHeader |
true |
Require Accept: text/event-stream for PHP-streamed brokers. |
Example:
final class Sse extends BaseSse
{
public array $route = [
'path' => 'live/events',
'name' => 'sse.stream',
'filters' => ['auth', 'sse-concurrency'],
'options' => [
'priority' => 100,
],
];
}
Both names in this example are application-defined CI4 filter aliases.
Set route['enabled'] = false when the application registers the SSE endpoint
manually in app/Config/Routes.php.
Private streams should use both:
- an application authentication filter;
- a rate/concurrency filter keyed by authenticated subject and, where useful, client address.
The second filter should cap simultaneously open streams and abusive reconnect
churn without rejecting the expected reconnect after
maxConnectionSeconds. Channel authorization remains mandatory even when an
auth route filter is present.
When route auto-registration is disabled, the application can register its own route explicitly:
$routes->get(
'live/events',
'\\' . \Maniaba\CodeIgniterSse\HTTP\SseController::class . '::stream',
['filter' => 'auth|sse-concurrency'],
);
SseRoutes::register() honors route['enabled'], so it is intended for automatic
package discovery rather than bypassing a disabled route. Keep the request
method GET. Direct frontend adapters open EventSource on this route. The
Mercure frontend adapter first requests JSON authorization from this route and
then connects directly to the authorized Hub URL.
Stream behavior
| Property | Default | Purpose |
|---|---|---|
retryMilliseconds |
3000 |
SSE reconnect delay hint sent to the browser. |
heartbeatInterval |
15 |
Seconds between heartbeat comments while idle. |
maxConnectionSeconds |
300 |
Finite lifetime of one HTTP stream. |
maxChannelsPerConnection |
20 |
Maximum unique requested logical channels. The raw channels query input is also bounded from this value before splitting. |
emitConnectedEvent |
true |
Send sse.connected after opening the stream. |
The browser automatically opens a new stream after the server reaches
maxConnectionSeconds. A finite lifetime releases PHP workers, rotates Redis
subscriptions, and allows deployments to drain old processes.
Keep heartbeatInterval lower than the shortest idle timeout between the
application and browser. A heartbeat is an SSE comment and does not invoke
application message handlers.
Debug toolbar
When CodeIgniter Debug Toolbar is enabled, the package adds an SSE Events
collector through module discovery. It lists SSE publish calls triggered during
the current HTTP request.
The collector records metadata only:
- channel;
- event name;
- event ID;
- publish status;
- payload size in bytes;
- top-level data keys;
- publisher class or error message.
It does not display payload values, because event data can contain user data or other sensitive fields.
| Property | Default | Purpose |
|---|---|---|
toolbar['enabled'] |
true |
Enable publisher tracing during web debug requests. |
toolbar['brokers'] |
['*'] |
Broker names tracked by the toolbar, or * for all configured brokers. |
toolbar['maxEvents'] |
100 |
Maximum number of publish records kept for one request. |
Track only selected brokers:
final class Sse extends BaseSse
{
public array $toolbar = [
'brokers' => ['redis', 'custom'],
];
}
Disable tracing when the application does not need it:
final class Sse extends BaseSse
{
public array $toolbar = [
'enabled' => false,
];
}
Broker
| Property | Default | Purpose |
|---|---|---|
broker |
redis |
Active key from brokers. |
brokers |
built-in Redis, Mercure, memory, null definitions | Broker adapter or broker adapter factory map. |
channelPrefix |
app:sse: |
Prefix added to logical Redis channels. |
Choosing Redis or Mercure
Use Redis when the deployment has a controlled number of simultaneous users
and PHP-FPM capacity is sized for one open SSE response per connected browser.
This is the simplest topology: CodeIgniter subscribes to Redis Pub/Sub and
streams events directly from the /sse route.
Use Mercure when the application expects larger concurrency, more active users, or frequent reconnect bursts. With Mercure, PHP only authorizes the requested channels and publishes events to the Hub. The browser's long-lived EventSource connection is held by Mercure, not by a PHP worker. This keeps normal page and API requests from competing with idle SSE streams for the same PHP-FPM pool.
Recommended rule of thumb:
| Deployment shape | Recommended broker | Reason |
|---|---|---|
| Local development, small internal tools, bounded traffic | redis |
Fewer moving parts and easy debugging. |
| Multi-instance PHP application with moderate SSE usage | redis or mercure |
Redis works if PHP worker capacity is planned; Mercure reduces worker pressure. |
| Public application, many concurrent users, multi-tenant dashboards, high reconnect churn | mercure |
The Hub is built to own long-lived SSE connections and lets PHP handle short requests. |
Publishing code does not change when switching between Redis and Mercure. The
server-side broker and the browser adapter must match.
memory is useful for isolated tests in one PHP process. It cannot carry a
message between separate HTTP requests or workers. null is a message sink:
it discards published events but an enabled SSE route still keeps each stream
open until disconnect or maximum lifetime. Set route['enabled'] = false to
disable the HTTP endpoint.
mercure has a publisher but no PHP subscriber. Its broker adapter supplies a
Mercure HTTP subscription endpoint, so the package route issues subscriber
authorization instead of creating a PHP stream. See Mercure Hub
for the complete configuration.
Do not use an empty shared prefix when several applications publish to the same Redis instance.
Redis Pub/Sub is global across numbered Redis databases. redis['database']
therefore does not separate SSE traffic; channelPrefix is the isolation
boundary.
Custom brokers are added by registering a new key in brokers. A broker
definition must provide exactly one of factory or adapter:
factory:BrokerAdapterFactoryInterface, callable returning one, or class name implementing it;adapter:BrokerAdapterInterface, callable returning one, or class name implementing it.
The adapter owns publishing and the HTTP subscription endpoint. If the broker
can stream through PHP, implement SubscriberAwareBrokerAdapterInterface too.
See Custom brokers for implementation examples and
troubleshooting.
use App\Sse\CustomBrokerAdapterFactory;
use Maniaba\CodeIgniterSse\Broker\InMemory\InMemoryBrokerAdapterFactory;
use Maniaba\CodeIgniterSse\Broker\Mercure\MercureBrokerAdapterFactory;
use Maniaba\CodeIgniterSse\Broker\Null\NullBrokerAdapterFactory;
use Maniaba\CodeIgniterSse\Broker\Redis\RedisBrokerAdapterFactory;
final class Sse extends BaseSse
{
public string $broker = 'custom';
public array $brokers = [
'redis' => [
'factory' => RedisBrokerAdapterFactory::class,
],
'mercure' => [
'factory' => MercureBrokerAdapterFactory::class,
],
'memory' => [
'factory' => InMemoryBrokerAdapterFactory::class,
'shared' => true,
],
'null' => [
'factory' => NullBrokerAdapterFactory::class,
'shared' => true,
],
'custom' => [
'factory' => CustomBrokerAdapterFactory::class,
],
];
}
Mercure
Mercure options live in one array, parallel to Redis. A complete Mercure setup has four parts:
- run a Mercure Hub with publisher and subscriber JWT keys;
- select
public string $broker = 'mercure';; - configure
hubUrl,publicHubUrl, keys, topic prefix, and cookie options; - use
MercureSseAdapterin the browser client.
Example application config:
public string $broker = 'mercure';
public array $mercure = [
'hubUrl' => 'http://mercure/.well-known/mercure',
'publicHubUrl' => 'https://app.example.com/.well-known/mercure',
'topicPrefix' => 'urn:storefront:sse:',
'private' => true,
'publisherKey' => null,
'subscriberKey' => null,
// Defaults to topicPrefix . '{channel}'.
'publisherTopicSelectors' => null,
'allowGlobalPublisherSelector' => false,
'cookie' => [
'name' => 'mercureAuthorization',
'domain' => '',
'path' => '/.well-known/mercure',
'secure' => true,
'httpOnly' => true,
'sameSite' => 'Lax',
],
];
Use a literal absolute IRI prefix such as urn:herceg:sse:. Prefixes
containing wildcard or URI-template characters, such as
https://example.com/{topic}, are rejected because they can broaden Mercure
topic selectors.
Nested .env overrides are supported:
sse.mercure.publisherKey = replace-with-the-hub-publisher-key
sse.mercure.subscriberKey = replace-with-the-hub-subscriber-key
For local plain HTTP only, also set:
sse.mercure.cookie.secure = false
Keep Hub signing keys out of source control. HMAC JWT signing keys must be at least 32 bytes. The package validates Mercure configuration when the Mercure adapter factory builds the broker. The full key reference and deployment guidance are in Mercure Hub.
Redis connection
Redis setup has five parts:
- run Redis and make it reachable from PHP;
- keep
public string $broker = 'redis';; - set an application-specific
channelPrefix; - configure the
redisconnection array or.envoverrides; - use
RedisSseAdapterin the browser client.
Minimal local configuration:
public string $broker = 'redis';
public string $channelPrefix = 'myapp:sse:';
public array $redis = [
'host' => '127.0.0.1',
'port' => 6379,
'database' => 0,
];
Equivalent .env overrides:
sse.broker = redis
sse.channelPrefix = myapp:sse:
sse.redis.host = 127.0.0.1
sse.redis.port = 6379
sse.redis.database = 0
Then verify the connection:
redis-cli ping
php spark sse:health-check
For the browser client, use the Redis adapter:
import {
RedisSseAdapter,
SseClient,
} from '/vendor/codeigniter4-sse/sse-client.js';
const live = new SseClient({
endpoint: '/sse',
adapter: new RedisSseAdapter(),
channels: ['public.news'],
});
When Redis is selected, /sse is the long-lived SSE response. Configure the
web server and PHP-FPM as described in
Streaming and deployment.
All Redis options live under one config property:
public array $redis = [
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
'username' => null,
'password' => null,
'database' => 0,
'connectTimeout' => 2.5,
'readTimeout' => 2.5,
'pollInterval' => 1.0,
'pingInterval' => 15.0,
'reconnectAttempts' => 2,
'reconnectDelayMilliseconds' => 250,
'deduplicationCapacity' => 1024,
'maxPayloadBytes' => 1_048_576,
'maxResponseElements' => 1024,
'maxResponseDepth' => 8,
'allowPatternSubscriptions' => false,
'clientName' => null,
'streamContext' => [],
];
| Array key | Default | Purpose |
|---|---|---|
scheme |
tcp |
PHP stream scheme, normally tcp or tls. |
host |
127.0.0.1 |
Redis hostname or IP address. |
port |
6379 |
Redis port. |
username |
null |
ACL username. |
password |
null |
Password or ACL secret. |
database |
0 |
Database selected after authentication; it does not isolate Pub/Sub channels. |
connectTimeout |
2.5 |
Socket connect timeout in seconds. |
readTimeout |
2.5 |
Timeout for complete Redis command/handshake reads. |
pollInterval |
1.0 |
Maximum idle poll before stream checks run. |
pingInterval |
15.0 |
Verify an otherwise idle subscribed socket with Redis PING. |
reconnectAttempts |
2 |
Publisher/subscriber transport reconnect attempts. |
reconnectDelayMilliseconds |
250 |
Delay between Redis reconnect attempts. |
deduplicationCapacity |
1024 |
Recent channel/event ID pairs retained to suppress duplicates from overlapping exact and pattern subscriptions. |
maxPayloadBytes |
1048576 |
Maximum serialized event or inbound RESP bulk string size. |
maxResponseElements |
1024 |
Maximum elements accepted in one RESP array. |
maxResponseDepth |
8 |
Maximum accepted RESP nesting depth. |
allowPatternSubscriptions |
false |
Permit Redis-style pattern requests. |
clientName |
null |
Optional Redis connection name. |
streamContext |
[] |
PHP stream context options, usually under ssl. |
Publisher and subscriber connections are always separate. The subscriber uses the poll interval to return control for heartbeats, client disconnect checks, and maximum lifetime enforcement. When no Redis frames arrive, periodic PING checks detect half-open sockets and trigger the bounded reconnect path.
The payload and RESP limits protect long-running PHP workers from unexpectedly
large broker frames. Raise them only when the application intentionally sends
larger events. When pattern subscriptions are enabled, channelPrefix must
not contain Redis glob metacharacters.
Redis ACL
Supply both username and password when ACL authentication is enabled:
public array $redis = [
'username' => 'sse_app',
'password' => 'change-me',
];
Use environment secrets rather than committing credentials to the config file.
The Redis identity needs only the commands used by the adapter, including
connection setup, PUBLISH, and channel subscription commands. Restrict its
accessible channel patterns to the configured application prefix where Redis
ACL policy allows it.
TLS
Configure TLS and certificate validation in the PHP config class:
final class Sse extends BaseSse
{
public array $redis = [
'scheme' => 'tls',
'port' => 6380,
'streamContext' => [
'ssl' => [
'verify_peer' => true,
'verify_peer_name' => true,
'cafile' => '/etc/ssl/certs/redis-ca.pem',
],
],
];
}
Do not disable peer verification in production.
Authorization
| Property | Default | Purpose |
|---|---|---|
channelAuthorizer |
PublicChannelAuthorizer::class |
Authorizes each requested logical channel. |
userResolver |
NullUserResolver::class |
Resolves the current authenticated user or null. |
The secure default allows only public.*. Set both class names when private
channels are used:
public string $channelAuthorizer = \App\Sse\ChannelAuthorizer::class;
public string $userResolver = \App\Sse\UserResolver::class;
Applications using CodeIgniter Shield can use the packaged resolver:
public string $userResolver = \Maniaba\CodeIgniterSse\Authorization\ShieldUserResolver::class;
Both classes must implement their package contracts. See Channels and authorization.
CORS and credentials
| Property | Default | Purpose |
|---|---|---|
allowedOrigins |
[] |
Exact cross-origin frontend origins. |
withCredentials |
true |
Allow credentialed CORS responses. |
rejectCrossSiteBootstrap |
true |
Reject Mercure authorization bootstrap requests with Sec-Fetch-Site: cross-site. |
Same-origin requests do not require an allowlist entry because browsers omit
the cross-origin Origin case this policy is intended to control.
For a separate frontend origin:
public array $allowedOrigins = [
'https://app.example.com',
];
public bool $withCredentials = true;
When credentials are enabled, * is not a valid allowed origin. Cookie domain,
Secure, and SameSite settings must also permit the browser to send the
session cookie.
rejectCrossSiteBootstrap adds a Fetch Metadata check for browsers that send
Sec-Fetch-Site. It is an extra defense for the Mercure authorization request
that sets the HttpOnly subscriber cookie; it does not replace CORS, session
authentication, or channel authorization. Disable it only for trusted legacy or
non-browser clients that cannot send Fetch Metadata headers.