Ruby on Rails

Full integration guide for Clowk with Ruby on Rails

Install

Add Clowk to your Gemfile:

gem 'clowk'
bundle install

Requires Ruby >= 3.1 and Rails >= 7.0.

Configure

Create an initializer with your keys:

config/initializers/clowk.rb
Clowk.configure do |config|
  config.publishable_key = ENV['CLOWK_PUBLISHABLE_KEY']
  config.secret_key      = ENV['CLOWK_SECRET_KEY']
end

All configuration options

OptionDefaultDescription
publishable_keynilYour instance publishable key
secret_keynilYour 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:clowkSession key for storing auth data
token_param:tokenQuery parameter name for the token
issuer'clowk'Expected JWT issuer claim
prefix_by:clowkPrefix 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
end

secret_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:

app/controllers/application_controller.rb
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
end

How 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, &)
end

That 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

config/routes.rb
Rails.application.routes.draw do
  mount Clowk::Engine => '/clowk'
end

This adds four routes:

PathDescription
/clowk/sign_inRedirects to the Clowk sign-in page
/clowk/sign_upRedirects to the Clowk sign-up page
/clowk/sign_outClears session and cookie
/clowk/oauth/callbackReceives the JWT from Clowk

Include Authenticable

Add the Clowk::Authenticable module to your ApplicationController:

app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Clowk::Authenticable
end

This gives you three helper methods:

MethodDescription
current_clowkReturns 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

app/controllers/dashboard_controller.rb
class DashboardController < ApplicationController
  before_action :authenticate_clowk!

  def index
    @user = current_clowk
  end
end

The 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 hash

Protect specific actions

app/controllers/posts_controller.rb
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
end

API-only controllers

For API-only apps, authenticate_clowk! returns a JSON 401 response instead of redirecting:

app/controllers/api/v1/base_controller.rb
class Api::V1::BaseController < ActionController::API
  include Clowk::Authenticable

  before_action :authenticate_clowk!
end

The middleware extracts the token from (in order):

  1. Query parameter (?token=eyJ...)
  2. Authorization header (Bearer eyJ...)
  3. Cookie (clowk_token)

Views with Hotwire & Turbo

app/views/layouts/application.html.erb
<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")

On this page