Appendix: Configuration Reference

Every key Kinetis and its packages read from the environment, the exact mechanics behind a named connection, and why .env sits outside the AOT cache. For the task-first path — .env loading, typed Config access, and when a named connection is practically relevant — see Configuration.

Named connections

Any storage technology — Redis, a SQL database, and so on — can be configured more than once under a name, alongside the usual unnamed default connection. The name is inserted, uppercased, as a segment right after the first underscore in the key:

REDIS_HOST=cache.internal         # default
REDIS_CACHE2_HOST=cache2.internal # named "cache2"

DB_HOST=db.internal               # default
DB_DB2_HOST=db2.internal          # named "db2"

Config::scopedKey(string $key, string $connection = 'default'): string is the shared helper every technology’s connection builder — see Database for kinetis/database-bridge’s ConnectionFactory and RedisSimpleCache — uses to compute which exact variable to read:

Config::scopedKey('REDIS_HOST');                        // 'REDIS_HOST'
Config::scopedKey('REDIS_HOST', 'cache2');              // 'REDIS_CACHE2_HOST'
Config::scopedKey('FILESYSTEM_S3_BUCKET', 'archive');   // 'FILESYSTEM_ARCHIVE_S3_BUCKET'

The segment always lands after the first underscore, so a key with a longer prefix splits at that same point: FILESYSTEM_S3_BUCKET becomes FILESYSTEM_ARCHIVE_S3_BUCKET, not FILESYSTEM_S3_ARCHIVE_BUCKET.

'default' resolves to the plain, unprefixed key — a connection you never name behaves exactly as if this feature did not exist.

Every package bootstrap reads its own selector unscoped — DB_CONNECTION, QUEUE_CONNECTION, FILESYSTEM_DRIVER, MAILER_DSN, SEARCH_OPENSEARCH_HOST, SEARCH_ELASTICSEARCH_HOST, SESSION_DRIVER, BROADCAST_DRIVER — and wires the default connection alone. A named connection is never resolved automatically: build one explicitly in bootstrap.php and register it under an id of your own, or construct it where it is needed:

.env
DB_REPORTING_CONNECTION=pgsql
DB_REPORTING_HOST=reporting.internal
DB_REPORTING_NAME=reports
DB_REPORTING_USER=reports
DB_REPORTING_PASSWORD=secret
bootstrap.php
use Kinetis\DatabaseBridge\ConnectionFactory;

return static function (AppScope $app, Config $config): void {
    // kinetis/database-bridge already binds the default DB_* connection
    // under its own dialect contract.
    $app->instance('db.reporting', ConnectionFactory::fromConfig($config, 'reporting'));
};

See Configuration’s “Named connections” for the short version, and Bootstrapping’s “Binding an application service” for the ordinary, non-database case.

Overriding the default Config registration

AppScope::boot() registers Config::fromEnvironment() unless you’ve already registered your own — useful for a test double, or a fully computed configuration that doesn’t come from the real environment at all:

use Kinetis\Config\Config;
use Kinetis\Container\AppScope;

$app = new AppScope();
$app->instance(Config::class, new Config(['DB_HOST' => 'test-db']));
$app->boot(); // your registration above is kept, not overwritten

This follows the same registration-before-boot discipline any other AppScope service does — see Bootstrapping for registering an application service from bootstrap.php, and Appendix: Container Lifecycle for the full registration-lock rule.

.env loading mechanics

Every framework-owned entry point — Kinetis\Runtime\HttpStartup (the whole of an application’s public/index.php), bin/kinetis, and Kinetis\Testing\TestApplication (see Testing) — calls Kinetis\Config\EnvFile::safeLoad($projectRoot) unconditionally, before Kinetis\Runtime\AppEnvironment::detect(). APP_ENV itself may be defined for the first time in .env rather than already set in the real process environment, so detection has to run after the load.

The load writes through putenv(), $_ENV and $_SERVER, which is what puts a .env value in reach of the plain getenv() both Config::fromEnvironment() and AppEnvironment::detect() read. See Core Concepts for where this sits in HttpStartup’s full boot sequence.

Config and the AOT cache

Config and .env sit outside the AOT compilation Kinetis builds for production (see Caching & AOT Compilation). That cache’s value is being reproducible from source alone — delete it, rebuild it, get back the identical artifact. Environment variables break that: the process that ran bin/kinetis build and the one serving requests later can legitimately have different values injected into them. Baking .env into a compiled cache file would mean a changed value did nothing until someone rebuilt the cache.

Every key in one place

Everything Kinetis and its packages read from the environment, grouped by subsystem. Keys marked scoped follow the named-connection convention above — DB_HOST becomes DB_REPORTING_HOST for a connection named reporting. Application-defined keys (a JWT_SECRET your own bootstrap reads via Config::required(), for instance) are yours to invent and are not listed here.

The Default column uses three forms. A literal value — including (empty) for a list — is what the key falls back to. means the key has no default; what leaving it out does is in the Purpose cell. (unset: …) marks an activation gate: the package or subsystem stays inert until the key is set, and the cell names what you get until then. (required for X) is the opposite — once X is selected the key has no default left, and reading it throws naming the key.

Application (core)

Key

Default

Purpose

APP_ENV

production

development — the exact name, ignoring case — selects live discovery; unset or any other name selects the AOT cache (see Caching & AOT Compilation).

OPENAPI_ENVIRONMENTS

Comma-separated APP_ENV values where /openapi.json and /openapi are served, matched ignoring case and surrounding space against the raw APP_ENV string (so a staging deployment can name itself). An absent APP_ENV matches production. Unset means neither path is reachable anywhere (see Routing & Validation).

MAX_BODY_SIZE

2097152

Request-body cap in bytes, enforced against declared Content-Length and actual bytes read (see Middleware); must be positive.

TRUSTED_PROXIES

(empty)

Comma-separated addresses and CIDR ranges naming the deployment’s own edge. Empty trusts no peer, and an entry that is neither an address nor a CIDR range is refused at startup. Runtime Adapters owns the forwarded-header policy this configures.

Security headers (core)

Each key below carries its header’s value verbatim, except the three HSTS keys, which compose Strict-Transport-Security between them. For the verbatim keys, an empty value omits the header and the literal off — in any case — omits it too, so a header with a built-in default can be switched off without sending an invalid value. See Middleware.

Key

Default

Purpose

SECURITY_FRAME_OPTIONS

DENY

X-Frame-Options value.

SECURITY_REFERRER_POLICY

strict-origin-when-cross-origin

Referrer-Policy value.

SECURITY_CSP

Content-Security-Policy value.

SECURITY_PERMISSIONS_POLICY

Permissions-Policy value.

SECURITY_HSTS_MAX_AGE

HSTS max-age in seconds. Unset means the header is not sent; an explicit 0 sends max-age=0, RFC 6797’s withdrawal of a cached policy, and carries no directives; a negative value throws.

SECURITY_HSTS_INCLUDE_SUBDOMAINS

true

Appends includeSubDomains to a positive max-age.

SECURITY_HSTS_PRELOAD

false

Appends preload to a positive max-age.

SECURITY_COOP

Cross-Origin-Opener-Policy value.

SECURITY_CORP

Cross-Origin-Resource-Policy value.

SECURITY_COEP

Cross-Origin-Embedder-Policy value.

X-Content-Type-Options: nosniff is always sent and is not configurable. A header already present on the response is never replaced, so one route can set its own policy and keep it.

Discovery restriction

All optional; comma-separated sub-paths relative to each PSR-4 base directory, for large applications that want a bounded scan (see CLI).

Key

Restricts the scan for

ROUTE_DISCOVERY_PATHS

HTTP controllers (#[Get]/#[Post]/…)

COMMAND_DISCOVERY_PATHS

CLI commands (#[Command])

MCP_DISCOVERY_PATHS

MCP tools and resources (#[McpTool]/#[McpResource]) — read by kinetis/mcp

MIDDLEWARE_DISCOVERY_PATHS

Global middleware (#[AsGlobalMiddleware]) and middleware groups (#[AsMiddlewareGroup])

LISTENER_DISCOVERY_PATHS

Event listeners (#[Listener])

BROADCAST_CHANNEL_DISCOVERY_PATHS

Channel authorization callbacks (#[BroadcastChannel]) — read by kinetis/broadcasting

These are read through getenv() at discovery time rather than through Config, so a .env value works and a TestApplication config override does not.

Database (kinetis/database-bridge) — all scoped

Read by kinetis/database-bridge’s ConnectionFactory. DB_CONNECTION is what the bridge’s package bootstrap gates on, and it gates on the key being absent, not blank: DB_CONNECTION= activates the package and then fails on the dialect check.

Key

Default

Purpose

DB_CONNECTION

(unset: no database)

mysql or pgsql.

DB_HOST

127.0.0.1

Server host.

DB_PORT

3306 / 5432

Per dialect; must be a valid TCP port (1–65535).

DB_NAME

app

Database name.

DB_USER

app

User.

DB_PASSWORD

(required for mysql/pgsql)

Password. An empty value is a valid empty password, not a missing key.

DB_DRIVER

auto

auto (native when frankenphp_handle_request() exists or RR_MODE=http is set, PDO everywhere else — Lambda included), native, or pdo.

DB_CHARSET

utf8mb4 on MySQL

Connection charset; identifier characters only.

DB_COLLATION

MySQL collation (SET NAMES ... COLLATE); identifier characters only. Rejected by the Postgres drivers.

DB_SSLMODE

disable/require/verify-ca/verify-full on every driver; libpq additionally accepts allow/prefer, which the MySQL drivers reject as having no opportunistic TLS.

DB_SSL_CA

CA bundle path for the verify modes.

DB_SSL_CERT

Client certificate for mutual TLS; requires DB_SSL_KEY and a TLS DB_SSLMODE.

DB_SSL_KEY

Client private key; requires DB_SSL_CERT. Postgres requires 0600 permissions.

DB_CONNECT_TIMEOUT

Seconds; must be a positive integer. Unset means no client-side connect timeout.

DB_APP_NAME

Postgres application_name. Rejected by the MySQL drivers.

DB_COMPRESSION

MySQL protocol compression. On for 1/true/on/yes (case-insensitively); any other value is off. Rejected by the Postgres drivers.

DB_MAX_CONNECTIONS

8

Async drivers’ pool width; must be at least 1 — per worker thread under FrankenPHP, per worker process under RoadRunner (see Performance tuning).

DB_WARM_CONNECTIONS

0

Connections opened at boot instead of first use — load-bearing for the mysqli driver under worker mode; must not be negative.

Redis (kinetis/cache-redis, kinetis/queue-redis) — all scoped

REDIS_CLUSTER, REDIS_URL and REDIS_HOST are checked in that order, and the first one set decides how the connection is addressed: REDIS_CLUSTER=true selects the cluster client, otherwise REDIS_URL wins over REDIS_HOST. A REDIS_URL carrying a password or a database index wins over REDIS_PASSWORD and REDIS_DATABASE too. With none of the three set, Redis is off — see Database for what CacheInterface binds to then.

Key

Default

Purpose

REDIS_CLUSTER

false

Use Redis Cluster mode. Supported by the cache; kinetis/queue-redis is single-node only.

REDIS_CLUSTER_SEEDS

(required for cluster)

Comma-separated seed nodes for Cluster bootstrap — host:port, or [ipv6-address]:port for an IPv6 node.

REDIS_URL

Full redis:// URI.

REDIS_HOST

Server host.

REDIS_PORT

6379

Port; must be a valid TCP port (1–65535).

REDIS_PASSWORD

Password.

REDIS_DATABASE

0

Database index (single-node only; Cluster has no SELECT); must not be negative.

REDIS_TIMEOUT

5

Operation budget, seconds — connect, reply, and cluster redirects together; must be positive.

REDIS_TLS

false

Connect over TLS.

REDIS_TLS_VERIFY_PEER

true

Verify the server certificate, discovered and redirected cluster nodes included.

REDIS_TLS_CA_FILE

CA certificate for verification.

REDIS_CACHE_NAMESPACE

default

Key namespace the cache owns; letters, digits, underscores and dashes.

Queue (kinetis/queue + backend packages)

Read by kinetis queue:work and kinetis/queue’s package bootstrap; the backend-specific keys are scoped by QUEUE_CONNECTION_NAME. Setting QUEUE_CONNECTION does two things beyond selecting a backend. It decides which capabilities the bound backend has beyond QueueInterfaceredis, sql, and rabbitmq can clear a queue and sqs cannot, so kinetis queue:clear refuses under QUEUE_CONNECTION=sqs, see Queue’s “Clearing is a separate capability”. And it is what makes a listener marked Kinetis\Events\ShouldQueue actually queue: the bootstrap binds ListenerInvokerInterface to the queued invoker, where leaving QUEUE_CONNECTION unset leaves core’s inline default in place (see Events).

Key

Default

Purpose

QUEUE_CONNECTION

(unset: no queue)

redis (needs kinetis/queue-redis), sql (needs kinetis/queue-sql), sqs (needs kinetis/queue-sqs), or rabbitmq (needs kinetis/queue-rabbitmq). Gates on the key being absent, not blank.

QUEUE_CONNECTION_NAME

default

Which named REDIS_*/DB_* block the worker uses.

QUEUE_MAX_ATTEMPTS

0

Worker-level default attempts cap (0 = no retries, and must not be negative); a job’s own push(maxAttempts: ...) wins. Bounds the attempt count only — retries are immediate, with no backoff (see Queue).

QUEUE_POLL_TIMEOUT

5

Seconds queue:work waits per poll; must be a positive integer, since a persistent worker needs a bounded wait to periodically check for a shutdown signal.

QUEUE_VISIBILITY_TIMEOUT_SECONDS

300

kinetis/queue-redis and kinetis/queue-sql: reclaim a crashed worker’s reserved job after this long. Must be a positive integer; set it above the slowest job you expect.

QUEUE_SQS_REGION

(required for sqs)

AWS region.

QUEUE_SQS_ENDPOINT

SQS-compatible endpoint (LocalStack). One origin — scheme, host, optional port — and nothing else; unset leaves AsyncAws its regional table and refuses an ambient AWS_ENDPOINT_URL.

QUEUE_SQS_PLAINTEXT

false

Allows an http:// value for QUEUE_SQS_ENDPOINT.

QUEUE_SQS_TIMEOUT

30

Seconds bounding each SQS request and each credential lookup; must be positive, and set above the longest long poll the application issues (at most a five-second slice).

QUEUE_SQS_QUEUE_PREFIX

Queue-name prefix for shared AWS accounts.

QUEUE_RABBITMQ_URL

(required for rabbitmq)

amqp:// URI.

QUEUE_RABBITMQ_QUEUE_PREFIX

Queue-name prefix.

AWS credentials are never read from Config — the SQS (and S3) clients use AWS’s own default credential provider chain.

Migrations (kinetis/migrations)

Read by the migrate* commands, which connect through the same DB_* keys as kinetis/database-bridge.

Key

Default

Purpose

MIGRATE_CONNECTION_NAME

default

Which named DB_* block to migrate; the --connection=<name> flag wins over it.

File storage (kinetis/storage + kinetis/storage-s3)

The gate below is on the unscoped key: installing kinetis/storage alone registers nothing, and FilesystemOperator binds only once FILESYSTEM_DRIVER is set. FilesystemFactory::fromConfig(), called directly, bypasses that gate — it reads the key scoped and falls back to local. Every other key here is scoped.

Key

Default

Purpose

FILESYSTEM_DRIVER

(unset: no filesystem)

local, or s3 (needs kinetis/storage-s3).

FILESYSTEM_ROOT

(required for local)

Local disk root path. Must be non-empty; / is valid.

FILESYSTEM_S3_BUCKET

(required for s3)

Bucket name.

FILESYSTEM_S3_REGION

(required for s3)

AWS region.

FILESYSTEM_S3_PREFIX

Key prefix.

FILESYSTEM_S3_ENDPOINT

S3-compatible endpoint (MinIO) — one origin, addressed path-style.

FILESYSTEM_S3_PLAINTEXT

false

Allow an http:// endpoint.

FILESYSTEM_S3_TIMEOUT

60

Seconds per S3 request — connect, idle and transfer. Must be positive.

Mail (kinetis/mailer) — scoped

The gate below is on the unscoped MAILER_DSN; both keys are scoped when MailerFactory::fromConfig() is called for a named connection.

Key

Default

Purpose

MAILER_DSN

(unset: no mailer)

Symfony Mailer transport DSN (smtp://..., sendgrid+api://..., …).

MAILER_TIMEOUT

30

Seconds per API send — idle and total. Must be positive. SMTP ignores it and carries its own timeouts from the DSN.

Search (kinetis/search-opensearch, kinetis/search-elasticsearch) — scoped

Both engine packages read the same keys under their own prefix — SEARCH_OPENSEARCH_ or SEARCH_ELASTICSEARCH_, written SEARCH_..._ below. The gate is on the unscoped ..._HOST; every key is scoped when the engine’s fromConfig() is called for a named connection.

Key

Default

Purpose

SEARCH_..._HOST

(unset: no client)

One http(s)://host[:port] origin. No userinfo, path, query or fragment.

SEARCH_..._PLAINTEXT

false

Accept an http origin.

SEARCH_..._TIMEOUT

30

Seconds per request — idle and total. Must be positive.

SEARCH_..._MAX_RESPONSE_BYTES

8388608

Largest response body accepted. Must be positive.

SEARCH_..._USERNAME

Basic-auth user.

SEARCH_..._PASSWORD

Basic-auth password.

SEARCH_..._VERIFY_PEER

true

Verify the server certificate.

SEARCH_ELASTICSEARCH_API_KEY

Elasticsearch only: an API key, instead of a username and password.

SEARCH_ELASTICSEARCH_API_KEY_ID

Elasticsearch only: the key’s id, when it is held separately from its secret.

Sessions (kinetis/session)

Key

Default

Purpose

SESSION_DRIVER

(unset: no store)

file, redis, or sql. Any other value throws at boot.

SESSION_LIFETIME

7200

Seconds a session stays readable from its last write; must be positive. Every write refreshes the browser cookie’s Max-Age alongside the backend’s own storage TTL, so the two never drift apart.

SESSION_COOKIE

kinetis_session

Cookie name. A __Host-/__Secure- prefix requires SESSION_SECURE.

SESSION_SAMESITE

Lax

Cookie SameSite attribute: Strict, Lax, or None. None requires SESSION_SECURE.

SESSION_SECURE

true

Cookie Secure attribute — false only for non-TLS local development.

SESSION_FILES_DIR

<system temp>/kinetis-sessions

The file driver’s directory.

The redis and sql drivers each need their own package installed and configured, and say which when they are not; Sessions & CSRF lists what each one requires.

MCP (kinetis/mcp)

MCP_DISCOVERY_PATHS, in the discovery table above, also belongs to this package.

Key

Default

Purpose

MCP_ALLOWED_ORIGINS

(empty)

Comma-separated exact Origin values allowed on /mcp, trimmed. Empty rejects with 403 any request that sends an Origin header at all; requests without one (CLI clients, server-to-server) always pass.

MCP_HTTP_PUBLIC

false

Serves /mcp to unauthenticated callers. Left at false, a request no mcp-group middleware registered a CurrentUserInterface for is answered 401 before anything is dispatched — see Model Context Protocol (MCP)’s “Securing the HTTP transport”.

Telemetry (kinetis/telemetry)

Key

Default

Purpose

OTEL_EXPORTER_OTLP_ENDPOINT

(unset: tracing off)

Collector’s OTLP/HTTP base URL. Without it the provider is a no-op, so telemetry is opt-in per environment rather than per install.

OTEL_SERVICE_NAME

kinetis

The service.name resource attribute.

OTEL_EXPORTER_OTLP_HEADERS

Export-request headers, key=value,key2=value2 — where a hosted backend’s auth goes.

OTEL_TRACES_SAMPLER

parentbased_always_on

Exactly one of always_on, always_off, traceidratio, parentbased_always_on, parentbased_always_off, parentbased_traceidratio. Any other name throws.

OTEL_TRACES_SAMPLER_ARG

1.0

Sampling ratio for the two traceidratio samplers; must be between 0 and 1.

Broadcasting (kinetis/broadcasting)

BROADCAST_DRIVER and BROADCAST_ALLOWED_ORIGINS are read unscoped. The Pusher connection keys are scoped, and the package bootstrap builds only the default connection.

Key

Default

Purpose

BROADCAST_DRIVER

null

null (NullBroadcaster, a silent no-op) or pusher — the wire protocol Soketi, Laravel Reverb, and Pusher’s hosted service all share. Any other value throws at boot.

BROADCAST_APP_ID

(required for pusher)

App ID.

BROADCAST_KEY

(required for pusher)

App key — also the value a browser client subscribes with.

BROADCAST_SECRET

(required for pusher)

App secret, used to sign trigger requests and private/presence channel authorizations.

BROADCAST_HOST

api.pusherapp.com

Server host the backend publishes to.

BROADCAST_PORT

443

Server port; must be a valid TCP port (1–65535).

BROADCAST_TLS

true

Connect over TLS.

BROADCAST_ALLOWED_ORIGINS

(empty)

Comma-separated exact Origin values this route’s own guard admits on POST /broadcasting/auth, on top of the request’s own origin, which always passes. Requests without an Origin header (server-side clients) pass too; any other origin is 403. Not a CORS policy: a cross-origin browser request must also be allowed by the global CorsMiddleware. Not connection-scoped — the endpoint is one route. See Broadcasting’s “Securing the endpoint”.

The keys above address the server your backend publishes to. Where the WebSocket server is reachable from a browser is a separate address, and no kinetis/broadcasting key holds it — the application owns that value and hands it to its own client code. The pingpong example reads it from its own BROADCAST_BROWSER_HOST/BROADCAST_BROWSER_PORT keys through Config, the same way any application-defined key works. A browser client needs that address and the app key, nothing else. See Broadcasting.

See also

  • Configuration — the task-first guide: .env loading, typed Config access, and named connections.

  • Bootstrapping — registering an application service or global middleware from bootstrap.php.

  • Appendix: Container LifecycleAppScope’s registration-lock discipline, and how RequestScope delegates to a Config it never explicitly registered itself.

  • Caching & AOT Compilation — the AOT cache’s reproducible-from-source invariant.

  • Appendix: System Layout — the Kinetis\Config namespace in the full system map.