Your backend
Rails
Protect controllers, read the signed-in user in views, serve server-rendered sessions and receive webhooks in Rails.
bundle add zevauth
Rails 7.0 or later. The integration is part of the Ruby gem and loads itself when your app has Rails.
ZEVAUTH_ENVIRONMENT_ID=env_live_…
ZEVAUTH_PUBLISHABLE_KEY=pk_live_…
ZEVAUTH_SECRET_KEY=sk_live_…
ZEVAUTH_WEBHOOK_SECRET=whsec_…
Prefer credentials? Set them in an initializer instead:
# config/initializers/zevauth.rb
ZevAuth.configure do |c|
c.secret_key = Rails.application.credentials.dig(:zevauth, :secret_key)
c.webhook_secret = Rails.application.credentials.dig(:zevauth, :webhook_secret)
end
Then mount the engine, which serves the session bridge and the handshake page:
# config/routes.rb
mount ZevAuth::Engine => "/zevauth"
Protecting controllers
class ApplicationController < ActionController::Base
before_action :require_zevauth!
end
With options:
class BillingController < ApplicationController
require_zevauth only: %i[update destroy], require_mfa: true
end
class OnboardingController < ApplicationController
# The one place a session that still has to set up two-step may reach.
skip_before_action :require_zevauth!
require_zevauth allow_pending_tasks: true
end
A refused request gets the same JSON every ZevAuth SDK sends:
| Status | error.code | When |
|---|---|---|
| 401 | unauthorized | Nobody is signed in |
| 403 | session_task_pending | The session has a task to finish. The body adds pendingTasks |
| 403 | mfa_required | require_mfa: true and the session didn’t pass a second factor |
The handlers are ordinary Rails ones, so you can replace the JSON with a redirect to your sign-in page:
class ApplicationController < ActionController::Base
rescue_from ZevAuth::AuthRefused do |error|
if request.format.html? && error.status == 401
redirect_to sign_in_path
else
render json: error.body, status: error.status
end
end
end
Who is signed in
In controllers and views:
<% if zevauth_signed_in? %>
Signed in as <%= zevauth_user_id %>
<% if zevauth_auth.has?(permission: "org:members:manage") %>
<%= link_to "Manage members", members_path %>
<% end %>
<% end %>
zevauth_auth is the full auth object: user_id, organization_id,
organization_role, has?, mfa_verified?, pending_tasks and claims.
See Ruby. It never raises: a lapsed session
reads as signed out.
The helpers are in every controller, including ActionController::API ones;
nothing is enforced until a controller asks.
Server-rendered pages
Rails renders pages on the server, so the browser doesn’t send a token in a header. The session bridge keeps the current access token in your own HttpOnly cookie instead, and the engine serves both parts of it:
POST /zevauth/session, where the browser SDK posts tokens;GET /zevauth/handshake, which a visitor passes through when their cookie lapsed.
Tell the browser SDK where to post, in your layout:
<script src="https://cdn.jsdelivr.net/npm/@zevauth/ui@0/dist/zevauth-ui.global.js"></script>
<script>
ZevAuthUI.init({
publishableKey: "<%= ENV.fetch('ZEVAUTH_PUBLISHABLE_KEY') %>",
sessionBridge: { url: "/zevauth/session" },
});
</script>
require_zevauth! handles a lapsed cookie for you: a page request is sent
through the handshake and comes straight back signed in, and an API request
gets 401 session_refresh_required so your front end can send its token in
the Authorization header instead.
Mounted somewhere other than /zevauth? Set c.bridge_path to the session
URL and c.bridge = ZevAuth::Bridge::Config.new(handshake_path: "...") to
the handshake URL.
Webhooks
# config/routes.rb
post "/webhooks/zevauth", to: "zevauth_webhooks#create"
# app/controllers/zevauth_webhooks_controller.rb
class ZevauthWebhooksController < ActionController::API
def create
event = zevauth_webhook_event
case event.type
when "user.created" then WelcomeMailer.with(user_id: event.data["id"]).deliver_later
end
head :ok
rescue ZevAuth::WebhookError
head :bad_request
end
end
zevauth_webhook_event verifies request.raw_post, the body exactly as
received. Never verify params: Rails has already parsed it, and a
re-encoded body fails the signature check, correctly.
An ActionController::API controller has no CSRF protection to skip. If
yours inherits from ActionController::Base, add
skip_forgery_protection only: :create.
Backend API
ZevAuth.client.users.get(zevauth_user_id)
ZevAuth.client.organizations.add_member("org_123", user_id: "user_456", role: "org:admin")
The full client is on the Ruby page.
Updated at, Sunday, November 1, 2026