Pangolin is pre-1.0 software under active hardening. It is a capable catalog with a broad feature set, and it is not yet something we would tell you to put in front of a production data lake without reading this page first.
Earlier versions of this site and the project README claimed 100% Apache Iceberg REST spec compliance and marked eleven features "Production-Ready". Neither claim was supported by the code. Both are gone. What follows is the same maturity assessment that now ships in the repository README — the good and the bad, in the same place.
| Area | Maturity | Notes |
|---|---|---|
| Iceberg REST — namespaces, tables, commits | Solid | Commit requirements including assert-ref-snapshot-id are enforced; unsupported operations return an error rather than a false 200 OK. |
| Iceberg REST — full spec coverage | Partial | Several endpoints are missing. See the coverage list below. |
| Multi-tenancy and isolation | Solid | Tenant scope is a required parameter throughout; isolation tests pass against the production middleware. |
| Git-style branching, tags, merge | Good | Merge direction and branch-asset tracking were fixed in 0.6.0. |
| RBAC, service users, API keys | Good | API keys carry a key ID, so authentication is one bcrypt verification rather than a scan of every service user. |
| Audit logging | Good | 40+ actions, 19 resource types, plus authentication events from 0.6.0. Writes are best-effort and records are not tamper-evident. |
| Observability | New in 0.6.0 | Prometheus metrics, request correlation IDs, a working RUST_LOG, JSON logs, and real health endpoints. |
| PostgreSQL backend | Good | The recommended backend. Provisioning from a fresh database was broken before 0.6.0. |
| SQLite backend | Good | Single-writer; suitable for one node or an evaluation. Versioned migrations added in 0.6.0. |
| MongoDB backend | Beta | No index management, transactions only where the deployment supports sessions, four known-failing tests. |
| Kubernetes deployment | Good | The chart shipped referencing three templates that did not exist; all present and CI-linted from 0.6.0. |
| Transactions for admin operations | Partial | Landed late in 0.6.0. PostgreSQL now wraps delete_catalog, delete_branch and merge_branch in transactions; MongoDB covers delete_catalog only, and only on a deployment that supports sessions. Branch creation by copying assets is still not atomic. |
| Rate limiting | Missing | Global concurrency and body limits and a request timeout exist; there is no per-IP or per-account throttle. |
| OIDC (PKCE, JWKS, discovery) | Missing | OAuth works and is now safe, but it is not full OIDC. |
| Encryption at rest for warehouse credentials | Missing | Cloud credentials are stored unencrypted in the catalog database. |
| HA at more than one replica | Partial | It works, but it is unproven — see the limitations below. |
| Backup / restore / DR | Undocumented and untested | No published RPO or RTO. |
| Performance | Unmeasured | No load-test harness, no published figures, no capacity model. |
AUDIT_EXECUTION_PLAN.md in the repository root is a candid, itemised assessment of the codebase with a phased plan. It is the best place to understand what is weak and what is being worked on.
Stated plainly rather than buried.
As of 0.6.0, PostgreSQL wraps a cascading catalog delete, a branch delete and a branch merge in a database transaction, and MongoDB wraps a cascading catalog delete where the deployment supports a session — a standalone mongod cannot. Creating a branch by copying assets is still issued as independent statements, so a failure partway through leaves the catalog partially applied with no rollback and no repair tool. Take a backup before large administrative operations.
The Iceberg table-commit path is safe: it uses compare-and-swap with full requirement enforcement.
There are global concurrency and body-size limits and a per-request timeout, but no per-IP or per-account throttle. The login endpoint is brute-forceable. Put Pangolin behind a gateway that can rate-limit if it is exposed to anything untrusted.
The 0.6.0 flow is safe — signed state, server-side nonces, allowlisted redirects, code exchange instead of a token in a URL — but it is not a complete OIDC implementation. There is no PKCE, no id_token signature validation, no JWKS, and no discovery document. Users are matched on the email a provider returns, with no email_verified check. JWTs are symmetric HS256 with no key rotation, so rotating the secret ends every session at once.
Cloud storage credentials live in the clear in the catalog database. Envelope encryption at rest is not implemented. Encrypt your database backups. The in-process warehouse cache is also node-local, so a rotated credential can be served by a peer for up to the cache TTL — 5 seconds by default.
The background token-cleanup job runs in every replica with no coordination, and the OAuth nonce store is in-process — so OAuth needs session affinity across replicas.
No procedure has been exercised and there is no published RPO or RTO. The production runbook covers health, metrics, incidents and upgrades, and is explicit about where the gaps are.
Rust makes for a small, fast binary, but "fast" is a claim we have not measured. There is no load-test harness and no capacity model.
This project previously claimed 100% spec compliance. That was not supported by the code, and is not claimed now.
getConfig listNamespaces createNamespace
dropNamespace updateNamespaceProperties listTables
createTable loadTable updateTable
dropTable tableExists renameTable
createView loadView
Plus credential vending and the OAuth token endpoint. getConfig returns per-warehouse configuration from 0.6.0.
loadNamespaceMetadata namespaceExists
registerTable commitTransaction
Plus most of the view API — no list, drop, replace, exists or rename. commitTransaction means there are no multi-table atomic commits.
assert-create assert-table-uuid
assert-ref-snapshot-id assert-current-schema-id
assert-default-spec-id assert-default-sort-order-id
assert-last-assigned-field-id
An unrecognised requirement is refused rather than ignored.
assign-uuid upgrade-format-version add-schema
set-current-schema add-snapshot set-snapshot-ref
remove-snapshot-ref set-properties remove-properties
set-location add-spec set-default-spec
add-sort-order set-default-sort-order remove-snapshots
An unrecognised update returns 501 rather than a false 200 OK.
CatalogStore is a large trait in which many methods carry a default implementation returning "Operation not supported by this store" — so a missing method compiles cleanly and surfaces only as an opaque runtime 500. This is the matrix that tells you, before you deploy, which features work where.
| Capability | Memory | SQLite | PostgreSQL | MongoDB |
|---|---|---|---|---|
| Tenants, catalogs, namespaces, assets | ✅ | ✅ | ✅ | ✅ |
| Warehouses and credential vending | ✅ | ✅ | ✅ | ✅ |
| Branches, tags, commits | ✅ | ✅ | ✅ | ✅ |
| Branch merge | ✅ | ✅ | ✅ | ✅ |
| Merge operations and conflicts | ✅ | ✅ | ✅ | ⚠️ |
| Users, roles, permissions (RBAC) | ✅ | ✅ | ✅ | ⚠️ |
| Service users and API keys | ✅ | ✅ | ✅ | ✅ |
| Token issue / revoke / list | ✅ | ✅ | ✅ | ✅ |
| Audit logging, filtering and counting | ✅ | ✅ | ✅ | ⚠️ |
| Business metadata and access requests | ✅ | ✅ | ✅ | ✅ |
| Federated catalogs | ✅ | ✅ | ✅ | ✅ |
| Asset search | ✅ | ✅ | ✅ | ✅ |
| Versioned schema migrations | n/a | ✅ | ✅ | ❌ |
| Multi-statement transactions | n/a | ⚠️ | ⚠️ | ⚠️ |
| Survives a restart | ❌ | ✅ | ✅ | ✅ |
✅ implemented and covered by tests · ⚠️ implemented with known gaps · ❌ not available
sqlx migration chain applied at startup under a pg_advisory_lock, so concurrent replicas do not race. Before 0.6.0 the chain could not provision a fresh database at all.revoked_tokens table without which token revocation failed at runtime.DATABASE_URL is unset — the server logs a warning when this happens.| ID | Issue | Impact |
|---|---|---|
| A-8 | The OAuth callback appended the session JWT to a redirect URL taken from the unsigned state parameter, with no allowlist |
Account takeover. An authorize link whose state decoded to {"redirect_uri":"https://evil.example/"} delivered a valid token for the victim to the attacker's access log, with no credential theft required |
| A-9 | The OAuth state nonce was generated but never stored or verified |
Login CSRF. An attacker could bind a victim's browser to the attacker's account |
| A-10 | PANGOLIN_JWT_SECRET fell back to a value published in this repository; the Helm chart shipped working placeholder passwords and the seeded admin used password123 |
Anyone could forge a Root token against a deployment that missed one environment variable |
| A-11 | The authentication whitelist matched any path ending in /config and any path containing /oauth/tokens |
Authentication bypass. A namespace or table named config was reachable unauthenticated, including its DELETE route |
| A-12 | API-key authentication ran bcrypt::verify against every service user in every tenant |
Unauthenticated denial of service. At 100 service users a single request with a bogus key burned roughly 25 CPU-seconds, before any rate limiting |
| A-13 | A store error during the token-revocation check was logged and ignored | Revoked tokens were accepted again during any database disruption. It now returns 503 |
| A-14 | The root password was compared with == |
Timing oracle on the password |
id_token signature validation, no JWKS, no discovery.PANGOLIN_JWT_SECRET is at least 32 bytes of real entropy and is not in Git. Prefer existingSecret in the Helm chart over values.yaml.PANGOLIN_NO_AUTH is unset. It grants tenant-admin to anonymous callers; the server refuses to start with it enabled on a non-loopback address, but do not rely on that.PANGOLIN_DEV_MODE is unset, and PANGOLIN_ROOT_USER is unset unless you actively need root basic auth.PANGOLIN_CORS_ALLOWED_ORIGINS is set. The default allows any origin, which is only safe behind a trusted gateway.PANGOLIN_ALLOW_LEGACY_API_KEYS is unset once keys are rotated./metrics is not exposed publicly. It is unauthenticated by design for scraping; set PANGOLIN_METRICS_ENABLED=false if you cannot restrict it.The outstanding work, tracked in AUDIT_EXECUTION_PLAN.md.
id_token validation, discovery, and lookup by (provider, subject) rather than email.loadNamespaceMetadata, namespaceExists, registerTable, commitTransaction, and the rest of the view API.Yes, if you are evaluating Iceberg catalogs, want Git-style branching over catalog metadata, are running a single-tenant internal lakehouse behind a trusted network, or want to contribute to a Rust catalog implementation.
Not yet, if you need a catalog in front of production data with a tested recovery path, a compliance-grade audit trail, credential encryption at rest, or full Iceberg REST spec coverage. Apache Polaris and Dremio are the honest recommendations for that today.