@wocha/remix
v0.1.0
Published
Remix / React Router v7 adapter for Wocha authentication (BFF pattern with httpOnly cookies)
Maintainers
Readme
@wocha/remix
Remix / React Router v7 adapter for Wocha authentication using the BFF (Backend-for-Frontend) pattern. OAuth token exchange and refresh happen on the server; sessions are stored in encrypted httpOnly cookies — refresh tokens never reach the browser.
Install
npm install @wocha/remix
# or: pnpm add / yarn add @wocha/remixPeer dependencies: @remix-run/node or react-router (>=7), React (>=18).
Quick start
1. Environment variables
WOCHA_CLIENT_ID=your-client-id
WOCHA_CLIENT_SECRET=your-client-secret
WOCHA_ISSUER=https://my-tenant.auth.wocha.ai
# Optional:
WOCHA_API_URL=https://my-tenant.api.wocha.ai
WOCHA_COOKIE_SECRET=optional-separate-cookie-secret2. Auth route handler
Create a splat route at app/routes/auth.$.tsx:
import { createWochaHandler, wochaAuthConfigFromEnv } from "@wocha/remix";
const config = wochaAuthConfigFromEnv()!;
const handler = createWochaHandler(config);
export const loader = handler.loader;
export const action = handler.action;This exposes:
| Route | Method | Purpose |
|-------|--------|---------|
| /auth/login | GET | Start OAuth login (?return_to=, ?signup=1) |
| /auth/callback | GET | OAuth callback — sets session cookie |
| /auth/logout | GET/POST | End session and redirect to IdP logout |
| /auth/session | GET | Return public session JSON |
| /auth/refresh | POST | Refresh access token |
| /auth/switch-org | POST | Switch active organisation |
3. Root loader (for client hooks)
// app/root.tsx
import { getSession, WOCHA_ROOT_LOADER_ID } from "@wocha/remix";
import type { LoaderFunctionArgs } from "react-router";
export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request);
return { session, customerApiUrl: undefined };
}
export const id = WOCHA_ROOT_LOADER_ID;4. Route protection
Option A — React Router v7 middleware:
// app/routes.ts
import { greetMiddleware } from "@wocha/remix";
export const unstable_middleware = [
greetMiddleware(undefined, { publicPaths: ["/", "/about"] }),
];Option B — loader guard:
import { requireSessionFromLoader } from "@wocha/remix";
export async function loader(args: LoaderFunctionArgs) {
const session = await requireSessionFromLoader(args);
return { user: session.user };
}5. Client hooks
import { useSession, useUser, useOrg, useSignIn, useSignOut } from "@wocha/remix";
export function UserMenu() {
const { session, status } = useSession();
const user = useUser();
const { orgId } = useOrg();
const signIn = useSignIn();
const signOut = useSignOut();
if (status === "loading") return <p>Loading…</p>;
if (!user) return <button onClick={() => signIn()}>Sign in</button>;
return (
<div>
<p>{user.email} · {orgId}</p>
<button onClick={() => signOut()}>Sign out</button>
</div>
);
}Server helpers
import { getSession, getUser, requireSession, getAccessToken } from "@wocha/remix";
export async function loader({ request }: LoaderFunctionArgs) {
const session = await getSession(request);
const token = await getAccessToken(request);
return { session, token };
}Configuration
import { configureWochaAuth } from "@wocha/remix";
configureWochaAuth({
clientId: "...",
clientSecret: "...",
issuer: "https://my-tenant.auth.wocha.ai",
authBasePath: "/auth",
refreshBufferSeconds: 300,
dpop: { enabled: true, algorithm: "ES256" },
});License
Apache-2.0
