Backend Finalization: Authentication, Permissions & Logging
5. Adding User Ownership to Books and Authors
Before implementing permissions, we need to add user ownership to our Book and Author models. This allows us to track who created each book and author, which is essential for permission checking.
Step 1: Update Prisma Schema
Add user relationships to Book and Author models:
Step 2: Generate Prisma Client and Migration
After updating the Prisma schema, generate the Prisma client and create a migration:
Important: Don't run the seed script yet - the seed data needs to be updated first to include user IDs.
Step 3: Update Domain Classes
Add field to your domain classes:
Step 4: Create Mappers for Prisma to Domain Conversion
To keep our repository code clean and avoid repetition, create mapper functions for converting Prisma models to domain objects:
Why use mappers?
- DRY principle: Avoid repeating the same mapping logic in every repository method
- Maintainability: Change the mapping in one place when the domain model changes
- Type safety: TypeScript ensures the Prisma model matches the domain model
- Testability: Easy to unit test the mapping logic separately
Step 5: Update Repositories
Now use the mappers in your repositories:
Note: By accepting the full object instead of just a string, the repository has access to the complete user context (email, name, role) if needed for logging, auditing, or permission checks.
Step 6: Update Services
Pass the full object to repository methods. Even though we won't use the parameter in all methods yet, we add it for consistency and future permission checks:
Pattern: Services receive the from resolvers and pass it through to repositories. This maintains a clean separation of concerns and allows repositories to access user context. We add the parameter to update and delete methods even though we don't use them yet - this makes it easier to add permission checks later.
Step 7: Update Resolvers
You will first have to import the AuthModule in both BookModule and AuthorModule:
Do the same for BooksModule:
Add authentication guards and pass the full object:
Key Points:
- All mutations (create, update, delete) are protected with
- All mutations receive the parameter
- Queries don't require authentication (public read access)
- Pass the entire object to services, not just - this follows domain-driven design and provides type safety
Step 8: Update Seed Data
Now update your seed functions to include user relationships (this must be done before running the seed):
Then update your main seed script to pass user IDs when creating books and authors:
Note: Seed functions use plain strings since they operate outside the normal request context and don't need the full object. This is acceptable for seeding data.
Step 9: Run the Seed Script
After updating all the seed functions and the seed database script, you can now run the seed:
This will populate your database with books and authors that are properly associated with users.
6. Extending the Permissions Module
Now let's implement proper permissions for your modules. We'll follow the pattern used in the quacks module.
Understanding the Permissions Pattern
The permissions system uses an Ability Factory pattern that:
- Creates permission rules based on user identity
- Checks permissions in services before performing actions
- Throws appropriate exceptions when permissions are denied
Step 1: Update Your Ability Factory
Update the ability factory to include permissions for your books and authors modules. Here's the complete file:
Step 2: Import PermissionsModule
Now import the PermissionsModule in your modules to use the AbilityFactory and AuthModule to enable our auth guard:
Do the same for BooksModule:
Step 3: Use Permissions in Services
Implement permission checks in your service layer:
Key Changes:
- Import correct domain types (, )
- Use instead of in the repository
- Use instead of in the repository
- Match the actual repository method signatures
Similarly, you can add permission checks to BookService following the same pattern.
Note: Resolvers are already protected with guards (covered in Step 7 of "Adding User Ownership"), so you don't need to add them again.
Step 4: Test Your Permissions
Test different permission scenarios using GraphQL:
6. Better Auth
1. Installing latest version
First, we need to update better-auth to the latest version as the template projects contain an outdated version.
After installation, verify the version in both files. It should be the latest version in both (>= 1.3.28).
2. Understanding better-auth
What is better-auth?
better-auth is a comprehensive authentication solution that handles all the complexity of modern authentication for you. It's a wrapper that manages:
- Tokens: Automatically generates and validates JWT tokens
- Sessions: Manages user sessions with configurable expiration times
- Security: Handles CSRF protection, secure cookies, and password hashing
- Database: Automatically creates and manages all necessary database tables
- Email verification: Built-in support for email verification flows
- Password reset: Complete password reset functionality
How It Works
better-auth uses a schema-based approach where it:
- Creates database tables automatically - You don't need to define user, session, or verification tables
- Handles all authentication endpoints - Login, signup, logout, password reset, email verification
- Manages cookies and sessions - Secure, httpOnly cookies with automatic refresh
- Provides type-safe client - Full TypeScript support for frontend integration
The key benefit is that you don't have to worry about:
- Token generation and validation
- Session expiration and refresh logic
- Password hashing and salt generation
- Email verification token management
- CSRF protection implementation
better-auth handles all of this behind the scenes, letting you focus on your application logic.
Database Tables Created by better-auth
When you run migrations with better-auth configured, it automatically creates these tables:
Based on which better-auth plugins you use, you might need to add additional fields to these tables or create new tables.
3. Implementing Email Verification
Backend Configuration
To require email verification, you need to update the better-auth configuration:
Important: Set the "from" address in SMTP adapter
By default, the SMTP adapter uses a placeholder sender address. Update it to an address that belongs to your domain (or your SMTP provider may reject or spam‑flag emails).
Fix: betterAuth config ordering
Previously, we spread too low inside , which could override your custom fields. To avoid overrides, spread it at the very top of the options object so anything defined below wins.
Incorrect (core config at the bottom can override your values):
Correct (core config first, then layer customizations):
This is a mistake on our part, sorry for the inconvenience.
4. Understanding better-auth Plugins
What Are Plugins?
better-auth has a powerful plugin system that extends its core functionality. Plugins can:
- Add new authentication methods (OAuth providers, magic links, etc.)
- Extend database schema with additional fields
- Add new API endpoints
- Modify authentication flows
- Add features like organizations, teams, social providers, etc.
Plugins are completely optional but highly recommended for features like:
- Organizations & Teams Management - Multi-tenant applications
- Social Providers - Google, GitHub, Discord, etc.
- Multi-factor Authentication - TOTP, SMS verification
- Magic Links - Passwordless authentication
Using plugins saves you from writing all this code yourself - you can simply use better-auth's API endpoints and hooks.
Exploring Available APIs
All available better-auth endpoints are documented at:
This OpenAPI documentation shows all the authentication endpoints that better-auth provides. The frontend client handles most of these under the hood, so you rarely need to call them directly from the backend.
Understanding Core Config vs Provider Config
Our NestJS integration uses a two-file approach to handle better-auth configuration:
1. Core Config ()
This file contains the minimal configuration needed for database schema generation:
When to add plugins here:
- When the plugin requires database tables/columns
- You want to use to generate the schema
- The plugin configuration doesn't need NestJS providers (like EmailService)
2. Provider Config ()
This file contains the full runtime configuration that actually runs in your application:
When to add configuration here:
- When you need NestJS providers (EmailService, ConfigService, etc.)
- For runtime settings that don't affect database schema
- To override or extend core config settings
Why This Two-File Approach?
This is a slightly hacky solution to solve a specific problem:
- needs a standalone better-auth instance to read the schema
- But our runtime configuration needs NestJS dependency injection
- So we split: core config for schema generation, provider config for runtime
Alternative Approach (Simpler):
If this two-file approach is confusing, you can skip entirely:
- Delete
- Put everything in
- Manually add database tables/columns from plugin documentation to your Prisma schema
- Don't use
This is actually the recommended approach for most projects - it's more explicit and less magical.