Files
stack/docs/TYPE-SHARING.md
Jason Woltje 6a038d093b feat(#4): Implement Authentik OIDC authentication with BetterAuth
- 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
2026-01-28 17:26:34 -06:00

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:

  1. 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
  2. Add to the appropriate file:

    • Database entities → database.types.ts
    • Auth-related → auth.types.ts
    • Enums → enums.ts
    • General API types → index.ts
  3. Export from index.ts:

    export * from "./your-new-types";
    
  4. Build the shared package:

    cd packages/shared
    pnpm build
    
  5. 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:

  1. Change a shared type - Both FE and BE automatically get the update
  2. Add a new field - TypeScript will show errors where the field is missing
  3. 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 info
  • AuthSession - Session data
  • Session - Full session entity
  • Account - OAuth account entity
  • LoginRequest, LoginResponse
  • OAuthProvider, OAuthCallbackParams

Database Entities (database.types.ts)

  • User - Full user entity
  • Workspace, WorkspaceMember
  • Task, Event, Project
  • ActivityLog, MemoryEmbedding

Enums (enums.ts)

  • TaskStatus, TaskPriority
  • ProjectStatus
  • WorkspaceMemberRole
  • ActivityAction, EntityType

API Utilities (index.ts)

  • ApiResponse<T> - Standard response wrapper
  • PaginatedResponse<T> - Paginated data
  • HealthStatus - 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.