dioxus-clerk
Clerk integration for Dioxus: components, hooks, and SSR initial state for web and fullstack apps.
Features
- Web (WASM): drop-in
<SignIn />,<UserButton />,<UserProfile />, organization, and waitlist widgets via Clerk's prebuilt JS UI; unstyled<SignInButton />/<SignUpButton />/<SignOutButton />; reactiveuse_auth/use_user/use_sessionhooks; cross-target<SignedIn>/<SignedOut>/<Protect>/<RedirectToSignIn />control-flow. - Fullstack (Axum):
ClerkAuthLayernon-rejecting tower middleware +current_auth()context reader for#[server]functions, with SSR initial auth state so the client hydrates without a flash of unauthenticated content. - Step-up & session tasks:
use_reverification()guards sensitive actions behind Clerk reverification;TaskSetupMFAmounts clerk-js's MFA-setup task.
API philosophy
dioxus-clerk is a Dioxus-native Clerk integration with React-inspired names
where they map cleanly. APIs such as ClerkProvider, SignIn, UserButton,
SignedIn, use_auth, and use_user should feel familiar to Clerk React
users, but this crate is not a React compatibility layer.
The primary API follows Dioxus and Rust conventions: typed options, signals, server functions, Axum middleware, and SSR initial auth state. When React semantics do not fit Dioxus well, this crate chooses the Dioxus-native design and documents the difference. See Migrating from Clerk React for the full mapping.
Getting started
Most apps depend on dioxus-clerk directly:
[]
= "0.2"
Fullstack apps enable the server feature on the native server build:
[]
= "0.2"
[]
= []
= []
= ["dioxus-clerk/server"]
Feature flags
| Feature | Default | Enables |
|---|---|---|
| (none) | ✅ | Client components, hooks, guards, Clerk widgets, and SSR initial-state consumption. |
server |
Axum middleware, extractors, #[server] context readers (current_auth), and SSR initial-state helpers. Enable on the native server build only. |
|
worker |
server plus Send-wrapped middleware futures for single-threaded Cloudflare Workers. |
60-second setup
For the smallest SPA integration, mount ClerkProvider at the app root or a
route-layout root, render a signed-out sign-in button, and render signed-in
user controls:
use *;
use *;
ClerkProvider is intended to wrap a whole app or route layout, not arbitrary
inline markup. In fullstack/SSR renders it may emit Clerk initial-state markup,
so avoid mounting it inside semantic containers such as table, ul, ol,
or p.
The rest of this section covers prerequisites, key sourcing, the bundled demo, and fullstack setup.
Get a Clerk publishable key
- Sign up at clerk.com and create an application.
- From the application dashboard, copy the Publishable key (
pk_test_...for development,pk_live_...for production). - For fullstack apps, also copy the Secret key (
sk_test_.../sk_live_...).
Where the keys live
The library doesn't dictate how you source the publishable key. ClerkProvider accepts an optional publishable_key prop, web-only apps normally pass it directly, and fullstack apps normally pass it during the server render so the server-rendered SSR initial state can carry the key to the wasm client. A wasm/client render can omit the prop only when an SSR initial-state script already includes the key. Source it however suits your app:
| Strategy | Works on | Notes |
|---|---|---|
env!("CLERK_PUBLISHABLE_KEY") |
wasm + native | Build fails at compile time if missing. What the bundled demo does: opinionated about catching misconfigured env loud and fast. In fullstack builds this bakes the same value into both halves, so keep the build env consistent. |
option_env!("CLERK_PUBLISHABLE_KEY") |
wasm + native | Same as env! but returns Option<&str> instead of erroring; lets you fall back to a runtime lookup. |
std::env::var("CLERK_PUBLISHABLE_KEY") |
server / native only | Read at runtime. Use on the server render or axum entrypoint, not in wasm code. |
Hardcoded &str constant |
anywhere | Fine for hello-world / dev. |
Fetched from a /config endpoint before provider mount, or hydrated via SSR initial state |
wasm at first provider render | Pattern for multi-tenant apps where the key varies per deploy. Render ClerkProvider only after the key is available; later publishable_key prop changes are intentionally ignored. For fullstack SSR hydration, the server-rendered provider must receive the key so it can emit it into the SSR initial state; the bundled demo uses the simpler env! path. |
For server-side ClerkAuthLayer setup, the secret key has to be runtime env (or some other secret store); it must never reach the wasm bundle. Use dioxus_clerk::server::ClerkAuthLayer::from_env() for the conventional CLERK_SECRET_KEY, pass an explicit secret to ClerkAuthLayer::new(secret), or use ClerkAuthLayerConfig plus ClerkAuthLayer::from_config(...) when you need a non-default Backend API base URL or optional claim validation such as authorized parties/audience. The publishable key stays separate: pass it to ClerkProvider on first server render, pass it directly in web-only apps, or arrange for SSR initial state / client config to provide it before clerk-js starts.
Run the demo
The demo uses env!, so the build fails at compile time with a clear rustc
error if CLERK_PUBLISHABLE_KEY isn't set:
Because env! resolves at build time, the same value must be present when the wasm and server halves of a fullstack app are compiled. The fullstack command passes the exported value to both builds. If clerk-js init fails at runtime (bad key, dashboard origin not whitelisted, network), use_clerk_error() exposes the failure so apps can render an error UI instead of staying stuck on Loading.
To run the whole demo in containers without a local Node/dx toolchain, use Dagger instead (it reads the keys from a .env file):
See demo/README.md for full run instructions, the Cloudflare Worker mode, and the matching Dagger commands. The demo combines the minimal SPA, router, and fullstack server-function flows into one app.
Quickstart (web-only SPA)
use *;
use *;
See demo/ for a working app; its /minimal route contains the same web-only shape.
Quickstart (fullstack)
The demo/ app uses this fullstack shape:
async
With the server feature enabled, ClerkError converts into Dioxus'
ServerFnError, so server functions returning Result<_, ServerFnError> can
use current_auth()? directly.
Auth lifecycle
window.Clerk and auth loadedness are separate lifecycle facts:
window.Clerkappears after clerk-js has executed in the browser.AuthState::is_loadedbecomes true only afterClerk.load()completes.AuthState::status()is the safest rendering input:Loading,SignedOut, orSignedIn.- Fullstack SSR initial state can report
is_signed_in: truewhileis_loaded: false. use_auth()exposes the signed-in initial auth snapshot immediately, includinguser_id,session_id,org_id, andorg_slugwhen available.use_user()anduse_session()mirror Clerk React's stateful hook shape withstatus,is_loaded,is_signed_in, and optional hydrated browserUser/Sessiondetails.
This means a signed-in fullstack page can render plain signed-in guards during hydration without waiting for clerk-js details. Transient clerk-js loading observations preserve existing signed-in knowledge, so signed-in SSR initial state should not flash signed-out or loading UI while browser details catch up.
Migrating from Clerk React
The public API intentionally follows Clerk React names where Dioxus can support
the same concept directly. Prop names become Rust snake_case, finite options
become Rust enums, and the unstyled buttons render a native <button> (don't
nest your own).
| Clerk React | dioxus-clerk |
|---|---|
<ClerkProvider publishableKey=...> |
ClerkProvider { publishable_key: ... } |
<SignIn />, <SignUp />, <Waitlist /> |
SignIn {}, SignUp {}, Waitlist {}; mounted widgets support fallback, host class, and host id |
<UserButton />, <UserProfile />, <UserAvatar /> |
Same names; UserAvatar is a lightweight <img> from use_user() |
<SignInButton />, <SignUpButton />, <SignOutButton /> |
Same names; render a native <button>, type="button" by default |
<SignedIn />, <SignedOut />, <Protect /> |
Same names; Protect supports role / permission |
<RedirectToSignIn />, <RedirectToSignUp /> |
Same names |
<ClerkLoading />, <ClerkLoaded />, <ClerkFailed /> |
Same names |
useAuth(), useUser(), useSession(), useClerk() |
use_auth(), use_user(), use_session(), use_clerk() |
useReverification() |
use_reverification() |
See docs/react-migration.md for the full parity matrix, the list of missing React APIs and their workarounds, intentional behavioural differences, and a side-by-side migration cookbook.
Recipes
Configure Clerk routing with props
ClerkProvider
Common Clerk options are component props. Use the options prop as an advanced raw escape hatch for Clerk options this crate has not named yet; explicit props win when both set the same Clerk option key.
When using Routing::Path for embedded auth widgets, make sure your app router also matches Clerk-owned child paths under the widget URL. OAuth and SSO flows can return to paths such as /sign-in/sso-callback; in Dioxus Router, add a catch-all route such as #[route("/sign-in/:..segments")] that renders the same <SignIn /> page. Use Routing::Hash if you want Clerk's internal routes kept out of the app path.
SignIn
Programmatic Clerk actions
let clerk = use_clerk;
clerk.open_sign_in;
clerk.open_sign_up_with_options;
clerk.sign_out_with_options;
use_clerk() is cross-target, so fullstack components no longer need cfg(target_arch = "wasm32") just to render auth buttons. Browser actions are still only executed after hydration in the browser.
Use the try_* variants when the caller needs to await completion or handle
errors locally:
let clerk = use_clerk;
rsx!
use_auth().get_token().await mirrors Clerk React's useAuth().getToken(),
and use_auth().sign_out() mirrors the common useAuth().signOut() path:
let auth = use_auth;
let token = auth
.get_token_with_options
.await?;
auth.sign_out_with_options;
Use use_clerk() directly for custom design-system buttons:
let clerk = use_clerk;
rsx!
The generated <SignInButton />, <SignUpButton />, and <SignOutButton />
components render their own native <button> and treat children as the button
contents. Unlike Clerk React, they are not wrappers that can safely receive a
nested custom <button>.
Step-up reverification
use_reverification() guards a sensitive action: run it through guard, and if
the action reports ClerkError::NeedsReverification (typically mapped from a
server 403 via ClerkError::from_reverification_hint), the guard opens clerk-js's
reverification prompt and retries the action once. A dismissed prompt surfaces as
ClerkError::ReverificationCancelled.
let reverify = use_reverification;
rsx!
Session tasks (MFA setup)
When a session is in a pending task state, read the task from
use_session().session().current_task and mount the matching task widget, or
pass task_urls to ClerkProvider to let clerk-js route pending users
automatically. Opt the gate in to pending sessions with
treat_pending_as_signed_out: false, otherwise SignedIn hides a pending user:
SignedIn
Router and script loading
router_push and router_replace are forwarded to Clerk as SPA navigation
callbacks. Pass Some(Callback::new(...)) when your router can navigate from a
string path.
ClerkProvider
For CSP or custom script hosting, configure the injected clerk-js script:
ClerkProvider
Loading and failure UI
ClerkLoading
ClerkLoaded
ClerkFailed
Use use_clerk_error() when you want to display the specific error. Use
use_clear_clerk_error() from a dismiss button when the displayed error is
recoverable:
let error = use_clerk_error;
let clear_error = use_clear_clerk_error;
rsx!
In fullstack/SSR apps, prefer SignedOutWhenLoaded for signed-out CTAs that
should not flash before clerk-js has finished checking the browser session:
SignedOutWhenLoaded
SignedIn
Rendering gates
Protect
Protect
use_auth().has_role(...), has_permission(...), and has(AuthRequirement::permission(...)) use the same server-verified org claims.
Axum handlers
For Dioxus server functions, use current_auth() / current_auth_opt(). For
regular Axum handlers under ClerkAuthLayer, use extractors:
use ClerkAuth;
async
async
Development
The crate targets both native (server) and wasm (web) builds, so the checks
span two targets. The pure Rust/Dioxus workflow needs the toolchain pinned in
rust-toolchain.toml with the wasm32-unknown-unknown
target and wasm-pack:
# Format and lint
# Build both halves: native/server and web (wasm)
# Build the docs
# Run the native + integration tests
# Run the wasm-bindgen browser tests
Dagger runs the same build, clippy, doc, test, and wasm-pack checks in pinned containers with a single command; this is exactly what CI runs:
License
Licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0)
- MIT license (LICENSE-MIT or http://opensource.org/licenses/MIT)
at your option.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.