Overview¶
The typed, long-running, API-first PHP framework for SaaS backends.
Start the tutorial Read the manifesto
Keel boots once and serves many requests (FrankenPHP, RoadRunner, FPM as a fallback). It compiles routes, the container, validation and serialization into plain PHP before deploy, so nothing on the hot path uses reflection. Types are the contract: one input class is the validation, the documentation and the API schema.
#[Post('/orders')]
#[Status(201)]
#[Authorize(scopes: ['orders:write'])]
final class CreateOrder
{
public function __invoke(CreateOrderInput $in, Orders $orders): OrderResource
{
return OrderResource::from($orders->create($in));
}
}
final readonly class CreateOrderInput
{
public function __construct(
#[Uuid] public string $merchantId,
/** @var list<LineItem> */
#[Count(min: 1)] public array $items,
public ?string $note = null,
) {}
}
Send {"merchantID": "x", "items": []} and the client gets every problem, with its path, without a line of
validation code:
{
"type": "about:blank", "title": "Validation failed", "status": 422,
"errors": [
{"path": "merchantID", "code": "unknown_field", "message": "Unknown field."},
{"path": "merchantId", "code": "required", "message": "This field is required."},
{"path": "items", "code": "count", "message": "Must contain at least 1 item(s)."}
]
}
What you get¶
-
Compiled, concurrent
Container, routes, hydrators and serializers are generated PHP.
keel runserves many requests per process on an event loop with non-blocking PostgreSQL, Redis and HTTP; FrankenPHP and RoadRunner work too. -
Secure by default
Every endpoint needs a token unless marked
#[Anonymous]. Tokens are bound to the client that holds them. Rate limits, idempotency keys, security headers and production boot checks are on from day one. -
Built for SaaS
Tenants come from the token, never from input. SQL on tenant tables must filter by the current tenant, and the ORM does it for you.
-
A compiled ORM
Plain typed entities, no lazy loading, an identity map, optimistic locking and keyset pagination on SQLite, PostgreSQL, MySQL, SQL Server, Oracle and standard SQL.
-
Background work
Typed jobs with a transactional outbox, async listeners, a cluster-safe scheduler, signed webhooks and a cache with stampede protection.
-
Agent-friendly
keel inspectprints the whole application as JSON, OpenAPI is generated from the types,keel sdkwrites a TypeScript client, and TOON answers cost language models fewer tokens. -
Observable
Traces with W3C propagation and OTLP export, Prometheus RED metrics per route, JSON logs with trace ids, liveness and readiness endpoints.
-
Real time
WebSockets and server-sent events on the same endpoints and auth as the rest of the API, with a pub/sub hub across processes and nodes.
-
FIX protocol
A native FIX engine: sessions with sequence recovery and heartbeats, typed messages compiled at build time, handlers per message type.
-
Encryption
#[Encrypted]columns and key rotation (AES-256-GCM), and end-to-end encrypted request and response bodies (JWE, ECDH-ES) on top of TLS. -
Money and ledgers
Exact
Money, IBAN/BIC/card validation, and a double-entry, hash-chained ledger with idempotent, overdraft-checked postings. -
Microservices
Service-to-service calls with identity, tenant, trace and deadline propagated; CloudEvents through the outbox with an exactly-once inbox; Docker and Kubernetes manifests.
-
Non-relational databases
MongoDB, DynamoDB, Elasticsearch/OpenSearch, CouchDB, Neo4j and InfluxDB drivers in PHP, non-blocking under
keel run, behind oneDocumentStore. -
Numeric vectors
IntVectorandFloatVectorwith exact integers and reproducible float sums. The optional Rust extension runs them with SIMD, 5–90× faster than arrays in a third of the memory.
Five minutes to a running API¶
composer global require el-hendawy/keel # the `keel` command
keel new acme && cd acme && composer install
keel serve # http://127.0.0.1:8000/hello/world
keel run # the Keel server: many requests per process, WebSockets, SSE
Then follow the tutorial: a multi-tenant notes API with a database, authentication, validation, pagination, tests and a production build, in about 150 lines.
Performance¶
With a 10 ms database query per request, keel run with the non-blocking PostgreSQL driver served
17,027 req/s in 244 MB, where FrankenPHP with 64 workers served 5,734 req/s; without I/O, 100,478 vs
51,985 req/s. Method and caveats: the Keel server.
In-process, one worker, PHP 8.3 on Apple silicon (benchmarks/kernel.php), every request but the health
check authenticated with a bound token:
| Scenario | req/s | µs/req |
|---|---|---|
GET /health/live (full default middleware) |
51,600 | 19.4 |
GET /orders/{id} (auth, dynamic route, SQL read) |
23,400 | 42.7 |
POST /orders 422 (auth, JSON parse, validation) |
29,700 | 33.6 |
POST /orders 201 (auth, validation, transaction, outbox) |
13,350 | 74.9 |
End to end against Laravel Octane on the same FrankenPHP binary (v0.1, without auth on either side), Keel served 2× to 6.3× more requests per second on PostgreSQL. See the benchmark for the method and caveats.
Status¶
Keel is a developer preview (0.3). Everything documented here is implemented and tested; until 1.0, minor versions may break compatibility, with upgrade notes in the changelog. PHP 8.3 or later.