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. A scoped key
can coincide with a default one: a SQL connection named app would read
its database name from DB_APP_NAME, which is also the default
connection’s Postgres application name. kinetis/orm refuses app as an
entity’s connection, and kinetis/migrations as a connection directory
or --connection value, for that reason. Both take connection names of
lowercase ASCII letters and digits starting with a letter, so no other
two connection names share a key.
Every package bootstrap except kinetis/queue’s reads its own selector
unscoped — DB_CONNECTION, FILESYSTEM_DRIVER, MAILER_DSN,
SEARCH_OPENSEARCH_HOST, SEARCH_ELASTICSEARCH_HOST, SESSION_DRIVER,
BROADCAST_DRIVER — and wires the default connection alone.
kinetis/queue’s bootstrap wires the one queue connection
QUEUE_CONNECTION_NAME names, default when unset, gated on that
connection’s own scoped QUEUE_CONNECTION selector;
queue:work --connection=<name> builds any other (see “Queue” below).
kinetis/queue-failed-jobs builds the one SQL connection
FAILED_JOBS_DB_CONNECTION_NAME names, default when unset (see
“Failed jobs” below). Beyond those, the one named connection resolved automatically is a SQL
connection an ORM entity names with #[Entity(connection: ...)]: with
kinetis/orm installed, kinetis/database-bridge builds it from its
scoped DB_* keys, or takes the application’s db.<name> binding, when
the ORM first needs it — see ORM’s “Entities on other
connections”. Any other named connection is built explicitly in
bootstrap.php and registered under an id of your own, or constructed
where it is needed:
DB_REPORTING_CONNECTION=pgsql
DB_REPORTING_HOST=reporting.internal
DB_REPORTING_NAME=reports
DB_REPORTING_USER=reports
DB_REPORTING_PASSWORD=secret
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 |
|---|---|---|
|
|
|
|
— |
Comma-separated |
|
|
Request-body cap in bytes, enforced against declared |
|
(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, and
the {nonce} placeholder in SECURITY_CSP. 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 |
|---|---|---|
|
|
|
|
|
|
|
— |
|
|
— |
|
|
— |
HSTS max-age in seconds. Unset means the header is not sent; an explicit |
|
|
Appends |
|
|
Appends |
|
— |
|
|
— |
|
|
— |
|
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 |
|---|---|
|
HTTP controllers ( |
|
CLI commands ( |
|
MCP tools and resources ( |
|
Global middleware ( |
|
Event listeners ( |
|
Channel authorization callbacks ( |
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. A named connection’s
DB_<NAME>_CONNECTION is the key the ORM wiring checks for an entity’s
connection before it reads any other.
Key |
Default |
Purpose |
|---|---|---|
|
(unset: no database) |
|
|
|
Server host. |
|
|
Per dialect; must be a valid TCP port (1–65535). |
|
|
Database name. |
|
|
User. |
|
(required for mysql/pgsql) |
Password. An empty value is a valid empty password, not a missing key. |
|
|
|
|
|
Connection charset; identifier characters only. |
|
— |
MySQL collation ( |
|
— |
|
|
— |
CA bundle path for the verify modes. |
|
— |
Client certificate for mutual TLS; requires |
|
— |
Client private key; requires |
|
— |
Seconds; must be a positive integer. Unset means no client-side connect timeout. |
|
— |
Postgres |
|
— |
MySQL protocol compression. On for |
|
|
Async drivers’ pool width; must be at least 1 — per worker thread under FrankenPHP, per worker process under RoadRunner (see Performance tuning). |
|
|
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 |
|---|---|---|
|
|
Use Redis Cluster mode. Supported by the cache; |
|
(required for cluster) |
Comma-separated seed nodes for Cluster bootstrap — |
|
— |
Full |
|
— |
Server host. |
|
|
Port; must be a valid TCP port (1–65535). |
|
— |
Password. |
|
|
Database index (single-node only; Cluster has no |
|
|
Operation budget, seconds — connect, reply, and cluster redirects together; must be positive. |
|
|
Connect over TLS. |
|
|
Verify the server certificate, discovered and redirected cluster nodes included. |
|
— |
CA certificate for verification. |
|
|
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.
Each queue connection has its own backend selector —
QUEUE_CONNECTION for default, QUEUE_<NAME>_CONNECTION for a named
one, with no fallback between them — and the backend-specific keys below
are scoped by the same name: QUEUE_REPORTS_CONNECTION=sqs reads
QUEUE_REPORTS_SQS_REGION. The bootstrap binds the connection
QUEUE_CONNECTION_NAME names; queue:work --connection=<name> runs any
connection. Appendix: Queue Contracts’s “Named connections” is the
contract. Setting the bound connection’s selector does two things
beyond selecting a backend. It decides which capabilities the bound
backend has beyond QueueInterface — redis, sql, and rabbitmq can
clear a queue and sqs cannot, so kinetis queue:clear refuses on an
sqs connection, 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 that
selector unset leaves core’s inline default in place (see Events).
Key |
Default |
Purpose |
|---|---|---|
|
(unset: no queue) |
The |
|
|
The connection the bootstrap binds to |
|
|
Worker-level default attempts cap ( |
|
|
Seconds the first retry of a failed job waits, doubling per attempt up to a fixed 15-minute ceiling: |
|
|
Seconds |
|
|
|
|
(required for sqs) |
AWS region. |
|
— |
SQS-compatible endpoint (LocalStack). One origin — scheme, host, optional port — and nothing else; unset leaves AsyncAws its regional table and refuses an ambient |
|
|
Allows an |
|
|
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-name prefix for shared AWS accounts. |
|
(required for rabbitmq) |
|
|
— |
Queue-name prefix. |
AWS credentials are never read from Config — the SQS (and S3) clients
use AWS’s own default credential provider chain.
Failed jobs (kinetis/queue-failed-jobs)#
Read when the failed-job store is first resolved — by queue:work
before it prints that it started, and by the queue:failed,
queue:retry, queue:forget and queue:prune commands. Installing the
package turns recording on; there is no on/off key.
Key |
Default |
Purpose |
|---|---|---|
|
|
The database connection the store uses: the |
Migrations (kinetis/migrations)#
Read by the migrate* commands, which connect through the same DB_*
keys as kinetis/database-bridge: the default block for the files in
migrations/, and DB_{NAME}_* for each connection directory
migrations/<name>/ (Migrations).
Key |
Default |
Purpose |
|---|---|---|
|
— |
When non-empty, narrows |
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 |
|---|---|---|
|
(unset: no filesystem) |
|
|
(required for local) |
Local disk root path. Must be non-empty; |
|
(required for s3) |
Bucket name. |
|
(required for s3) |
AWS region. |
|
— |
Key prefix. |
|
— |
S3-compatible endpoint (MinIO) — one origin, addressed path-style. |
|
|
Allow an |
|
|
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 |
|---|---|---|
|
(unset: no mailer) |
Symfony Mailer transport DSN ( |
|
|
Seconds per API send — idle and total. Must be positive. SMTP ignores it; Symfony instead inherits PHP’s per-operation |
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 |
|---|---|---|
|
(unset: no client) |
One |
|
|
Accept an |
|
|
Seconds per request — idle and total. Must be positive. |
|
|
Largest response body accepted. Must be positive. |
|
— |
Basic-auth user. |
|
— |
Basic-auth password. |
|
|
Verify the server certificate. |
|
— |
Elasticsearch only: an API key, instead of a username and password. |
|
— |
Elasticsearch only: the key’s id, when it is held separately from its secret. |
Sessions (kinetis/session)#
Key |
Default |
Purpose |
|---|---|---|
|
(unset: no store) |
|
|
|
Seconds a session stays readable from its last write; must be positive. Every write refreshes the browser cookie’s |
|
|
Cookie name. A |
|
|
Cookie |
|
|
Cookie |
|
|
The |
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 |
|---|---|---|
|
(empty) |
Comma-separated exact |
|
|
Serves |
Telemetry (kinetis/telemetry)#
Key |
Default |
Purpose |
|---|---|---|
|
(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. |
|
|
The |
|
— |
Export-request headers, |
|
|
Exactly one of |
|
|
Sampling ratio for the two |
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 |
|---|---|---|
|
|
|
|
(required for pusher) |
App ID. |
|
(required for pusher) |
App key — also the value a browser client subscribes with. |
|
(required for pusher) |
App secret, used to sign trigger requests and private/presence channel authorizations. |
|
|
Server host the backend publishes to. |
|
|
Server port; must be a valid TCP port (1–65535). |
|
|
Connect over TLS. |
|
|
The whole budget for one trigger request, in seconds; must be above zero. A non-positive value fails at worker boot and names the scoped key. |
|
(empty) |
Comma-separated exact |
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:
.envloading, typedConfigaccess, and named connections.Bootstrapping — registering an application service or global middleware from
bootstrap.php.Appendix: Container Lifecycle —
AppScope’s registration-lock discipline, and howRequestScopedelegates to aConfigit never explicitly registered itself.Caching & AOT Compilation — the AOT cache’s reproducible-from-source invariant.
Appendix: System Layout — the
Kinetis\Confignamespace in the full system map.