How we improved Organization Invites to Keycloak
Many discussions with the Keycloak maintainers and a lot of code reviews later, our biggest contribution to Keycloak to date has been merged
Author
Alexis RicoDate published
Contributing a six-month feature to one of the largest open-source identity projects
At Xata, we use Keycloak for authentication. Like many SaaS companies, we rely heavily on its Organizations feature for multi-tenancy where each of our customers gets their own isolated workspace with dedicated identity controls. After hitting a frustrating limitation with organization invitations, we decided to fix it upstream rather than work around it.
Six months of back and forth, many discussions with the Keycloak maintainers at Red Hat and IBM and a lot of code reviews later, our biggest contribution to Keycloak to date has been merged 🎉
This post covers the problem we faced, why we chose to contribute upstream and the technical details of what we built.
What is Keycloak?
If you're not familiar with Keycloak, it's an open-source Identity and Access Management (IAM) solution that handles authentication, single sign-on, and user federation. Originally created at Red Hat, it joined the CNCF as an incubating project in 2023. It supports all the protocols you'd expect: OpenID Connect, OAuth 2.0, and SAML 2.0.
For SaaS platforms, Keycloak’s Organizations feature is especially useful. It provides built-in multi-tenancy, letting you manage all customers under a single, scalable setup instead of spinning up isolated environments for each one.
The problem: Invisible invitations
Organizations in Keycloak let you invite users via email. The flow works like this:
- An admin invites someone by email address
- Keycloak generates an action token (a signed JWT) and emails it to the invitee
- The invitee clicks the link, registers (if new) or logs in and joins the organization
The problem? Once you send an invitation, it disappears into the void. There's no way to:
- See pending invitations
- Check if an invitation expired
- Resend or revoke an invitation
- Know which invitations were accepted
This is particularly painful when inviting users who don't yet have a user account. Since no user entity exists, there's nothing to attach the invitation state to. The action token lives only in Keycloak's cache until it's used or expires with no visibility for administrators.
Why contribute upstream?
We could have worked around this. We could have built our own invitation tracking layer on top of Keycloak, intercepting API calls and maintaining our own database of pending invites. But that approach has costs:
Maintenance burden: Every Keycloak upgrade becomes a potential breaking change. You're now maintaining integration code indefinitely.
Sync problems: Your external state can drift from Keycloak's actual state. What happens when an invitation is accepted but your webhook fails?
Duplication: You're reimplementing logic that belongs in the identity layer. Every other Keycloak user with this problem does the same.
Contributing upstream means the feature lives where it belongs. It is tested by Keycloak's CI, maintained by the community and benefits everyone. The trade-offs are time and dedication to open source but the result is permanent infrastructure rather than a fragile workaround.
The technical challenge
The core challenge was architectural. Keycloak's action tokens are stateless JWTs, verified cryptographically and tied to user state. They are designed this way intentionally:
- No database writes on token creation
- Validation happens by checking the signature and user state at redemption time
- Tokens are cached but not persisted
This works beautifully for existing users. The token encodes a user ID, and validation checks if the user's state has changed since issuance (for example, if the password was already reset). But for organization invitations to non-existent users, there is no user ID to encode. The invitation hangs in limbo.
What we built
The solution required changes across multiple layers of Keycloak:
REST API: Full CRUD for Invitations
We extended the existing OrganizationInvitationResource to support full invitation management:
GET /admin/realms/{realm}/organizations/{orgId}/invitations | List all invitations for an organization |
|---|---|
GET /admin/realms/{realm}/organizations/{orgId}/invitations/{id} | Get a specific invitation |
DELETE /admin/realms/{realm}/organizations/{orgId}/invitations/{id} | Revoke an active invitation |
POST /admin/realms/{realm}/organizations/{orgId}/invitations/{id}/resend | Resend an invitation |
The existing invite-user endpoint was modified to create a database record alongside generating the action token.
Admin Console UI
The Admin Console received a new "Invitations" tab within the Organizations section, displaying:
- Email address of the invitee
- Invitation status (pending, expired)
- Sent date and expiration date
- Actions (resend, revoke)
Token lifecycle integration
The trickiest part was integrating with the existing action token flow. When an invitation is accepted:
- The
InviteOrgActionTokenHandlervalidates the token as usual - We added a hook to mark the corresponding invitation record as accepted
- The user becomes an organization member
When an invitation is revoked:
- The database record is deleted
- If the action token is still in the cache, it's invalidated
- Attempting to use the token returns an appropriate error
The review process
Contributing to Keycloak is not a quick process and I expected that going in. The project has high standards, since this is identity infrastructure that protects countless applications. A few things stood out:
Architecture discussions: Pedro Igor, one of the core maintainers, raised the question of whether we should build a more generic PersistentActionEntity that could serve multiple use cases. We discussed the trade-offs and agreed to proceed with a focused implementation first.
Multiple iterations: The PR went through several changes as we addressed feedback. Review comments covered naming conventions, error handling, test coverage and API design.
Collaboration model: Pedro ended up co-authoring several commits, refining aspects of the implementation with improvements to the persistence layer that they could reuse in other features.
Patience required: From opening the PR in June to merging in November, the calendar time was substantial. Much of that was waiting for review cycles, which is normal for a project with this many contributors and priorities.
When will this ship?
The PR was merged into Keycloak's main branch on November 27, 2025. It will be part of version 26.5.0, expected in early 2026. If you want to try it now, it is available in Keycloak's nightly builds.
Reflections on contributing to large OSS projects
Six months is a long time to wait for a single feature. Was it worth it? Absolutely but with caveats:
Do it when the feature is core to your product. We use organization invitations to onboard new users to their teams. This wasn't a nice-to-have, it was blocking real workflows.
Budget for the long haul. Open-source projects have long review cycles. If you need something in two weeks, contributing upstream isn't the path.
Expect collaboration, not just review. The best outcome isn't "they merged my code unchanged", it's "the code improved through the process."
Try it out
If you are using Keycloak Organizations and have struggled with invitation visibility, this feature is for you. You can test it today using Keycloak's nightly channel or wait for the 26.5.0 release.
And if you are facing limitations in other open-source tools you depend on, consider contributing. It is slower than a workaround but the result is infrastructure that benefits everyone.
What's next
Looking ahead, we plan to keep contributing back to Keycloak and other open-source projects that form the backbone of our platform. These efforts help us improve the tools we depend on while we continue building what we aim to be the best database platform out there.
Related Posts
From OpenAPI spec to MCP: How we built Xata's MCP server
Learn how we built an OpenAPI-driven MCP server using Kubb, custom code generators, and Vercel’s Next.js MCP adapter.