better-auth
Originally fromjezweb/claude-skills
SKILL.md
Better Auth Patterns
Overview
Implement authentication and authorization using Better Auth with TRPC procedures following the project's established patterns.
When to Use This Skill
- Configuring Better Auth settings
- Creating protected TRPC procedures
- Implementing organization/project access control
- Working with sessions and user roles
- Setting up OAuth providers
Auth Configuration
// apps/web-app/src/auth/auth.ts
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { admin, organization } from "better-auth/plugins";
export const auth = betterAuth({
baseURL: serverEnv.BETTER_AUTH_URL,
secret: serverEnv.BETTER_AUTH_SECRET,
database: drizzleAdapter(db, {
provider: "pg",
schema: {
user: usersTable,
session: sessionsTable,
account: accountsTable,
verification: verificationsTable,
organization: organizationsTable,
member: membersTable,
invitation: invitationsTable,
},
}),
session: {
cookieCache: {
enabled: true,
maxAge: 5 * 60, // 5 minutes cache
},
},
socialProviders: {
google: {
clientId: serverEnv.GOOGLE_CLIENT_ID,
clientSecret: serverEnv.GOOGLE_CLIENT_SECRET,
},
},
emailAndPassword: { enabled: true },
plugins: [admin(), organization({ sendInvitationEmail: async () => {} })],
});
Auth-Related Database Tables
Core Tables:
usersTable- User accounts withrolefield (admin|user)sessionsTable- Auth sessions withactiveOrganizationIdaccountsTable- OAuth accounts (stores access/refresh tokens)
Organization Tables:
organizationsTable- OrganizationsmembersTable- Organization members withrole(owner|admin|member)invitationsTable- Pending invitations
Project Tables:
projectsTable- Projects (belong to organizations)projectMembersTable- Project members withrole(admin|editor|viewer)
TRPC Context & Session
// apps/web-app/src/infrastructure/trpc/init.ts
export const createTRPCContext = async ({ headers }: { headers: Headers }) => {
const session = await auth.api.getSession({ headers });
return { db, session, headers };
};
Protected Procedure Patterns
Base Auth Procedures
// apps/web-app/src/infrastructure/trpc/procedures/auth.ts
const enforceUserIsAuthenticated = t.middleware(({ ctx, next }) => {
if (!ctx.session?.user) throw unauthorizedError();
return next({
ctx: {
session: { ...ctx.session, user: ctx.session.user },
userId: ctx.session.user.id as UserId,
},
});
});
const enforceUserIsAdmin = t.middleware(async ({ ctx, next }) => {
if (!ctx.session?.user || ctx.session.user.role !== "admin") throw unauthorizedError();
return next({ ctx });
});
export const publicProcedure = t.procedure.use(debugMiddleware).use(sentryMiddleware);
export const protectedProcedure = publicProcedure.use(enforceUserIsAuthenticated);
export const adminProcedure = publicProcedure.use(enforceUserIsAdmin);
Organization Access Procedures
// apps/web-app/src/infrastructure/trpc/procedures/organization.ts
import { OrganizationId } from "@project/common";
// Member access - any org member
export const protectedOrganizationMemberProcedure = protectedProcedure
.input(Schema.standardSchemaV1(Schema.Struct({ organizationId: OrganizationId })))
.use(async function isMemberOfOrganization(opts) {
const memberAccess = await opts.ctx.db
.select()
.from(membersTable)
.where(
and(
eq(membersTable.organizationId, opts.input.organizationId),
eq(membersTable.userId, opts.ctx.userId),
),
)
.limit(1);
if (memberAccess.length === 0)
throw forbiddenError("You are not a member of this organization");
return opts.next({
ctx: {
member: memberAccess[0],
organizationId: opts.input.organizationId,
},
});
});
// Admin access - org admin/owner only
export const protectedOrganizationAdminProcedure = protectedProcedure
.input(Schema.standardSchemaV1(Schema.Struct({ organizationId: OrganizationId })))
.use(async function isAdminOfOrganization(opts) {
const memberAccess = await opts.ctx.db
.select()
.from(membersTable)
.where(
and(
eq(membersTable.organizationId, opts.input.organizationId),
eq(membersTable.userId, opts.ctx.userId),
or(eq(membersTable.role, "admin"), eq(membersTable.role, "owner")),
),
)
.limit(1);
if (memberAccess.length === 0) throw forbiddenError("Admin access required");
return opts.next({
ctx: {
member: memberAccess[0],
organizationId: opts.input.organizationId,
},
});
});
Project Access Procedures
// apps/web-app/src/infrastructure/trpc/procedures/project-access.ts
// Single optimized query - checks both org and project membership
export const protectedProjectMemberProcedure = protectedProcedure
.input(Schema.standardSchemaV1(Schema.Struct({ projectId: ProjectId })))
.use(async function hasProjectAccess(opts) {
const result = await ctx.db
.select({
projectId: projectsTable.id,
organizationId: projectsTable.organizationId,
orgMemberRole: membersTable.role,
projectMemberRole: projectMembersTable.role,
})
.from(projectsTable)
.leftJoin(
membersTable,
and(
eq(membersTable.organizationId, projectsTable.organizationId),
eq(membersTable.userId, ctx.userId),
),
)
.leftJoin(
projectMembersTable,
and(
eq(projectMembersTable.projectId, projectsTable.id),
eq(projectMembersTable.userId, ctx.userId),
),
)
.where(eq(projectsTable.id, projectId));
// Org admins get automatic project admin access
const isOrgAdmin = data.orgMemberRole === "admin" || data.orgMemberRole === "owner";
if (isOrgAdmin) {
return opts.next({
ctx: {
project,
projectRole: "admin",
orgRole: data.orgMemberRole,
},
});
}
// Check explicit project membership...
});
// Chained procedure for admin-only
export const protectedProjectAdminProcedure = protectedProjectMemberProcedure.use(
async function requiresProjectAdmin(opts) {
if (ctx.orgRole === "admin" || ctx.orgRole === "owner" || ctx.projectRole === "admin")
return opts.next({ ctx });
throw forbiddenError("Project admin permissions required");
},
);
Available Procedures Summary
| Procedure | Access Level | Context Provided |
|---|---|---|
publicProcedure |
No auth | { db, session?, headers } |
protectedProcedure |
Authenticated | { db, session, userId, headers } |
adminProcedure |
Admin role | { db, session, headers } |
protectedOrganizationMemberProcedure |
Org member | { ..., member, organizationId } |
protectedOrganizationAdminProcedure |
Org admin/owner | { ..., member, organizationId } |
protectedProjectMemberProcedure |
Project access | { ..., project, projectRole, orgRole } |
protectedProjectAdminProcedure |
Project admin | { ..., project, projectRole, orgRole } |
protectedProjectEditorProcedure |
Project editor+ | { ..., project, projectRole, orgRole } |
Client-Side Auth
// apps/web-app/src/auth/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { adminClient, organizationClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
betterAuthBaseUrl,
plugins: [adminClient(), organizationClient()],
});
export const { signIn, signOut, useSession, getSession } = authClient;
// Sign in with redirect
export const signInWithEmail = async (
email: string,
password: string,
callbackURL = "/app/dashboard",
) => {
return withAuthRedirect((callbacks) => signIn.email({ email, password }, callbacks), callbackURL);
};
export const signInWithGoogle = async (callbackURL = "/app/dashboard") => {
return signIn.social({ provider: "google", callbackURL });
};
Admin API Usage
// Using Better Auth server API in TRPC procedures
export const router = {
setUserAdmin: adminProcedure.mutation(async ({ ctx, input }) => {
const users = await auth.api.listUsers({
headers: ctx.headers,
query: {
searchField: "email",
searchValue: input.email,
},
});
await auth.api.setRole({
headers: ctx.headers,
body: {
userId: user.id,
role: input.isAdmin ? "admin" : "user",
},
});
}),
banUser: adminProcedure.mutation(async ({ ctx, input }) => {
await auth.api.banUser({
headers: ctx.headers,
body: { userId: input.userId, banReason },
});
}),
};
Key Rules
- Use appropriate procedure for access level needed
- Org admins get automatic project access - don't duplicate checks
- Single query for access checks - use JOINs, not multiple queries
- Pass headers to auth.api calls for session context
- Chain procedures for more specific access (e.g.,
protectedProjectAdminProcedure)
Weekly Installs
35
Repository
blogic-cz/blogiā¦ketplaceGitHub Stars
3
First Seen
14 days ago
Security Audits
Installed on
opencode35
claude-code34
codex20
gemini-cli19
github-copilot19
amp19