Ruby on Rails
Full integration guide for Clowk with Ruby on Rails
Install
Add Clowk to your Gemfile:
gem 'clowk'bundle installRequires Ruby >= 3.1 and Rails >= 7.0.
Configure
Create an initializer with your keys:
Clowk.configure do |config|
config.publishable_key = ENV['CLOWK_PUBLISHABLE_KEY']
config.secret_key = ENV['CLOWK_SECRET_KEY']
endAll configuration options
| Option | Default | Description |
|---|---|---|
publishable_key | nil | Your instance publishable key |
secret_key | nil | Your instance secret key (for JWT verification) |
mount_path | '/clowk' | Path where the engine is mounted |
callback_path | '/clowk/oauth/callback' | OAuth callback path |
cookie_key | 'clowk_token' | Cookie name for the JWT |
session_key | :clowk | Session key for storing auth data |
token_param | :token | Query parameter name for the token |
issuer | 'clowk' | Expected JWT issuer claim |
prefix_by | :clowk | Prefix for generated helper methods |
after_sign_in_path | '/' | Redirect path after sign-in |
after_sign_out_path | '/' | Redirect path after sign-out |
app_base_url | 'https://app.clowk.in' | Clowk app URL |
api_base_url | 'https://api.clowk.dev/client/v1' | Clowk API URL |
Credentials that are not known at boot
Clowk.configure is right when your keys come from the environment. Two common
shapes do not work that way:
- Self-hosted products with a settings screen. An operator pastes a publishable key into your admin UI and expects sign-in to start working on the next request — no redeploy, no new environment variable.
- Multi-tenant apps. One process, one instance per tenant.
For those there is one method — Clowk.with_credentials. It scopes the
credentials to a block instead of the process:
Clowk.with_credentials(publishable_key: 'pk_live_…') do
# sign-in URLs, JWKS, token verification and the SDK client
# all resolve against that instance in here
endsecret_key and subdomain_url go in the same way and are both optional —
RS256 needs no secret, and the auth domain is resolved from the publishable key
when absent.
In a controller, wire it with an around_action you name yourself:
class ApplicationController < ActionController::Base
include Clowk::Authenticable
around_action :require_tenant_key!
before_action :authenticate_clowk_user!
private
def require_tenant_key!(&)
Clowk.with_credentials(publishable_key: Tenant.current.key, &)
end
endHow you spell the block is yours too — (&), (&block), (&action), or
{ yield }. The examples here use (&); nothing in the gem depends on it.
:allow_own_credentials!, :use_workspace_sso! — the gem does not care and
never looks for a method named its way. Where the credentials come from is
equally yours: an environment variable, a settings row, the subdomain, a header.
Pass nil and the block runs against whatever Clowk.configure set, so a
request with no tenant needs no branch:
def require_tenant_key!(&)
Clowk.with_credentials(Tenant.current&.clowk_credentials, &)
endThat form takes a Clowk::Credentials object, for callers that already have one.
The keyword form builds it for you, so the common case never has to name the
class. The same call works in a job, a rake task or a script — there is nothing
controller-specific about it.
Nothing installs the filter for you. around_action has to wrap
authenticate_clowk_user!, and where it sits among your own filters is your
call — not something a gem should decide on your behalf.
There is deliberately no Clowk.secret_key = … setter. A setter has no
lifetime: one request raising between the assignment and its reset would leave
that key installed for the whole process, and the secret key mints HS256 tokens
for any subject. with_credentials restores the previous value even when the
block raises, which is what makes the lifetime a property of the API rather than
of your code's discipline.
Scoping is fiber-based, so it survives streaming responses and async adapters.
Scopes nest, and the inner one restores the outer. Apps that configure once at
boot are unaffected — Clowk.credentials falls back to Clowk.configure, and
explicitly passed arguments still win over both.
Mount the engine
Rails.application.routes.draw do
mount Clowk::Engine => '/clowk'
endThis adds four routes:
| Path | Description |
|---|---|
/clowk/sign_in | Redirects to the Clowk sign-in page |
/clowk/sign_up | Redirects to the Clowk sign-up page |
/clowk/sign_out | Clears session and cookie |
/clowk/oauth/callback | Receives the JWT from Clowk |
Include Authenticable
Add the Clowk::Authenticable module to your ApplicationController:
class ApplicationController < ActionController::Base
include Clowk::Authenticable
endThis gives you three helper methods:
| Method | Description |
|---|---|
current_clowk | Returns a Clowk::Current object or nil |
authenticate_clowk! | Requires authentication — redirects to sign-in or returns 401 for JSON |
clowk_signed_in? | Returns true if authenticated |
Protect controllers
class DashboardController < ApplicationController
before_action :authenticate_clowk!
def index
@user = current_clowk
end
endThe Clowk::Current object
current_clowk returns a Clowk::Current instance with these accessors:
current_clowk.id # User UUID (from "sub" claim)
current_clowk.email # "jane@example.com"
current_clowk.name # "Jane Doe"
current_clowk.avatar_url # "https://lh3.googleusercontent.com/..."
current_clowk.provider # "google"
current_clowk.instance_id # "inst_abc123"
current_clowk.app_id # "app_xyz789"
current_clowk[:custom] # Access any JWT claim by key
current_clowk.to_h # Full payload as hashProtect specific actions
class PostsController < ApplicationController
before_action :authenticate_clowk!, only: [:create, :update, :destroy]
def index
@posts = Post.all
end
def create
@post = Post.create!(
title: params[:title],
author_email: current_clowk.email,
author_id: current_clowk.id
)
redirect_to @post
end
endAPI-only controllers
For API-only apps, authenticate_clowk! returns a JSON 401 response instead of redirecting:
class Api::V1::BaseController < ActionController::API
include Clowk::Authenticable
before_action :authenticate_clowk!
endThe middleware extracts the token from (in order):
- Query parameter (
?token=eyJ...) - Authorization header (
Bearer eyJ...) - Cookie (
clowk_token)
Views with Hotwire & Turbo
<nav>
<% if clowk_signed_in? %>
<span>Hello, <%= current_clowk.name %></span>
<%= link_to "Sign out", clowk.sign_out_path, data: { turbo_method: :delete } %>
<% else %>
<%= link_to "Sign in", clowk.sign_in_path %>
<%= link_to "Sign up", clowk.sign_up_path %>
<% end %>
</nav>Sending a form to /sign_in
/clowk/sign_in answers with a redirect to your Clowk instance — a different
origin. Turbo cannot follow a cross-origin redirect. It does not raise and
it does not warn: the fetch is dropped, the page stays exactly as it was, and
the submit looks like a button that does nothing. A later manual refresh works,
which makes it read as flakiness rather than as a missing opt-out.
Links are fine. If you submit a form that ends up at /sign_in — a settings
screen that turns sign-in on, for instance — opt that form out of Turbo so the
browser performs the navigation itself:
<%= form_with url: clowk.sign_in_path, method: :get, data: { turbo: false } do |form| %>
<%= form.submit "Sign in" %>
<% end %>The same applies to any of your own actions that redirect to /sign_in at the
end — the redirect chain still terminates cross-origin.
SDK Client
Use Clowk::SDK::Client to interact with the Clowk API from your backend:
client = Clowk::SDK::Client.new
client.tokens.verify(token: "eyJ...")
client.users.find("user-uuid")
client.users.search(email: "jane@example.com")