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:

  • @tulip-systems/auth/client provides React context helpers
  • @tulip-systems/auth/next/server provides Next.js server helpers
  • apps own their Better Auth database schema and migrations
  • apps configure Better Auth plugins directly

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

Server entrypoint

Use Better Auth directly to create your auth instance. Import Tulip's shared constants from focused subpaths and keep database schema definitions inside the app.

import { passkey } from "@better-auth/passkey";
import { authAdditionalFields } from "@tulip-systems/auth";
import { authCookiePrefix } from "@tulip-systems/auth/next/server";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { betterAuth } from "better-auth/minimal";
import { admin, emailOTP } from "better-auth/plugins";

export const auth = betterAuth({
  advanced: {
    cookiePrefix: authCookiePrefix,
    database: { generateId: () => false as const },
  },
  baseURL: {
    allowedHosts: ["*.newcode.be", "*.vercel.app", "*.localhost:*"],
  },
  database: drizzleAdapter(db, {
    provider: "pg",
    usePlural: true,
  }),
  emailAndPassword: {
    enabled: true,
  },
  user: {
    additionalFields: authAdditionalFields,
    changeEmail: {
      enabled: true,
    },
  },
  plugins: [
    passkey(),
    admin({ ac, roles, adminRoles: ["admin", "developer"], defaultRole: "user" }),
    emailOTP({ sendVerificationOTP }),
  ],
});

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

Server defaults

Apps usually keep these Better Auth defaults consistent:

  • 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 and email delivery behavior.

Client entrypoint

Create the client with Better Auth directly. Use @tulip-systems/auth/client for Tulip's React context helpers.

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

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/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 focused entrypoints:

  • useSession(...) from @tulip-systems/auth/client
  • useAuthClient(...) from @tulip-systems/auth/client
  • usePermission(...) from @tulip-systems/auth/permissions/client

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 integration points live in Tulip subpaths
  • 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