better-auth

Implement authentication and authorization using Better Auth with TRPC procedures following the project's established patterns.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "better-auth" with this command: npx skills add blogic-cz/blogic-marketplace/blogic-cz-blogic-marketplace-better-auth

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 with role field (admin | user )

  • sessionsTable

  • Auth sessions with activeOrganizationId

  • accountsTable

  • OAuth accounts (stores access/refresh tokens)

Organization Tables:

  • organizationsTable

  • Organizations

  • membersTable

  • Organization members with role (owner | admin | member )

  • invitationsTable

  • Pending invitations

Project Tables:

  • projectsTable

  • Projects (belong to organizations)

  • projectMembersTable

  • Project members with role (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 )

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

marketing-expert

No summary provided by upstream source.

Repository SourceNeeds Review
General

debugging-with-opensrc

No summary provided by upstream source.

Repository SourceNeeds Review
General

testing-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
General

requirements

No summary provided by upstream source.

Repository SourceNeeds Review