Client authentication
The shared session contract, login destinations and client rendering boundaries.
Core owns the AuthService contract. Optional provider plugins supply the session;
@bitakit/ui observes it and uses the existing framework navigation binding.
There is no second session store. Server authentication and authorization remain
with the provider and application server.
Better Auth and application paths
This assumes the Better Auth adapter and the generated BitaKit provider are already
configured. Keep the provider's existing client and server setup. The authClient import
below refers to the application's existing client module; adjust that path to your project.
// src/configs/better-auth.ts
import { defineConfig } from '@bitakit/core';
import type { BetterAuthOptions } from '@bitakit/better-auth';
export default defineConfig({
defaults: {
paths: { signIn: '/account/login', afterSignIn: '/customers', afterSignOut: '/' },
} satisfies BetterAuthOptions,
});// src/services/better-auth-client.ts
import { defineService } from '@bitakit/core';
import { betterAuthClient } from '@bitakit/better-auth';
import { authClient } from '@/lib/auth-client';
export default defineService(() => ({ client: authClient }), { provides: betterAuthClient });Paths are application-owned, safe root-relative URLs. No routes are created.
useAuth().signIn({ returnTo }) navigates to the configured login page with a
returnTo query parameter, unless the adapter supplies a native signIn handler.
External or login-loop return destinations fall back to afterSignIn.
The login page must pass resolveAuthReturnTo(paths, returnToFromURL) to the native
provider login API as its completion destination. afterSignIn is a fallback,
not an automatic session-change redirect. Providers requiring absolute callbacks
should resolve the validated path against the configured application origin,
not the authentication-server origin. Keep secrets and server configuration on the server.
'use client';
import type { ReactNode } from 'react';
import { AuthBoundary } from '@bitakit/ui';
import { LoadingPage, ErrorPage } from '@bitakit/shell/pages';
export function ProtectedContent({ children }: { children: ReactNode }) {
return (
<AuthBoundary pending={<LoadingPage />} renderError={() => <ErrorPage />}>
{children}
</AuthBoundary>
);
}useAuth from @bitakit/ui exposes the current snapshot and signIn/signOut. Bind
these methods through your existing controls and handle rejected promises through the host's
error presentation.
AuthBoundary waits for pending session resolution and does not redirect on session
errors. redirect={false} allows host-owned unauthenticated presentation. It is a
client rendering convenience, never a security boundary. Keep native server checks.
useAuth().signOut() awaits the adapter and then navigates to afterSignOut;
the boundary does not race that operation with a login redirect. A subsequent
protected route can start login normally. Calling a native SDK or service directly
does not invoke this UI navigation policy. Configure only one owner for logout navigation.
Writing another adapter
Implement the existing AuthService and register it as a service that provides the
authService contract, in an ordinary plugin. No Core change, provider registry or host
bootstrap is needed. The following is an adapter template: source is a normalized SDK
bridge that the adapter author implements using their provider's public APIs, not an
Auth.js or Clerk API.
import {
authService,
configureAuthPaths,
definePlugin,
defineService,
type AuthPaths,
type AuthService,
} from '@bitakit/core';
export function customAuthPlugin(source: AuthService, paths: AuthPaths) {
const signIn = source.signIn?.bind(source);
return definePlugin({
id: 'custom-auth',
services: {
// Reached as services['custom-auth'].session and, through the contract, services.auth.
session: defineService(
(): AuthService => ({
paths: configureAuthPaths(paths),
getSnapshot: () => source.getSnapshot(),
subscribe: (listener) => source.subscribe(listener),
signOut: () => source.signOut(),
...(signIn ? { signIn } : {}),
}),
{ provides: authService },
),
},
});
}Inside a plugin folder the same service is services/session.ts with the options as the
second argument; discovery derives the ID.
The bridge must return a stable { user, pending, error, organizationId? } snapshot until it changes;
user contains at least id, and null means signed out only once pending is false
and error is absent. Subscribe to native SDK updates and return an unsubscribe
function. Preserve SDK failures, await sign-out, and release any adapter-owned
resources through the ordinary service/plugin lifecycle. Keep React-only SDK providers
inside the optional integration package using existing plugin providers.
Check an adapter in its own tests with assertAuthContract from @bitakit/core/testing. It
drives the provider's login through signIn and rejects with the first broken rule: an
unstable snapshot, a missing unsubscribe, a non-string user.id, a signOut that resolves
before the user is cleared, or invalid paths.
import { assertAuthContract } from '@bitakit/core/testing';
await assertAuthContract(runtime.services.auth, {
signIn: () => runtime.services.auth.signInWithPassword({ identifier: 'ada', password: 'test' }),
});Auth.js and Clerk adapters are not implemented yet. Their native session/login APIs
must be mapped inside their optional packages; organizations, registration and password
recovery are not standardized by this small contract and stay on the provider's own service,
which services.auth exposes with the provider's full type. @bitakit/better-auth and
@bitakit/strapi (Users & Permissions, with registration, password reset and provider login)
are working references.
organizationId identifies the current settings scope when supplied by the provider; it
is not proof of organization membership. Server access checks remain with the provider.
See Integrations for available adapters and
manual composition for providing a contract without discovery.