Tulip Logo IconTulip
Auth

Auth

Tulip auth primitives for Better Auth server setup, client transport, and app-local auth composition.

Tulip's auth module keeps the core surface intentionally small:

  • Auth.init(...) bootstraps a Better Auth server instance
  • Auth.plugins.* provides only Tulip-specific plugin helpers
  • apps compose their own client plugins, middleware, and protected procedures locally

This keeps core light while preserving Better Auth's own inference.

Server entrypoint

Use @tulip-systems/core/auth/server to create your auth instance.

import { passkey } from "@better-auth/passkey";
import { Auth, authAdditionalFields } from "@tulip-systems/core/auth/server";

export const auth = Auth.init({
  db,
  baseURL: {
    allowedHosts: ["*.newcode.be", "*.vercel.app", "*.localhost:*"],
  },
  user: {
    additionalFields: authAdditionalFields,
  },
  plugins: [
    passkey(),
    Auth.plugins.admin({ ac, roles }),
    Auth.plugins.emailOTP({
      email,
      defaultFrom: config.email.defaultFrom,
    }),
  ],
});

export type AuthServer = typeof auth;
export type AuthServerOptions = typeof auth.options;

What Auth.init(...) owns

Tulip applies the server defaults that should be consistent across apps:

  • drizzleAdapter(db, { provider: "pg", usePlural: true })
  • advanced.cookiePrefix
  • advanced.database.generateId
  • emailAndPassword.enabled = true
  • user.changeEmail.enabled = true

Apps remain responsible for choosing Better Auth plugins.

Built-in Tulip plugin helpers

Tulip currently wraps only the pieces that are app-specific enough to justify a helper.

Auth.plugins.admin(...)

Wraps Better Auth's admin plugin and keeps Tulip's default admin roles.

Auth.plugins.admin({ ac, roles })

Auth.plugins.emailOTP(...)

Wraps Better Auth's email OTP plugin and lets you inject app-specific mail delivery.

Auth.plugins.emailOTP({
  email,
  defaultFrom: config.email.defaultFrom,
})

Client entrypoint

Use @tulip-systems/core/auth/client for the shared client transport and React context helpers.

import { passkeyClient } from "@better-auth/passkey/client";
import { createAuthClient } from "@tulip-systems/core/auth/client";
import { adminClient, emailOTPClient, inferAdditionalFields } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  baseURL: url(),
  plugins: [
    emailOTPClient(),
    passkeyClient(),
    adminClient({ ac, roles }),
    inferAdditionalFields<AuthServer>(),
  ],
});

export type AppAuthClient = typeof authClient;

Auth provider and client hook

AuthProvider only cares that the value is one of Tulip's auth clients. It does not need to know the full plugin tuple. The exact client type can be recovered at the usage site.

import { AuthProvider } from "@tulip-systems/core/auth/client";
import { authClient } from "@/server/auth/client";

export default function Layout(props: { children: React.ReactNode }) {
  return <AuthProvider authClient={authClient}>{props.children}</AuthProvider>;
}

When you need the exact client type back:

const authClient = useAuthClient<typeof authClient>();

For common shared code, the default hook type is usually enough:

const authClient = useAuthClient();

Session and permission hooks

Tulip exports client-side helpers from the same entrypoint:

  • useSession(...)
  • useAuthClient(...)
  • usePermission(...)

These are intended for app-local composition with the concrete client type your app creates.

Keep auth composition in the app:

  1. server/auth/init.ts
  2. server/auth/client.ts
  3. app-local router middleware that uses context.auth.api.getSession(...)

This matches the same design used by the router module:

  • core provides the base primitives
  • apps own the concrete policy and middleware composition

Why Tulip keeps this minimal

Tulip does not try to mirror every Better Auth plugin through core.

That means:

  • standard Better Auth plugins can be installed directly in apps
  • only Tulip-specific integrations live behind Auth.plugins.*
  • type inference stays closer to Better Auth's native API surface

This is especially useful for:

  • passkeys
  • contract-first routers with app-local auth middleware
  • future OAuth 2.1 / provider integrations without making core own all auth policy

On this page