SDKs
How to actually use WolfieAuth from your code — quickstarts, route protection, claim reads, plan gating, org switching, webhook handling. Copy-paste recipes for the common stacks.
SDKs — building your app on top of WolfieAuth
This page is the developer’s home base. If you’re integrating WolfieAuth into something you’re building (as opposed to dropping it into an off-the-shelf product like WordPress, Portainer, or Grafana — those live under Integrations), start here.
The page is structured in three layers:
- The SDK matrix — which packages exist, what each ships, when to pick which kind.
- Quickstarts — minimal hello-world for the four most-used stacks (Next.js, SvelteKit, Laravel, Django).
- Common patterns — concrete recipes for the things you’ll do every day: protecting routes, reading user info, gating by plan or permission, switching orgs, handling refresh, verifying webhooks.
Pick your SDK
WolfieAuth ships in two flavours of SDK. The template-based SDKs (@wolfieauth/sdk-*) come with full-screen login templates, sealed cookies, and the per-app paywall built in — drop in three files and you have a complete signup → login → plan-picker → app flow. The backend SDKs (wolfieauth/<lang> packages) wire just the OIDC handshake and let you build your own UI.
| Stack | Package | Kind | What it ships |
|---|---|---|---|
| JS / Node / browsers | @wolfieauth/sdk-core | Core | PKCE client, sealed sessions, claims helpers, theme tokens |
| SvelteKit | @wolfieauth/sdk-sveltekit | Template | All 5 templates + Svelte 5 components |
| Next.js / React | @wolfieauth/sdk-react | Template | createAuthHandlers, middleware, useWolfieAuth() hook |
| Laravel | wolfieauth/sdk-laravel | Template | Service provider, controllers, Blade views |
| Symfony / Sylius | wolfieauth/sdk-sylius | Template | Symfony bundle, Twig templates, ShopUserProvisioner |
| Paywall (any JS) | @wolfieauth/sdk-paywall | Add-on | Stripe checkout + entitlement reads + KSeF invoice issuance |
| Express | @wolfieauth/express | Backend | OIDC handshake + requireAuth middleware |
| Next.js (Auth.js) | @wolfieauth/nextjs | Backend | Auth.js provider + standalone routes |
| Django | wolfieauth (PyPI) | Backend | URL routes + claims helpers + webhook verifier |
| Rails | wolfieauth gem | Backend | Engine routes + Devise integration |
| Laravel | wolfieauth/laravel | Backend | Socialite driver, auto-create users, events |
| Symfony | wolfieauth/symfony | Backend | Standalone bundle without Sylius dep |
| CakePHP | wolfieauth/cakephp | Backend | Authentication adapter |
| Go | github.com/wolfieauth/go | Backend | Stateless session middleware |
Template vs backend — when to pick what:
- Template if you want everything (login screens, signup, plan picker, account settings, error pages) ready out of the box. Custom CSS is supported via theme tokens; full template overrides are supported via copy-paste of the file you want to change.
- Backend if you already have your own UI and just need the OIDC handshake plumbed in (button, callback route, session middleware). Most existing apps adopt the backend variant.
Quickstarts
Every quickstart assumes you’ve already created an app at /admin/clients/new and have the client_id + client_secret handy.
Next.js / React (@wolfieauth/sdk-react)
pnpm add @wolfieauth/sdk-react @wolfieauth/sdk-core
.env:
WOLFIEAUTH_ISSUER=https://auth.wolfieguard.com
WOLFIEAUTH_CLIENT_ID=yourorg-yourapp
WOLFIEAUTH_CLIENT_SECRET=...
WOLFIEAUTH_REDIRECT_URI=http://localhost:3000/api/auth/callback
WOLFIEAUTH_SESSION_SECRET= # any 32+ char random string
app/api/auth/[...wolfieauth]/route.ts:
import { createAuthHandlers } from '@wolfieauth/sdk-react/server';
const handlers = createAuthHandlers({
// env vars are picked up automatically; pass options here only if overriding
});
export const GET = handlers.GET;
export const POST = handlers.POST;
middleware.ts:
export { default } from '@wolfieauth/sdk-react/middleware';
export const config = { matcher: ['/((?!api/auth|_next).*)'] };
That’s it for the wiring. In any client component:
'use client';
import { useWolfieAuth } from '@wolfieauth/sdk-react';
export default function Header() {
const { user, claims, signOut, signIn } = useWolfieAuth();
if (!user) return <button onClick={() => signIn()}>Sign in</button>;
return <span>Hi {user.name} ({claims.wolfieauth_org_slug}) — <button onClick={signOut}>Sign out</button></span>;
}
In a server component or route handler:
import { getSession } from '@wolfieauth/sdk-react/server';
export default async function Dashboard() {
const session = await getSession();
if (!session) redirect('/api/auth/signin');
return <h1>Welcome {session.claims.email}</h1>;
}
SvelteKit (@wolfieauth/sdk-sveltekit)
pnpm add @wolfieauth/sdk-sveltekit @wolfieauth/sdk-core
src/hooks.server.ts:
import { wolfieauthHandle } from '@wolfieauth/sdk-sveltekit/server';
export const handle = wolfieauthHandle();
src/routes/+layout.server.ts:
export const load = async ({ locals }) => ({
user: locals.session?.user ?? null,
claims: locals.session?.claims ?? null,
});
In any +page.svelte:
<script>
let { data } = $props();
</script>
{#if data.user}
<p>Hi {data.user.name} — <a href="/auth/logout">Sign out</a></p>
{:else}
<a href="/auth/login">Sign in</a>
{/if}
In +page.server.ts (server-side gating):
import { redirect } from '@sveltejs/kit';
import { isApproved } from '@wolfieauth/sdk-core/oidc/claims';
export const load = async ({ locals }) => {
if (!locals.session) throw redirect(302, '/auth/login');
if (!isApproved(locals.session.claims)) throw redirect(302, '/auth/pending');
return { /* page data */ };
};
Laravel (wolfieauth/sdk-laravel)
composer require wolfieauth/sdk-laravel
php artisan vendor:publish --tag=wolfieauth-config
.env:
WOLFIEAUTH_ISSUER=https://auth.wolfieguard.com
WOLFIEAUTH_CLIENT_ID=yourorg-yourapp
WOLFIEAUTH_CLIENT_SECRET=...
WOLFIEAUTH_REDIRECT_URI="${APP_URL}/auth/wolfieauth/callback"
Routes auto-register. In any controller:
use Illuminate\Support\Facades\Auth;
public function dashboard()
{
$user = Auth::user();
$claims = session('wolfieauth.claims');
return view('dashboard', compact('user', 'claims'));
}
In a Blade template:
@auth
Hi {{ Auth::user()->name }} ({{ session('wolfieauth.claims.wolfieauth_org_slug') }})
<a href="{{ route('wolfieauth.logout') }}">Sign out</a>
@else
<a href="{{ route('wolfieauth.login') }}">Sign in with WolfieAuth</a>
@endauth
Route protection:
Route::middleware(['auth', 'wolfieauth.feature:wolfie-yourapp,ksef_enabled'])->group(function () {
Route::get('/invoices/ksef', [KsefController::class, 'index']);
});
Django (wolfieauth on PyPI)
pip install wolfieauth
settings.py:
INSTALLED_APPS = [..., 'wolfieauth']
MIDDLEWARE = [..., 'wolfieauth.middleware.WolfieAuthMiddleware']
WOLFIEAUTH = {
'ISSUER': 'https://auth.wolfieguard.com',
'CLIENT_ID': 'yourorg-yourapp',
'CLIENT_SECRET': '...',
'REDIRECT_URI': 'http://localhost:8000/auth/wolfieauth/callback/',
}
urls.py:
from django.urls import include, path
urlpatterns = [
path('auth/wolfieauth/', include('wolfieauth.urls')),
# …your app urls…
]
In a view:
from django.contrib.auth.decorators import login_required
from wolfieauth import claims
@login_required
def dashboard(request):
c = request.wolfieauth_claims
if not claims.is_approved(c):
return redirect('/auth/pending')
return render(request, 'dashboard.html', {'claims': c})
In a DRF view:
from rest_framework.permissions import IsAuthenticated
from wolfieauth.permissions import RequireFeature
class KsefView(APIView):
permission_classes = [IsAuthenticated, RequireFeature('wolfie-yourapp', 'ksef_enabled')]
def get(self, request): ...
Common patterns
The recipes below appear in nearly every WolfieAuth-backed app. They’re written stack-by-stack so you can copy the one that matches yours.
1. Protecting a route
The pattern: middleware-level redirect to /auth/login if no session; page-level redirect to /auth/pending if approval gate is on. Templates do both automatically; backend SDKs require one line.
// Next.js — middleware.ts is enough; page-level extra check:
const session = await getSession();
if (!session) redirect('/api/auth/signin');
if (!isApproved(session.claims)) redirect('/auth/pending');
// Laravel — middleware groups in routes/web.php:
Route::middleware(['auth', 'wolfieauth.approved'])->group(...);
# Django — decorator pattern:
@login_required
@wolfieauth_required(approved=True)
def view(request): ...
2. Reading user info in your code
Always read from the claims, never refetch from /userinfo per request — claims are signed and cached in the session cookie, so reading them is free.
const { claims } = useWolfieAuth();
claims.email // '[email protected]'
claims.name // 'Pawel Wolfie'
claims.sub // 'clxxx...' — stable WolfieAuth user ID
claims.wolfieauth_org_id // active org for this token
claims.wolfieauth_org_slug // 'acme'
claims.wolfieauth_role_slug // 'editor' / 'admin' / etc
claims.wolfieauth_permissions // ['invoices.read', ...]
claims.wolfieauth_membership_kind // 'MEMBER' | 'GUEST' | 'SPECIAL_ADMIN'
The full claim shape lives in @wolfieauth/sdk-core/oidc/claims.ts (TypeScript types) and the SSO & Sessions doc.
3. Plan and feature gating
Two helpers, two semantics. Most apps want the active variant.
import { hasFeature, hasActiveFeature, hasActivePlan } from '@wolfieauth/sdk-core/oidc/claims';
// Workspace mode — does THIS org have the feature?
if (hasActiveFeature(claims, 'wolfie-yourapp', 'ksef_enabled')) renderKsefButton();
// Union mode — does the user have it ANYWHERE? (cross-org upsell)
if (hasFeature(claims, 'wolfie-yourapp', 'advanced_reports')) {
showHint("You already have this in another org — switch context to use it here.");
}
// Hard plan check
if (!hasActivePlan(claims, 'wolfie-yourapp', 'premium')) showUpsell();
Shape of an entry in claims.wolfieauth_plans[]:
{
"client_id": "wolfie-yourapp",
"plan_slug": "premium",
"status": "ACTIVE",
"orgId": "org_abc",
"orgSlug": "acme",
"orgIsShadow": false,
"features": ["ksef_enabled", "advanced_reports"]
}
orgIsShadow: true means the personal 1-person workspace from solo signup. Some apps treat shadow orgs the same as real orgs; some explicitly upsell users to “create a real org with teammates.”
4. Permission checks
Permissions are role-based and computed server-side at token-mint time — your app reads them from the claim, no roundtrip needed.
import { hasPermission } from '@wolfieauth/sdk-core/oidc/claims';
if (!hasPermission(claims, 'invoices.write')) return forbidden();
from wolfieauth import claims
if not claims.has_permission(c, 'invoices.write'):
return HttpResponseForbidden()
use WolfieAuth\Sdk\Permissions\Permissions;
if (!Permissions::has($claims, 'invoices.write')) abort(403);
For permission discovery (which permissions a logged-in user has, e.g. to render a UI menu), read claims.wolfieauth_permissions directly — it’s a flat array.
5. Switching active org
When the user belongs to multiple orgs, your “org switcher” UI should let them swap context without a full re-handshake. Helper exists in every SDK:
// React
const { switchOrg } = useWolfieAuth();
await switchOrg('org_xyz'); // the next render sees new claims
// SvelteKit (call from a +server.ts action)
import { switchOrg } from '@wolfieauth/sdk-sveltekit/server';
await switchOrg(event, 'org_xyz');
\WolfieAuth\Sdk\Sessions\Session::switchOrg('org_xyz');
return redirect()->back();
from wolfieauth.session import switch_org
switch_org(request, 'org_xyz')
return redirect('/dashboard')
Under the hood: hits GET /api/auth/me?orgId=…, refreshes the session cookie, your next request sees wolfieauth_org_id updated.
6. Refresh handling
You usually don’t need to do anything — the SDK middleware silently refreshes near-expired sessions in the background. Two cases require attention:
Long-lived background jobs (queue worker holding a token for 30 minutes) — call
refreshIfNeeded()before each invocation:await session.refreshIfNeeded(); // sdk-coreReading claims from a refresh that landed mid-render — the React/Svelte hooks emit a
claims-refreshedevent you can subscribe to if your UI needs to react (rare).
7. Webhook handling
Every SDK ships a webhook.verify() helper. Verify the X-Wolfie-Signature header on every incoming hook before parsing the body:
# Django
from wolfieauth import webhook
@csrf_exempt
def hook(request):
sig = request.META.get('HTTP_X_WOLFIE_SIGNATURE')
if not webhook.verify(settings.WOLFIEAUTH_WEBHOOK_SECRET, sig, request.body):
return HttpResponseForbidden('bad_signature')
payload = json.loads(request.body) # {id, event, timestamp, data}
// Express
import { verifyWebhook } from '@wolfieauth/sdk-core/webhooks';
app.post('/hooks/wolfieauth', express.raw({type:'application/json'}), (req, res) => {
const sig = req.header('x-wolfie-signature');
if (!verifyWebhook(process.env.WOLFIEAUTH_WEBHOOK_SECRET, sig, req.body)) {
return res.status(403).send('bad_signature');
}
const payload = JSON.parse(req.body.toString());
// …
});
Header format: X-Wolfie-Signature: t=<unix>,v1=<hex>. HMAC-SHA256 over f"{t}.{raw_body}" — pass raw bytes, NOT a re-serialised JSON dict (Stripe-style). Replays older than 5 minutes should be rejected; the helpers do this automatically.
Common events your app will subscribe to:
| Event | When it fires |
|---|---|
user.created | a user has just signed up to your app for the first time |
subscription.activated | plan moved from PENDING / INCOMPLETE to ACTIVE |
subscription.cancelled | plan moved to CANCELLED (use the active-feature helpers; nothing extra to do) |
org.suspended | the whole org is paused (e.g. via reseller cascade-suspend) |
linked_account.created | a user just linked into your app from another |
8. Custom UI without templates
If you’re using a backend SDK and rendering your own login screens, here’s the four URLs you’ll need:
GET /auth/wolfieauth/login # SDK route — redirects to WolfieAuth /authorize
GET /auth/wolfieauth/callback # SDK route — exchanges code, sets session
GET /auth/wolfieauth/logout # SDK route — clears session, hits /session/end
GET /api/public/clients/<your-client-id>/branding # public endpoint — fetch your theme JSON for branded login button styling
You don’t have to build the login form yourself — that lives at auth.wolfieguard.com and is themed by your client’s theme tokens. You just paint the entry button in your app.
Same helper names everywhere
Every SDK exports the same canonical helpers so you can grep across stacks. The shapes adapt to language idioms but the names are stable.
TypeScript / JavaScript
import {
isApproved, isPendingApproval, canApproveIn,
getPlan, hasPlan, hasActivePlan, hasFeature, hasActiveFeature,
hasPastDuePlan, isBundlePlan,
isResellerOf, getResellerSeatStatus, isChildOrg,
} from '@wolfieauth/sdk-core/oidc/claims';
Python (Django)
from wolfieauth import claims
claims.has_feature(c, 'wolfie-yourapp', 'ksef_enabled') # union
claims.has_active_feature(c, 'wolfie-yourapp', 'ksef_enabled') # workspace
claims.has_active_plan(c, 'premium', app_client_id='wolfie-yourapp')
claims.plans_for_app(c, 'wolfie-yourapp')
claims.has_permission(c, 'invoices.write')
claims.is_reseller_of(c, 'wolfie-yourapp')
claims.can_approve_in(c) # ['org_xyz', ...]
PHP (Laravel / Sylius)
use WolfieAuth\Sdk\Plans\Plans;
use WolfieAuth\Sdk\Resellers\Resellers;
if (Plans::hasFeature($claims, 'wolfie-yourapp', 'ksef_enabled')) { ... }
if (Plans::hasActivePlan($claims, 'wolfie-yourapp', 'premium')) { ... }
if (Resellers::isResellerOf($claims, 'wolfie-yourapp')) { ... }
Go
import "github.com/wolfieauth/go/claims"
if claims.HasFeature(c, "wolfie-yourapp", "ksef_enabled") { ... }
Workspace mode vs union mode
A user can belong to many orgs; claims.wolfieauth_plans[] carries entries for every active subscription across every org. Two ways to interpret:
- Union mode (
hasFeature) — does the user have this feature ANYWHERE? Use for upsell (“you already have Premium in your other org — switch context to use it here”). - Workspace mode (
hasActiveFeature) — does the user have this feature in their currently-selected org? Use for tenant-isolated apps where each org is a hard boundary.
Each plan entry carries orgId / orgSlug / orgIsShadow — the active-* helpers filter on orgId === claims.wolfieauth_org_id before checking features. Shadow orgs (1-person personal workspaces from solo signup) have orgIsShadow: true so apps can opt to mix or exclude them.
Error handling
The SDKs throw or return errors when something goes wrong on the auth side. The shapes you’ll encounter:
| Error | What happened | What to do |
|---|---|---|
WolfieAuthError("token_expired") | Access token expired and refresh failed | Redirect to /auth/login |
WolfieAuthError("invalid_session") | Session cookie tampered with or expired | Clear local session, redirect to /auth/login |
WolfieAuthError("approval_pending") | User logged in but org needs approval | Redirect to /auth/pending |
WolfieAuthError("network") | WolfieAuth unreachable | Show a “auth is down” page; existing sessions still work via JWKS cache |
| HTTP 403 from your own gating | User doesn’t have the required claim | Show forbidden page or upsell |
Prefer letting the SDK middleware handle the first three transparently (every template-based SDK does); only the last two need app-level UX.
Continue reading
- Reseller mode — when your downstream app needs to host its own sub-tenants
- 3rd-party app SSO — for plugging WolfieAuth into apps you didn’t write
- SSO & Sessions — what’s actually on the wire
- Plans & Billing — pricing models and Stripe wiring
Last updated: