No results found.

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:

  1. The SDK matrix — which packages exist, what each ships, when to pick which kind.
  2. Quickstarts — minimal hello-world for the four most-used stacks (Next.js, SvelteKit, Laravel, Django).
  3. 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.

StackPackageKindWhat it ships
JS / Node / browsers@wolfieauth/sdk-coreCorePKCE client, sealed sessions, claims helpers, theme tokens
SvelteKit@wolfieauth/sdk-sveltekitTemplateAll 5 templates + Svelte 5 components
Next.js / React@wolfieauth/sdk-reactTemplatecreateAuthHandlers, middleware, useWolfieAuth() hook
Laravelwolfieauth/sdk-laravelTemplateService provider, controllers, Blade views
Symfony / Syliuswolfieauth/sdk-syliusTemplateSymfony bundle, Twig templates, ShopUserProvisioner
Paywall (any JS)@wolfieauth/sdk-paywallAdd-onStripe checkout + entitlement reads + KSeF invoice issuance
Express@wolfieauth/expressBackendOIDC handshake + requireAuth middleware
Next.js (Auth.js)@wolfieauth/nextjsBackendAuth.js provider + standalone routes
Djangowolfieauth (PyPI)BackendURL routes + claims helpers + webhook verifier
Railswolfieauth gemBackendEngine routes + Devise integration
Laravelwolfieauth/laravelBackendSocialite driver, auto-create users, events
Symfonywolfieauth/symfonyBackendStandalone bundle without Sylius dep
CakePHPwolfieauth/cakephpBackendAuthentication adapter
Gogithub.com/wolfieauth/goBackendStateless 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-core
    
  • Reading claims from a refresh that landed mid-render — the React/Svelte hooks emit a claims-refreshed event 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:

EventWhen it fires
user.createda user has just signed up to your app for the first time
subscription.activatedplan moved from PENDING / INCOMPLETE to ACTIVE
subscription.cancelledplan moved to CANCELLED (use the active-feature helpers; nothing extra to do)
org.suspendedthe whole org is paused (e.g. via reseller cascade-suspend)
linked_account.createda 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:

ErrorWhat happenedWhat to do
WolfieAuthError("token_expired")Access token expired and refresh failedRedirect to /auth/login
WolfieAuthError("invalid_session")Session cookie tampered with or expiredClear local session, redirect to /auth/login
WolfieAuthError("approval_pending")User logged in but org needs approvalRedirect to /auth/pending
WolfieAuthError("network")WolfieAuth unreachableShow a “auth is down” page; existing sessions still work via JWKS cache
HTTP 403 from your own gatingUser doesn’t have the required claimShow 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

Last updated: