Nitrofy LogoNitrofy
Como funcionaBeneficiosIntegracoesPlanosDuvidas
Comece agora
Nitrofy Logo
Comecar
Introduction
QuickstartEnvironment SetupRunning LocallyFirst Deploy
Organizations & TenancyAuthentication & SessionsRoles & PermissionsControllers & ProceduresJobs & QueuesPlugin ManagerContent LayerBuilt-in MCP ServerBillingNotificationsFile StorageEmailWebhooksAPI KeysSEOData FetchingDesign System
Development

Authentication & Sessions

Learn how the SaaS Boilerplate handles user authentication, session management, and organization-based access control using Better Auth.

By the end of this guide, you'll understand how authentication works in the SaaS Boilerplate, how sessions manage user state and organization context, and how to implement secure access control in your features.

Before You Begin

  • Basic Knowledge: Familiarity with web authentication concepts and TypeScript
  • Environment Setup: A running instance with Better Auth configured (see Environment Setup)
  • Database: PostgreSQL with migrations applied
  • Optional: OAuth provider credentials for social login

Core Concepts

The SaaS Boilerplate uses Better Auth as its authentication foundation, enhanced with organization management and multi-tenant access control. This creates a robust system where users can authenticate, manage multiple organizations, and access resources securely.

Authentication Methods

The system supports multiple authentication flows to accommodate different user preferences and security needs:

  • Email & Password: Traditional username/password authentication
  • Email OTP: One-time passwords sent via email for passwordless login
  • Social Providers: OAuth integration with GitHub and Google
  • Two-Factor Authentication: TOTP-based 2FA for enhanced security
  • API Keys: Programmatic access for integrations and automation

Session Management

Sessions are the cornerstone of user state management. Each session contains:

  • User Identity: Basic user information and metadata
  • Organization Context: The currently active organization and user's role within it
  • Billing Information: Current subscription status and payment details
  • Security Tokens: Encrypted session data stored in HTTP-only cookies

Sessions automatically handle organization switching, ensuring all subsequent requests respect the active organization's boundaries.

Organization-Based Access Control

Unlike simple user-based permissions, the SaaS Boilerplate implements organization-scoped access:

  • Multi-Tenancy: Each organization operates as an isolated tenant
  • Role-Based Permissions: Users have different roles (owner, admin, member) within organizations
  • Data Isolation: All business data is automatically scoped by organization ID
  • Membership Management: Users can belong to multiple organizations with different roles

API Key Authentication

For programmatic access, the system supports API keys that bypass traditional user sessions:

  • Organization-Scoped: API keys belong to organizations, not individual users
  • Role Requirements: Keys can only access endpoints requiring specific organization roles
  • Expiration Control: Keys can be set to never expire or have custom expiration dates
  • Audit Trail: All API key usage is logged for security monitoring

Data Models

The authentication system defines several TypeScript interfaces that govern how sessions and permissions work throughout the application.

Prop

Type

Implementation Details

The authentication system is implemented through a layered architecture that provides both low-level auth services and high-level procedures for business logic.

Configure Better Auth Service

The foundation is set up in src/services/auth.ts, which initializes Better Auth with all necessary plugins and configurations.

export const auth = betterAuth({
  baseURL: Url.get(),
  secret: AppConfig.providers.auth.secret,
  database: prismaAdapter(prisma, { provider: 'postgresql' }),
  socialProviders: {
    github: {
      clientId: AppConfig.providers.auth.providers.github.clientId,
      clientSecret: AppConfig.providers.auth.providers.github.clientSecret,
    },
    google: {
      clientId: AppConfig.providers.auth.providers.google.clientId,
      clientSecret: AppConfig.providers.auth.providers.google.clientSecret,
    },
  },
  account: {
    accountLinking: {
      enabled: true,
    },
  },
  plugins: [
    twoFactor(),
    organization({
      sendInvitationEmail: async ({ email, organization, id }) => {
        await mail.send({
          to: email,
          template: 'organization-invite',
          data: {
            email,
            organization: organization.name,
            url: Url.get(`/auth?invitation=${id}`),
          },
        })
      },
    }),
    emailOTP({
      async sendVerificationOTP({ email, otp, type }) {
        const subjectMap = {
          'sign-in': 'Your Access Code',
          'email-verification': 'Verify Your Email',
          'forget-password': 'Password Recovery',
          default: 'Verification Code',
        }

        const subject = subjectMap[type] || subjectMap.default

        await mail.send({
          to: email,
          subject,
          template: 'otp-code',
          data: {
            email,
            otpCode: otp,
            expiresInMinutes: 10,
          },
        })
      },
    }),
    nextCookies(),
  ],
})

This configuration enables social login, email OTP, two-factor authentication, and organization management with invitation emails.

Inject Authentication Context

The AuthFeatureProcedure wraps all authenticated endpoints, providing a unified authentication context that includes session management, organization switching, and role validation.

export const AuthFeatureProcedure = igniter.procedure({
  name: 'AuthFeatureProcedure',
  handler: async (options, { request, response, context }) => {
    return {
      auth: {
        setActiveOrganization: async (input: { organizationId: string }) => {
          // Business Logic: Switch the user's active organization using the auth service
          await tryCatch(
            context.services.auth.api.setActiveOrganization({
              body: input,
              headers: request.headers,
            }),
          )
        },
        // ... other auth methods
        getSession: async (options?: GetSessionInput<TRequirements, TRoles>) => {
          // Complex session retrieval logic with role validation
        },
      },
    }
  },
})

Create Authentication Controllers

Controllers expose authentication endpoints that handle sign-in flows, session management, and organization switching.

signInWithProvider: igniter.mutation({
  name: 'signInWithProvider',
  description: 'Sign in with OAuth provider',
  method: 'POST',
  path: '/sign-in',
  use: [AuthFeatureProcedure()],
  body: z.object({
    provider: z.string(),
    callbackURL: z.string().optional(),
  }),
  handler: async ({ request, response, context }) => {
    const { provider, callbackURL } = request.body

    const result = await context.auth.signInWithProvider({
      provider: provider as AccountProvider,
      callbackURL,
    })

    if (result.error) {
      throw new Error(result.error.code)
    }

    return response.success(result.data)
  },
}),

Implement Role-Based Access Control

In your business logic controllers, use the authentication context to enforce permissions and data isolation.

list: igniter.query({
  name: 'List',
  description: 'List all leads for an organization.',
  path: '/',
  use: [AuthFeatureProcedure(), LeadProcedure()],
  handler: async ({ context, response }) => {
    const session = await context.auth.getSession({
      requirements: 'authenticated',
      roles: ['admin', 'owner', 'member'],
    })

    if (!session || !session.organization) {
      return response.unauthorized(
        'Authentication required and active organization needed.',
      )
    }

    const organizationId = session.organization.id
    const leads = await context.lead.findMany(organizationId)

    return response.success(leads)
  },
}),

Practical Examples

Let's see how authentication integrates with real business features in the SaaS Boilerplate.

Lead Management with Organization Scoping

The lead feature demonstrates how authentication ensures data isolation between organizations. Each lead operation automatically scopes to the user's active organization.

create: igniter.mutation({
  name: 'Create',
  description: 'Create a new lead.',
  path: '/',
  method: 'POST',
  use: [
    AuthFeatureProcedure(),
    LeadProcedure(),
    IntegrationFeatureProcedure(),
  ],
  body: LeadCreationSchema,
  handler: async ({ context, request, response }) => {
    const session = await context.auth.getSession({
      requirements: 'authenticated',
      roles: ['admin', 'owner', 'member'],
    })

    if (!session || !session.organization) {
      return response.unauthorized(
        'Authentication required and active organization needed.',
      )
    }

    const organizationId = session.organization.id
    const { email, name, phone, metadata } = request.body

    const lead = await context.lead.create(organizationId, {
      email,
      name,
      phone,
      metadata,
    })

    return response.success(lead)
  },
}),

API Key Authentication for Integrations

For programmatic access, API keys provide organization-scoped authentication without user sessions.

getSession: async (options?: GetSessionInput<TRequirements, TRoles>) => {
  const session = await context.services.auth.api.getSession({
    headers: request.headers,
  })

  // Security Rule: Check for API Key authentication if no regular session exists
  let apiKeyOrganization = null
  if (!session) {
    const authHeader = request.headers.get('Authorization')

    if (authHeader && authHeader.startsWith('Bearer ')) {
      const token = authHeader.substring(7)

      const apiKey = await context.services.database.apiKey.findUnique({
        where: {
          key: token,
          enabled: true,
        },
        include: {
          organization: true,
        },
      })

      if (apiKey) {
        if (
          !apiKey.neverExpires &&
          apiKey.expiresAt &&
          new Date() > apiKey.expiresAt
        ) {
          throw new Error('API_KEY_EXPIRED')
        }

        if (!options?.roles || options.roles.length === 0) {
          throw new Error('API_KEY_REQUIRES_ORGANIZATION_ENDPOINT')
        }

        apiKeyOrganization = apiKey.organization
      }
    }
  }
  // ... rest of session logic
}

Error Codes

The authentication system provides specific error codes to help developers understand and handle different authentication scenarios. These errors are thrown during session validation and can be caught and handled appropriately in your application.

Prop

Type

Troubleshooting

Best Practices

Organizations & Tenancy

Understand multi-tenant isolation, membership roles, and how the active org shapes access control.

Roles & Permissions

Learn how role-based access control (RBAC) works in the SaaS Boilerplate, including membership roles, permission validation, and organization-scoped access control.

On this page

Before You BeginCore ConceptsAuthentication MethodsSession ManagementOrganization-Based Access ControlAPI Key AuthenticationData ModelsImplementation DetailsConfigure Better Auth ServiceInject Authentication ContextCreate Authentication ControllersImplement Role-Based Access ControlPractical ExamplesLead Management with Organization ScopingAPI Key Authentication for IntegrationsError CodesTroubleshootingBest Practices
Nitrofy LogoNitrofy

Automatize o envio e a cobrança dos seus contratos

© 2026 Nitrofy, All rights reserved