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/clientprovides React context helpers@tulip-systems/auth/next/serverprovides 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.cookiePrefixadvanced.database.generateIdemailAndPassword.enabled = trueuser.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/clientuseAuthClient(...)from@tulip-systems/auth/clientusePermission(...)from@tulip-systems/auth/permissions/client
These are intended for app-local composition with the concrete client type your app creates.
Recommended app structure
Keep auth composition in the app:
server/auth/init.tsserver/auth/client.ts- app-local router middleware that uses
context.auth.api.getSession(...)
This matches the same design used by the router module:
coreprovides 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
coreown all auth policy