Clowk Ruby SDK
Clowk is the Ruby gem for integrating Clowk authentication into Rails applications.
It focuses on a small client-side surface:
- redirect users to Clowk
- verify the JWT returned by Clowk
- expose Rails-friendly auth helpers
- provide a minimal HTTP client for the Clowk API
Product domains
Clowk uses different domains for different concerns:
clowk.in: public product siteapp.clowk.in: dashboard used to manage apps and instances*.clowk.dev: per-instance auth domain used by your end users
For a Rails app using this gem, the important part is the instance auth domain. Your app redirects users there, Clowk authenticates them, then redirects back with a signed JWT.
Install
# Gemfile
gem 'clowk'
Quick start
# config/initializers/clowk.rb
Clowk.configure do |config|
config.publishable_key = ENV['CLOWK_PUBLISHABLE_KEY']
config.secret_key = ENV['CLOWK_SECRET_KEY']
end
# config/routes.rb
Rails.application.routes.draw do
mount Clowk::Engine => '/clowk'
end
class ApplicationController < ActionController::Base
include Clowk::Authenticable
end
class DashboardController < ApplicationController
before_action :authenticate_clowk!
def index
@user = current_clowk
end
end
Authentication flow
- Your app redirects the user to
*.clowk.dev. - Clowk authenticates the user.
- Clowk redirects back to your callback URL with
tokenandstate. - The gem validates
state, verifies the JWT, stores the authenticated session, and redirects back to a safe internal path.
The callback flow includes:
- session-backed
statevalidation - JWT verification against Clowk's published keys (RS256), falling back to your instance
secret_keyfor legacy HS256 tokens - internal-only redirect sanitization
- session reset before persisting the authenticated user
httponlycookie persistence withSameSite=Lax
Configuration
Clowk.configure do |config|
config.secret_key = ENV['CLOWK_SECRET_KEY']
config.publishable_key = ENV['CLOWK_PUBLISHABLE_KEY']
config.subdomain_url = 'https://acme.clowk.dev'
config.prefix_by = :clowk
config.after_sign_in_path = '/'
config.after_sign_out_path = '/'
config.api_base_url = 'https://api.clowk.dev/api/v1'
config.callback_path = '/clowk/oauth/callback'
config.mount_path = '/clowk'
config.http_open_timeout = 5
config.http_read_timeout = 10
config.http_write_timeout = 10
config.http_retry_attempts = 2
config.http_retry_interval = 0.05
config.http_logger = Rails.logger
end
Important settings:
| Setting | Purpose |
|---|---|
secret_key |
Verifies legacy HS256 tokens. Not needed to verify RS256 tokens. |
publishable_key |
Preferred for auth URL resolution. The gem resolves the latest instance URL from it before sign in/sign up. Also the default audience. |
subdomain_url |
Fallback auth domain when you do not want publishable-key-based resolution. |
jwks_url |
Where to fetch Clowk's public keys. Defaults to <auth domain>/.well-known/jwks.json. |
audience |
Expected aud claim on RS256 tokens. Defaults to publishable_key. Set to nil to skip the check. |
prefix_by |
Prefix used to generate helper names. Default: :clowk. |
mount_path |
Local mount prefix used by helper path generation. Default: /clowk. |
callback_path |
Callback route Clowk redirects back to. Default: /clowk/oauth/callback. |
http_logger |
Optional logger used by Clowk::Http. |
session_status_cache |
Where API-only apps cache session status. Defaults to Rails.cache. |
API-only Rails apps
Clowk::Authenticable works in ActionController::API controllers with no
extra setup — no session middleware, no cookie middleware:
class ApplicationController < ActionController::API
include Clowk::Authenticable
before_action :authenticate_clowk!
end
What changes in that mode:
- Nothing is written back. A bearer request produces no
Set-Cookieand no session write. There is no browser to hold a cookie, a mobile client ignores it, and writing a session per request defeats the point of being stateless. - Failures are always 401 JSON, never a redirect — including when the
request carries no
Accept: application/json. A 302 to a sign-in page is useless to an API caller. - Session status is cached in
session_status_cacheinstead of the Rails session, keyed by a digest of the token. Without it,enforce_active_sessionwould cost a round trip to Clowk on every authenticated request.
Token verification
Tokens signed with RS256 are verified against Clowk's public key set, fetched
from jwks_url and cached in memory. The kid header selects the key, and a
kid the cache has not seen triggers a single refetch — that is what makes key
rotation invisible instead of an outage.
Because every consumer trusts the same public key under RS256, the aud claim
is what keeps a token minted for one app from verifying in another. It defaults
to your publishable_key, so the check is on without extra configuration.
Tokens signed with HS256 still verify against secret_key, so tokens issued
before Clowk migrated to RS256 keep working until they expire. Audience is not
enforced on that path: those tokens predate the claim.
Auth URL resolution priority:
publishable_keysubdomain_url
When publishable_key is present, the gem resolves the current auth base URL first and caches it briefly in memory. The lookup endpoint returns the full instance JSON payload, and the gem derives the final auth URL from that data (including subdomain). This keeps dashboard subdomain changes visible without redeploying the client app. If you do not want that lookup, configure only subdomain_url.
Internally, that lookup is done through Clowk::SDK::Client, via client.subdomains.find_by_pk('pk_...').
If you mount the engine under a different prefix, keep mount_path and callback_path aligned with that choice.
Example:
Clowk.configure do |config|
config.mount_path = '/auth'
config.callback_path = '/auth/oauth/callback'
end
Rails.application.routes.draw do
mount Clowk::Engine => '/auth'
end
Generated helpers
With the default prefix_by = :clowk, the concern exposes:
current_clowkauthenticate_clowk!clowk_signed_in?
You can change the prefix to avoid collisions with another auth system.
Clowk.configure do |config|
config.prefix_by = :member
end
That generates:
current_memberauthenticate_member!member_signed_in?
Current subject
current_clowk returns a Clowk::Current object.
current_clowk.id
current_clowk.email
current_clowk.name
current_clowk.avatar_url
current_clowk.provider
current_clowk.instance_id
current_clowk.app_id
You can also access raw claims:
current_clowk[:sub]
current_clowk.to_h
Route protection
class AdminController < ApplicationController
before_action :authenticate_clowk!
end
Use authenticate_clowk! for protected pages. Use current_clowk when the route can be public but should still know who is authenticated.
View and URL helpers
Local mounted routes:
<%= link_to 'Sign in', clowk_sign_in_path(return_to: dashboard_path) %>
<%= link_to 'Sign up', clowk_sign_up_path(return_to: dashboard_path) %>
<%= link_to 'Sign out', clowk_sign_out_path %>
Direct remote URLs:
<%= link_to 'Direct sign in', clowk_sign_in_url(redirect_to: dashboard_url) %>
<%= link_to 'Direct sign up', clowk_sign_up_url(redirect_to: dashboard_url) %>
When publishable_key is configured, these helpers resolve the latest instance URL before building the final sign-in or sign-up destination. When it is absent, they use subdomain_url directly.
Mounted routes exposed by the engine:
/clowk/sign_in/clowk/sign_up/clowk/sign_out/clowk/oauth/callback
When you mount the engine elsewhere, the same route set is exposed under your chosen prefix.
Token sources
The concern can read the token from:
params[:token]- cookies
Authorization: Bearer <token>
That keeps the integration usable for callback routes, regular controllers, and API-style endpoints.
SDK Client
The gem includes a resource-oriented SDK for the Clowk API.
client = Clowk::SDK::Client.new
The client uses your global configuration by default. You can also pass options explicitly:
client = Clowk::SDK::Client.new(
secret_key: 'sk_live_xxx',
publishable_key: 'pk_live_xxx'
)
Resources
Each resource is accessible as a method on the client:
client.users
client.sessions
client.subdomains
These return Clowk::SDK::Resource subclasses with a standard CRUD interface:
client.users.list
client.users.find('user_123')
client.users.show('user_123')
client.users.destroy('user_123')
Token verification
response = client.tokens.verify(token: params[:token])
response.status # => 200
response.success? # => true
response.body_parsed # => { 'valid' => true }
Subdomain resolution
response = client.subdomains.find_by_pk('pk_live_xxx')
Search operators
The search method uses a Zendesk-style query syntax. You can use keyword arguments or a raw string for advanced operators.
Keywords:
client.users.search(email: "user@example.com")
# GET /users/search?query=email%3Auser%40example.com
client.users.search(status: "active", role: "admin")
# GET /users/search?query=status%3Aactive+role%3Aadmin
Raw string for custom operators like >, <, >=:
client.users.search("email:user@example.com active:true created_at>2026-01-01")
# GET /users/search?query=email%3Auser%40example.com+active%3Atrue+created_at%3E2026-01-01
The raw string is sent as-is, giving full control over the query syntax. The API backend parses and applies the operators.
Raw HTTP methods
The client also exposes raw HTTP methods for custom requests:
client.get('custom/endpoint')
client.post('custom/endpoint', { key: 'value' })
client.put('custom/endpoint', { key: 'value' })
client.patch('custom/endpoint', { key: 'value' })
client.delete('custom/endpoint')
client.head('custom/endpoint')
client.('custom/endpoint')
Clowk::Http::Response
HTTP responses are returned as Clowk::Http::Response objects.
response = client.get('users/user_123')
response.status # => 200
response.success? # => true
response.body # => '{"id":"user_123"}'
response.body_parsed # => { 'id' => 'user_123' }
response.headers # => { 'content-type' => ['application/json'] }
For compatibility, the response also supports:
response[:status]
response[:success?]
response.to_h
HTTP middleware
Clowk::Http uses a small internal middleware stack built on Net::HTTP.
Included behavior:
- request and response logging
- open, read, and write timeouts
- retry on retryable network errors
- automatic JSON request encoding
- automatic
body_parsedJSON decoding when possible
Scope
This gem is intentionally narrow. It does not try to replace your entire app session architecture or act as an auth server.
Its job is to make the Rails side of Clowk integration predictable:
- start authentication
- validate callbacks safely
- expose a clean authenticated subject
- make future Clowk API access straightforward
License
AGPL-3.0. See LICENSE.