Customize¶
Customizability is one of the core features that sets Lakekeeper apart from other Iceberg REST Catalog implementations. Almost every part of the server is a Rust trait or an injectable hook, so you can replace a backend or extend behaviour — for example to grant access to tables via your company's data-governance solution, persist secrets in a vault you already operate, react to every table change, expose custom endpoints, or publish events to your messaging system.
There are two ways to extend Lakekeeper, and the canonical list of everything you can supply is the ServeConfiguration struct passed to serve().
Compile-time backends (generic parameters)¶
These are the type parameters of serve<C: CatalogStore, S: SecretStore, A: Authorizer, N: Authenticator> and CatalogServer<C, A, S>. Choosing an implementation is a build-time decision, so an integration is type-checked when you compile the server — there is no runtime plugin loading.
| Component | Trait | Responsibility | Ships with |
|---|---|---|---|
Catalog |
CatalogStore |
Persistence for Warehouses, Namespaces, Tables, Views, and other entities. | Postgres (lakekeeper-storage-postgres) |
SecretStore |
SecretStore |
Secure storage for storage credentials and other secrets. | Postgres, HashiCorp Vault-compatible KV v2 (lakekeeper-secrets-kv2) |
Authorizer |
Authorizer |
The permission system. May expose its own management APIs. Default: Allow. |
AllowAll, OpenFGA |
Authenticator |
limes:: |
Token authentication chain. Default: Authenticator. |
OIDC/OAuth2, Kubernetes |
Runtime extension points (ServeConfiguration)¶
These are fields of ServeConfiguration, set with its builder when the server starts. Several accept a list, so multiple implementations can be active at once.
| Field | Trait / type | What it lets you do |
|---|---|---|
event_ |
EventListener |
React to strongly-typed domain events — table created/dropped/renamed/registered, transaction committed, view changes, and more. Override only the events you care about. Lakekeeper does not call transaction_ for a table commit that changes nothing. |
cloud_ |
Cloud |
Receive those changes as serialized CloudEvents and forward them somewhere. Ships with Kafka, NATS, and tracing sinks. |
contract_ |
Contract |
Veto table/view changes — for example when a data contract would be violated. Implementations are collected into Contract. |
admission_gates |
AdmissionGate |
Accept or reject an already-authenticated request before it reaches any handler — e.g. consult an external entitlement service, or suspend a tenant or principal. Gates run before the assume-role check; they are evaluated in order and the first rejection wins. |
stats |
Endpoint |
Consume per-project, per-endpoint API call statistics — for chargeback, quotas, or your own monitoring pipeline. |
modify_ |
fn(axum:: |
Modify the HTTP router before serving: add custom routes, middleware, or layers. (The built-in UI is injected exactly this way.) Routes added here have no authentication. For authenticated management routes, use management_. |
management_ |
Management |
Add your own endpoints to the management API under /management/v1/. They are authenticated, checked by the admission gates and counted in endpoint statistics like Lakekeeper's own management endpoints, and they appear in the management OpenAPI document. Lakekeeper does not authorize them: each handler authorizes its request and records it in the audit log itself, for example with the emit_authz method of APIEventContext. Name a warehouse path parameter {warehouse_id}, so that statistics count the call for that warehouse. The server does not start if an extension breaks a rule, for example if a route overlaps a Lakekeeper route, or if the OpenAPI document of the extension does not describe exactly the routes of the extension. A test can run the same checks with validate_. |
register_ |
Register |
Register custom background task queues alongside the built-in soft-delete expiration and purge queues. |
register_ |
Register |
Spawn arbitrary additional background services / futures that share the server's graceful-shutdown lifecycle. |
enable_ |
bool |
Disable the built-in task queues if you want to run them out-of-process. |
ServeConfiguration also accepts license_status, build_info, and system_roles, which downstream binaries (such as Lakekeeper Plus) use to inject build metadata and seed catalog-managed system roles.
Two layers of event handling¶
Reacting to changes has two levels, depending on what you need:
- Implement an
EventListenerwhen you want to run typed Rust logic in-process on specific events (the default trait methods are no-ops, so you override only what you care about). Register it via theEventDispatcher. - Provide a
CloudEventBackendwhen you just want changes shipped out as CloudEvents. The built-in CloudEvents publisher is itself anEventListenerthat fans every event out to all configuredcloud_event_sinks.
What the hooks are for¶
A few common reasons to reach for each one:
- Custom task queues run persisted, scheduled background work on Lakekeeper's own task infrastructure — the tasks are stored in the database and picked up by workers across your Lakekeeper instances, so you don't have to stand up a separate scheduler or worry about a single worker dying mid-job. Beyond the built-in soft-delete expiration and purge queues, typical uses include triggering table maintenance/compaction, custom retention and cleanup policies, recomputing statistics, refreshing downstream materialized views, running data-quality or lineage scans, and pushing periodic digests to external systems.
- Event listeners / cloud-event sinks keep external systems in step with the catalog without polling — feeding data-discovery and lineage tools, audit pipelines, cache invalidation, or webhooks.
- Contract verification enforces organisational rules at write time, for example rejecting a schema change that would break a published data contract.
- Admission gates allow or deny an already-authenticated principal before any handler runs. This is separate from the per-resource
Authorizer. Use one to ask an external control plane whether a caller may use this instance, or to suspend a tenant or principal. A gate sees the user it admits (AdmissionContext::user_id) and can test their token against a claim rule (AdmissionContext::token_claims) without reading any claim value. It never sees the request's project or warehouse, so the roles it grants hold in every project. A gate returns a terminal403, or a503with aRetry-Afterwhen it fails closed because an upstream it needs is unreachable. If a gate governs only some principals — one identity provider or tenant — returnGateDecision::not_applicable()for the rest instead ofGateDecision::admit(). Both let the request through, but only the first keeps the requests the gate approved apart from the ones it never examined. Gates run on every authenticated request, so cache aggressively and set your own timeouts. A claim test belongs to the request's token, so never cache its outcome per user. Every gate evaluation is timed for you (lakekeeper_admission_gate_duration_seconds); a gate's own cache can report into Lakekeeper's sharedlakekeeper_cache_*series via the helpers inlakekeeper::service::cache_metrics. See Monitoring. - Endpoint-statistics sinks route usage data into billing/chargeback, per-tenant quotas, or your own dashboards.
- Router modifications expose extra endpoints without authentication (custom health/readiness checks) or middleware alongside the catalog — this is how the built-in UI is mounted. For admin endpoints that need authentication, use management API extensions.
Adding a custom implementation¶
Extending Lakekeeper always follows the same shape:
- Implement the trait. Each extension point above links to its trait definition — that source is the authoritative, always-current signature.
- Wire it in. For a runtime hook, pass your implementation to the matching
ServeConfigurationbuilder field (for example.cloud_event_sinks(...),.stats(...), or.modify_router_fn(...)). For a compile-time backend, pass your type as the generic parameter toserve()(CatalogStore,SecretStore,Authorizer,Authenticator).
The best examples are the implementations Lakekeeper ships — they are compiled against the current traits and never drift out of date:
- Event sinks — the Kafka (
lakekeeper-events-kafka) and NATS (lakekeeper-events-nats) crates, plus the in-treeTracingPublisher. - Catalog & secrets —
lakekeeper-storage-postgresandlakekeeper-secrets-kv2. - Authorizer — the OpenFGA implementation in
crates/authz-openfga.
See the Developer Guide for building Lakekeeper from sources.