Skip to content

Integration · How-to guide

Proxy setup

Run the recommended same-origin integration with a hardened server proxy and packaged React client.

The proxy method exposes /api/auth/* on your application and forwards those requests to the central service. The SDK replaces caller-supplied proxy headers with the original hostname, protocol, and your existing service API key. Auth validates that key and any configured permission restrictions before using the forwarded origin for authentication or email branding.

Authorize the proxy

Use the service API key already configured on your application server:

env
KRAKSTACK_AUTH_SERVICE_API_KEY=svc_...

All three proxy helpers read this variable automatically. You can also pass apiKey: Redacted.make(serviceKey) in the helper's options (Redacted comes from effect). Keep the key server-only and use HTTPS between the application and Auth in production. The proxy fails if its key is missing.

Service keys with no permissions configured (null or an empty object) have all permissions by default. They can proxy requests for any registered project; no additional grant is needed. The admin dashboard displays All permissions for these keys and lists explicit grants using the same list summary as SDK API-key tables.

When a service key has a non-empty permissions object, proxy access is restricted to explicit auth:proxy grants for the relevant project IDs. An Auth administrator can configure these through the service-key create/update API:

json
{
  "permissions": {
    "project_your_id": ["auth:proxy"]
  }
}

Register each application hostname, including local hosts with their ports, as an active domain of its project. Auth derives the project from that registration, not from a project ID supplied by the caller. The hostname must have exactly one active registration. An explicit empty action list for a project does not grant proxy access. Existing key referrer restrictions also apply to the forwarded application origin.

The dedicated x-krakstack-proxy-key header authenticates the proxy without replacing the user's cookies, bearer token, or API key. Auth removes proxy credentials before handing the request to Better Auth. Key verification uses the existing service-key validity, expiry, permissions, and rate-limit checks; proxied requests count toward that key's verification quota. Invalid credentials and unauthorized domains are rejected. Standard forwarding headers alone cannot establish a proxy origin; direct requests use their own host.

Roll out the change

  1. Verify active domain registrations. If the service key has explicit permission restrictions, include the project's auth:proxy grant.
  2. Publish the updated SDK and prepare each proxy application's deployment. It attaches the existing service key automatically.
  3. Deploy the updated Auth service, then immediately deploy the updated proxy applications as a coordinated rollout. Auth must strip and redact the credential header before applications start sending it; older Auth deployments do not provide that protection.

During a mixed-version rollout, legacy custom-origin headers without a service key are rejected, and legacy proxies that send only standard forwarding headers lose their forwarded tenant context. No additional proxy secrets or signing configuration are required; rotation and revocation use the existing API-key management.

Add the catch-all route

For TanStack Start, create src/routes/api/auth.$.tsx:

tsx
import { proxyAuthRequest } from "@krak-stack/auth/server";
import { createFileRoute } from "@tanstack/react-router";

const proxyAuth = (request: Request) =>
  proxyAuthRequest(request, process.env.KRAKSTACK_AUTH_URL!);

export const Route = createFileRoute("/api/auth/$")({
  server: {
    handlers: {
      GET: ({ request }) => proxyAuth(request),
      HEAD: ({ request }) => proxyAuth(request),
      POST: ({ request }) => proxyAuth(request),
      PUT: ({ request }) => proxyAuth(request),
      PATCH: ({ request }) => proxyAuth(request),
      DELETE: ({ request }) => proxyAuth(request),
      OPTIONS: ({ request }) => proxyAuth(request),
    },
  },
});

Keep the wildcard path unchanged so every Better Auth endpoint is forwarded. Do not implement a generic open proxy; the target must come from trusted server configuration.

Compose with an Effect API

If the application already sends /api/* requests through an Effect HTTP router, add the auth proxy to the same router instead of creating a dedicated TanStack Start route:

ts
import { proxyAuthHttpEffect } from "@krak-stack/auth/server";
import { Layer } from "effect";
import { FetchHttpClient, HttpRouter } from "effect/http";
import { HttpApiBuilder } from "effect/http-api";

const authUrl = process.env.KRAKSTACK_AUTH_URL;
if (!authUrl) throw new Error("KRAKSTACK_AUTH_URL is required");

const authProxyLayer = HttpRouter.add(
  "*",
  "/api/auth/*",
  proxyAuthHttpEffect(authUrl, {
    cookieDomain: "example.com",
  }),
).pipe(HttpRouter.provideRequest(FetchHttpClient.layer));

const apiLayer = Layer.mergeAll(
  HttpApiBuilder.layer(Api).pipe(Layer.provide(apiHandlers)),
  authProxyLayer,
);

Replace Api and apiHandlers with the application's existing HttpApi and handler layers. The application's general /api/* server route must pass the request to the Effect router.

Keep the proxy beside HttpApiBuilder.layer(Api) rather than adding it to an HttpApiGroup. The proxy is an opaque wildcard transport that must preserve raw Better Auth requests and responses; it is not a typed application endpoint or an OpenAPI contract. Explicit HttpApi endpoints can remain in the same Effect router and the /api/auth/* route handles the remaining Better Auth traffic.

proxyAuthHttpEffect uses Effect HttpClient, which automatically creates a client span and injects its W3C and B3 trace headers into the upstream request. The auth service's Effect server tracer extracts that context, so both services remain in one distributed trace. Use this helper instead of wrapping proxyAuthRequest in HttpEffect.fromWebHandler, which crosses into a Promise callback before Effect HttpClient can propagate the active span.

Omit cookieDomain for host-only cookies. Set it only when trusted applications on subdomains of the same root domain must share auth cookies.

Use the browser contract

The packaged components and atoms use the application origin by default, so no Better Auth client needs to be created. For a custom Effect client, import the owned browser contract:

ts
import { AuthApiGroup } from "@krak-stack/auth/auth";

Register AuthApiGroup with HttpApi or use the combined AuthClientApi from @krak-stack/auth/api when constructing an Effect HttpApiClient or AtomHttpApi client. Configure that client with the application origin, not the upstream auth origin.

Add the provider

tsx
import { KrakstackAuthProvider } from "@krak-stack/auth/components";

<KrakstackAuthProvider
  locale="en"
  projectId={import.meta.env.VITE_KRAKSTACK_AUTH_PROJECT_ID}
>
  <App />
</KrakstackAuthProvider>;

Configure central auth

Add every application origin to BETTER_AUTH_TRUSTED_ORIGINS and BETTER_AUTH_VALID_AUDIENCES on the auth server. Use exact production origins and HTTPS. Register the project's root hostname when using dynamic domain resolution.

Verify

  1. Open https://app.example.com/api/auth/ok and confirm a successful response.
  2. Sign in and inspect cookies; they should belong to app.example.com.
  3. Refresh a protected page and confirm the server can resolve the session.
  4. Test sign-out, email verification, password reset, and two-factor redirects.