Showcasing you that rollin' your own auth is not that hard.
We also rolled our own analytics in here 😉
The main application runs on a postgres database via drizzle-orm. For the analytics I opted for a seperate database being libsql (turso).
A modern, type-safe, and modular application architecture built with Next.js, focusing on clean separation of concerns and maintainable code structure.
The application follows a layered architecture pattern with clear boundaries between different concerns:
graph TD
DB[(Database)] --> Repo[Repository Layer]
Repo --> Service[Service Layer]
Service --> Actions[Server Actions]
Actions --> Hooks[React Hooks]
Hooks --> Components[React Components]
Components --> Pages[Pages/Views]
- Type Safety: Extensive use of TypeScript with strict type checking
- Modular Design: Feature-based module organization
- Clean Architecture: Clear separation between data, business logic, and presentation
- Functional Approach: Emphasis on functional programming patterns
src/
├── api/
│ ├── db/ # Database configuration and migrations
│ └── queries/ # Global API queries
├── modules/ # Feature modules
│ ├── authenticatie/ # Authentication module
│ ├── notifications/ # Notifications module
│ └── workspaces/ # Workspaces module
├── shared/ # Shared utilities and components
│ ├── components/ # Shared UI components
│ ├── types/ # Common type definitions
│ └── utilities/ # Shared utilities
└── app/ # Next.js app router pages
- Uses Drizzle ORM for type-safe database operations
- Tables defined using Drizzle's schema builder
- Strong typing for all database operations
// Example repository pattern
export function notificationRepository() {
return {
async findByUserId(userId: UUID, options: TGetNotificationsOptions): Promise<TNotification[]>,
async create(data: TCreateNotificationInput): Promise<TNotification>,
// ... other methods
};
}
// Example service layer
export const notificationService = {
async createNotification(data: TCreateNotificationInput): Promise<TNotificationWithActor>,
async getUserNotifications(userId: UUID, options): Promise<TNotificationWithActor[]>,
// ... other methods
};
// Example server action
export async function getUserNotifications(options: TGetNotificationsOptions) {
const session = await getSession();
return await notificationService.getUserNotifications(session.userId, options);
}
// Example hook
export function useNotifications(initialOptions: TGetNotificationsOptions) {
const [notifications, setNotifications] = useState<TNotificationWithActor[]>([]);
// ... implementation
}
// UUID Type for strong ID typing
export type UUID = string & { readonly _brand: unique symbol };
// Base Entity Type
export interface TBaseEntity {
id: UUID;
createdAt: Date;
updatedAt: Date;
}
// Base Response Types
export interface TBaseMutationResponse<T> {
success: boolean;
data?: T;
error?: string;
}
Each module defines its own types that extend from base types:
export interface TNotification extends TBaseEntity {
userId: UUID;
type: TNotificationType;
title: string;
message: string;
// ... other properties
}
- User authentication through email/password or OAuth providers
- Session management using JWT tokens
- Role-based access control (RBAC)
- Protected routes and API endpoints
- Each module is self-contained with its own:
- Repository layer
- Service layer
- Server actions
- React hooks
- Components
- Strict TypeScript configuration
- Type inference from database to UI
- Custom type utilities and guards
- Efficient data fetching
- Proper caching strategies
- Optimized bundle splitting
- Clear separation of concerns
- Consistent patterns across modules
- Strong type safety
- Functional programming approach
- Framework: Next.js
- Language: TypeScript
- Database: PostgreSQL
- ORM: Drizzle
- State Management: React Hooks + Context
- UI Components: Custom components + Shadcn
- Styling: Tailwind CSS
- Authentication: Custom JWT implementation