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 instanceAuth.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.cookiePrefixadvanced.database.generateIdemailAndPassword.enabled = trueuser.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.
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 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
coreown all auth policy