- Integrated BetterAuth library for modern authentication - Added Session, Account, and Verification database tables - Created complete auth module with service, controller, guards, and decorators - Implemented shared authentication types in @mosaic/shared package - Added comprehensive test coverage (26 tests passing) - Documented type sharing strategy for monorepo - Updated environment configuration with OIDC and JWT settings Key architectural decisions: - BetterAuth over Passport.js for better TypeScript support - Separation of User (DB entity) vs AuthUser (client-safe subset) - Shared types package to prevent FE/BE drift - Factory pattern for auth config to use shared Prisma instance Ready for frontend integration (Issue #6). Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> Fixes #4
7.3 KiB
Type Sharing Strategy
This document explains how types are shared between the frontend and backend in the Mosaic Stack monorepo.
Overview
All types that are used by both frontend and backend live in the @mosaic/shared package. This ensures:
- Type safety across the entire stack
- Single source of truth for data structures
- Automatic type updates when the API changes
- Reduced duplication and maintenance burden
Package Structure
packages/shared/
├── src/
│ ├── types/
│ │ ├── index.ts # Main export file
│ │ ├── enums.ts # Shared enums (TaskStatus, etc.)
│ │ ├── database.types.ts # Database entity types
│ │ └── auth.types.ts # Authentication types (NEW)
│ ├── utils/
│ └── constants.ts
└── package.json
Authentication Types
Shared Types (@mosaic/shared)
These types are used by both frontend and backend:
AuthUser
The authenticated user object that's safe to expose to clients.
interface AuthUser {
readonly id: string;
email: string;
name: string;
image?: string;
emailVerified?: boolean;
}
AuthSession
Session data returned after successful authentication.
interface AuthSession {
user: AuthUser;
session: {
id: string;
token: string;
expiresAt: Date;
};
}
Session, Account
Full database entity types for sessions and OAuth accounts.
LoginRequest, LoginResponse
Request/response payloads for authentication endpoints.
OAuthProvider
Supported OAuth providers: "authentik" | "google" | "github"
OAuthCallbackParams
Query parameters from OAuth callback redirects.
Backend-Only Types
Types that are only used by the backend stay in apps/api/src/auth/types/:
BetterAuthRequest
Internal type for BetterAuth handler compatibility (extends web standard Request).
Why backend-only? This is an implementation detail of how NestJS integrates with BetterAuth. The frontend doesn't need to know about it.
Usage Examples
In the Backend (API)
// apps/api/src/auth/auth.controller.ts
import { AuthUser } from "@mosaic/shared";
@Get("profile")
@UseGuards(AuthGuard)
getProfile(@CurrentUser() user: AuthUser) {
return {
id: user.id,
email: user.email,
name: user.name,
};
}
In the Frontend (Web)
// apps/web/app/components/UserProfile.tsx
import type { AuthUser } from "@mosaic/shared";
export function UserProfile({ user }: { user: AuthUser }) {
return (
<div>
<h1>{user.name}</h1>
<p>{user.email}</p>
{user.emailVerified && <Badge>Verified</Badge>}
</div>
);
}
API Response Types
// Both FE and BE can use this
import type { ApiResponse, AuthSession } from "@mosaic/shared";
// Backend returns this
const response: ApiResponse<AuthSession> = {
success: true,
data: {
user: { id: "...", email: "...", name: "..." },
session: { id: "...", token: "...", expiresAt: new Date() },
},
};
// Frontend consumes this
async function login(email: string, password: string) {
const response = await fetch("/api/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
const data: ApiResponse<AuthSession> = await response.json();
if (data.success) {
// TypeScript knows data.data exists and is AuthSession
saveSession(data.data);
}
}
Database Types
The @mosaic/shared package also exports database entity types that match the Prisma schema:
User- Full user entity (includes all fields from DB)Workspace,Task,Event,Project- Other entities- Enums:
TaskStatus,TaskPriority,ProjectStatus, etc.
Key Difference: User vs AuthUser
| Type | Purpose | Fields | Used By |
|---|---|---|---|
User |
Full database entity | All DB fields including sensitive data | Backend internal logic |
AuthUser |
Safe client-exposed subset | Only public fields (no preferences, etc.) | API responses, Frontend |
Example:
// Backend internal logic
import { User } from "@mosaic/shared";
async function updateUserPreferences(userId: string, prefs: User["preferences"]) {
// Has access to all fields including preferences
}
// API response
import { AuthUser } from "@mosaic/shared";
function sanitizeUser(user: User): AuthUser {
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
emailVerified: user.emailVerified,
};
// Preferences and other sensitive fields are excluded
}
Adding New Shared Types
When adding new types that should be shared:
-
Determine if it should be shared:
- ✅ API request/response payloads
- ✅ Database entity shapes
- ✅ Business domain types
- ✅ Enums and constants
- ❌ Backend-only implementation details
- ❌ Frontend-only UI component props
-
Add to the appropriate file:
- Database entities →
database.types.ts - Auth-related →
auth.types.ts - Enums →
enums.ts - General API types →
index.ts
- Database entities →
-
Export from
index.ts:export * from "./your-new-types"; -
Build the shared package:
cd packages/shared pnpm build -
Use in your apps:
import { YourType } from "@mosaic/shared";
Type Versioning
Since this is a monorepo, all packages use the same version of @mosaic/shared. When you:
- Change a shared type - Both FE and BE automatically get the update
- Add a new field - TypeScript will show errors where the field is missing
- Remove a field - TypeScript will show errors where the field is still used
This ensures the frontend and backend never drift out of sync.
Benefits
Type Safety
// If the backend changes AuthUser.name to AuthUser.displayName,
// the frontend will get TypeScript errors everywhere AuthUser is used.
// You'll fix all uses before deploying.
Auto-Complete
// Frontend developers get full autocomplete for API types
const user: AuthUser = await fetchUser();
user. // <-- IDE shows: id, email, name, image, emailVerified
Refactoring
// Rename a field? TypeScript finds all usages across FE and BE
// No need to grep or search manually
Documentation
// The types ARE the documentation
// Frontend developers see exactly what the API returns
Current Shared Types
Authentication (auth.types.ts)
AuthUser- Authenticated user infoAuthSession- Session dataSession- Full session entityAccount- OAuth account entityLoginRequest,LoginResponseOAuthProvider,OAuthCallbackParams
Database Entities (database.types.ts)
User- Full user entityWorkspace,WorkspaceMemberTask,Event,ProjectActivityLog,MemoryEmbedding
Enums (enums.ts)
TaskStatus,TaskPriorityProjectStatusWorkspaceMemberRoleActivityAction,EntityType
API Utilities (index.ts)
ApiResponse<T>- Standard response wrapperPaginatedResponse<T>- Paginated dataHealthStatus- Health check format
Remember: If a type is used by both frontend and backend, it belongs in @mosaic/shared. If it's only used by one side, keep it local to that application.