The official web platform for the Linux & Open-Source Lovers Community (LOSL-C), built with Next.js frontend and Express.js backend.
LOSL-C is more than just a Linux user group. We're passionate advocates for open-source software, cybersecurity excellence, digital freedom, and technological empowerment across Africa. Our community is dedicated to fostering the next generation of African tech leaders through collaboration, knowledge sharing, and hands-on learning.
loslc.tech/
βββ frontend/ # Next.js React application
βββ backend/ # Express.js API server
βββ docker-compose.yml # Multi-service orchestration
βββ .env.example # Environment configuration
- π¨ Modern UI/UX - Built with Next.js, TailwindCSS, and shadcn/ui
- π Authentication System - Complete user management with sessions and 2FA
- π₯ Role-Based Access Control - Flexible permissions and role management
- π± Responsive Design - Mobile-first approach with dark/light themes
- ποΈ Database Integration - PostgreSQL with Drizzle ORM
- π³ Docker Support - Containerized development and deployment
- π API Versioning - RESTful API with proper versioning
- π‘οΈ Security First - Input validation, sanitization, and secure headers
- Node.js (v18 or higher)
- Bun (v1.1.29 or higher)
- PostgreSQL (v14 or higher)
- Docker (optional, recommended)
# Clone the repository
git clone <repository-url>
cd loslc.tech
# Copy environment files
cp .env.example .env
# Start all services
docker-compose up -d
# The application will be available at:
# Frontend: http://localhost:3000
# Backend API: http://localhost:8000
# Database: localhost:5432# Clone the repository
git clone <repository-url>
cd loslc.tech
# Setup Backend
cd backend
cp .env.example .env
bun install
bun run db:migrate
bun run dev
# Setup Frontend (in another terminal)
cd ../frontend
bun install
bun run devThe application uses multiple environment files for different components:
- Root
.env- Docker Compose and global configuration - Backend
.env- Backend-specific settings - Frontend
.env.local- Frontend-specific settings
# ============================================================================
# π³ Docker Compose Configuration
# ============================================================================
FRONTEND_PORT=3000
BACKEND_PORT=8000
# ============================================================================
# ποΈ Database Configuration
# ============================================================================
PG_USER=loslc_user
PG_PASSWORD=secure_password_here
PG_DB=loslc_database
DATABASE_URL=postgresql://${PG_USER}:${PG_PASSWORD}@db:5432/${PG_DB}
# ============================================================================
# π Application URLs
# ============================================================================
APP_URL=https://loslc.tech
BACKEND_URL=http://backend:8000 # For Docker, use http://localhost:8000 for local dev
# ============================================================================
# π§ Email Configuration
# ============================================================================
MAIL_SERVICE=gmail
APP_EMAIL=no-reply@loslc.tech
SADMIN_EMAIL=admin@loslc.tech
SMTP_PASSWORD=your-smtp-app-password-here
# ============================================================================
# π Security & Authentication
# ============================================================================
OTP_EXPIRATION_MINUTES=10
AUTH_SESSION_EXPIRATION_DAYS=7
EMAIL_VERIFICATION_EXPIRATION_HOURS=24
PASSWORD_RESET_EXPIRATION_MINUTES=30
# ============================================================================
# π Development
# ============================================================================
DEBUG=true
STORAGE=fs/storage# Application settings
APP_URL=https://loslc.tech
PORT=8000
DEBUG=true
# Database
DATABASE_URL=postgresql://username:password@localhost:5432/database
# Email configuration
MAIL_SERVICE=gmail
APP_EMAIL=no-reply@loslc.tech
SADMIN_EMAIL=admin@loslc.tech
SMTP_PASSWORD=your-smtp-password
# Authentication timeouts
OTP_EXPIRATION_MINUTES=10
AUTH_SESSION_EXPIRATION_DAYS=7
EMAIL_VERIFICATION_EXPIRATION_HOURS=24
PASSWORD_RESET_EXPIRATION_MINUTES=30
# File storage
STORAGE=fs/storage# Backend API URL
BACKEND_URL=http://localhost:8000-
Copy example files:
# Root configuration for Docker cp .env.example .env # Backend configuration cp backend/.env.example backend/.env # Frontend configuration cp frontend/.env.example frontend/.env.local
-
Update database credentials:
- Change
PG_USER,PG_PASSWORD, andPG_DBin root.env - Update
DATABASE_URLin backend.env
- Change
-
Configure email service:
- Set up Gmail App Password or other email service
- Update
SMTP_PASSWORDand email addresses
-
Set application URLs:
- For development: Use
localhostURLs - For production: Use your domain names
- For development: Use
- Never commit actual credentials to version control
- Use strong passwords for database and email accounts
- Enable 2FA on email accounts used for SMTP
- Use environment-specific URLs (localhost for dev, domain for prod)
- Set
DEBUG=falsein production environments
frontend/
βββ src/
β βββ app/ # App Router pages
β β βββ layout.tsx # Root layout
β β βββ page.tsx # Home page
β β βββ globals.css # Global styles
β β βββ [pages]/ # Additional pages
β βββ components/
β β βββ ui/ # shadcn/ui components
β β βββ core/ # Core application components
β β βββ providers/ # Context providers
β βββ lib/
β βββ utils.ts # Utility functions
βββ public/ # Static assets
βββ package.json
βββ next.config.ts
βββ tailwind.config.js
βββ tsconfig.json
backend/
βββ src/
β βββ main.ts # Application entry point
β βββ api/
β β βββ v1/
β β βββ router.ts # Main API router
β β βββ controllers/ # Route handlers
β β βββ providers/ # Business logic
β β βββ dto/ # Data transfer objects
β βββ core/
β β βββ db/
β β β βββ db.ts # Database configuration
β β β βββ schema/ # Database schemas
β β βββ security/ # Security utilities
β β βββ utils/ # Utility functions
β βββ migrations/ # Database migrations
βββ drizzle.config.ts # Drizzle ORM configuration
βββ package.json
βββ tsconfig.json
Our development workflow follows a structured approach that ensures type safety, maintainability, and scalability. Here's the recommended process for adding new features:
Start by defining your data structure in the database schema:
// backend/src/core/db/schema/posts.ts
export const postsTable = pgTable("posts", {
id: varchar("id").primaryKey().$defaultFn(() => randId(20)),
title: varchar("title", { length: 255 }).notNull(),
content: text("content").notNull(),
authorId: varchar("author_id").references(() => usersTable.id),
published: boolean("published").default(false),
createdAt: timestamp("created_at").defaultNow().notNull(),
});cd backend
bun run db:generate # Creates migration files
bun run db:migrate # Applies to databaseDTOs are essential for type safety and API contracts:
// backend/src/api/v1/dto/posts.ts
export interface CreatePostRequest {
title: string;
content: string;
published?: boolean;
}
export interface PostResponse {
id: string;
title: string;
content: string;
authorId: string;
published: boolean;
createdAt: Date;
updatedAt: Date;
}
export interface UpdatePostRequest {
title?: string;
content?: string;
published?: boolean;
}Providers handle database operations and business rules:
// backend/src/api/v1/providers/posts.ts
import { db } from "@/core/db/db";
import { postsTable } from "@/core/db/schema/posts";
import type { CreatePostRequest, PostResponse } from "../dto/posts";
export const postsProvider = {
async create(data: CreatePostRequest, authorId: string): Promise<PostResponse> {
const [post] = await db
.insert(postsTable)
.values({ ...data, authorId })
.returning();
return post;
},
async getById(id: string): Promise<PostResponse | null> {
const [post] = await db
.select()
.from(postsTable)
.where(eq(postsTable.id, id))
.limit(1);
return post || null;
},
async getAll(published?: boolean): Promise<PostResponse[]> {
const query = db.select().from(postsTable);
if (published !== undefined) {
query.where(eq(postsTable.published, published));
}
return await query;
}
};Controllers handle HTTP requests and responses:
// backend/src/api/v1/controllers/posts.ts
import { Router } from "express";
import { postsProvider } from "../providers/posts";
import type { CreatePostRequest } from "../dto/posts";
export const router = Router();
// GET /api/v1/posts
router.get("/", async (req, res) => {
try {
const published = req.query.published === 'true';
const posts = await postsProvider.getAll(published);
res.json(posts);
} catch (error) {
res.status(500).json({ error: "Failed to fetch posts" });
}
});
// POST /api/v1/posts
router.post("/", async (req, res) => {
try {
const data: CreatePostRequest = req.body;
const authorId = req.user?.id; // From auth middleware
const post = await postsProvider.create(data, authorId);
res.status(201).json(post);
} catch (error) {
res.status(400).json({ error: "Failed to create post" });
}
});Add new routes to the main router:
// backend/src/api/v1/router.ts
import { router as postsRouter } from "./controllers/posts";
export const router = Router();
router.use("/hello-world", helloWorldRouter);
router.use("/posts", postsRouter); // Add new routeCreate corresponding types and API calls in the frontend:
// frontend/src/types/posts.ts
export interface Post {
id: string;
title: string;
content: string;
published: boolean;
createdAt: string;
}
// frontend/src/lib/api/posts.ts
export async function createPost(data: CreatePostRequest): Promise<Post> {
const response = await fetch('/api/v1/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
}DTOs ensure type consistency between frontend, backend, and database:
// β
Good: Clear interface contracts
interface UserCreateRequest {
email: string;
password: string;
fullname: string;
}
// β Bad: No type safety
function createUser(data: any) { ... }DTOs serve as living documentation for your API:
// Self-documenting API contracts
export interface PaginatedResponse<T> {
data: T[];
pagination: {
page: number;
limit: number;
total: number;
hasNext: boolean;
};
}cd backend
# Install dependencies
bun install
# Start development server with hot reload
bun run dev
# Database operations
bun run db:generate # Generate migrations
bun run db:migrate # Apply migrations
bun run db:push # Push schema directly (dev only)
bun run db:studio # Open database studio
# Production
bun run startcd frontend
# Install dependencies
bun install
# Start development server
bun run dev
# Build for production
bun run build
bun run start
# Linting and formatting
bun run lintFrontend (Next.js)
β HTTP/API calls
Controllers (Express routes)
β Business logic
Providers (Service layer)
β Data access
Database (PostgreSQL)
1. Frontend sends typed request (DTO)
2. Controller validates and routes
3. Provider processes business logic
4. Database operations with Drizzle
5. Response flows back with typed DTOs
// β
Good: Explicit interfaces
export interface LoginRequest {
email: string;
password: string;
}
// β Bad: Direct database types in API
function login(user: DatabaseUser) { ... }- Controllers: Handle HTTP, validation, auth
- Providers: Business logic, data processing
- Schema: Database structure and relationships
- DTOs: API contracts and type definitions
// Consistent error responses
export interface ErrorResponse {
error: string;
message?: string;
details?: Record<string, string>;
}- Always generate migrations for schema changes
- Never modify existing migrations
- Test migrations in development first
- Use descriptive migration names
- Start with the database schema - Design your data structure first
- Use TypeScript everywhere - Leverage type safety across the stack
- Keep DTOs simple - Focus on API contracts, not business logic
- Test your migrations - Always verify schema changes work
- Use Drizzle Studio - Visual database management and query testing
- Follow the workflow - Schema β Migration β DTO β Provider β Controller
- Document your APIs - DTOs serve as living documentation
The application uses a comprehensive database schema with:
- Users Management - User accounts with authentication
- Session Handling - Login and auth sessions
- Account Verification - Email verification and OTP
- Role-Based Access Control - Roles and permissions system
users- User accounts and profileslogin_sessions- Long-term user sessionsauth_sessions- Short-term authentication tokensroles- User roles definitionpermissions- Granular permissionsusers_roles- User-role associationsroles_permissions- Role-permission mapping
- Multi-Session Management - Handle multiple device logins
- Email Verification - Secure account activation
- Two-Factor Authentication - OTP-based 2FA
- Role-Based Permissions - Granular access control
- Secure Password Handling - Bcrypt hashing
- Session Expiration - Configurable session timeouts
- Frontend - Next.js application (Port 3000)
- Backend - Express.js API (Port 8000)
- Database - PostgreSQL (Port 5432)
# Start all services
docker-compose up -d
# View logs
docker-compose logs -f [service-name]
# Run migrations
docker-compose exec backend bun run db:migrate
# Stop services
docker-compose down
# Rebuild services
docker-compose up --build# Application Ports
FRONTEND_PORT=3000
BACKEND_PORT=8000
# Database Configuration
PG_USER=postgres
PG_PASSWORD=password
PG_DB=loslc
DATABASE_URL=postgresql://postgres:password@localhost:5432/loslc
# Additional configuration
NODE_ENV=developmentBACKEND_PORT=8000
DATABASE_URL=postgresql://username:password@localhost:5432/databaseNEXT_PUBLIC_API_URL=http://localhost:8000/api/v1- Hero Section - Landing page hero
- Features - Product features showcase
- Testimonials - User testimonials
- About Section - Company information
- Footer - Site footer with links
- Navigation - Responsive navigation bar
- Button - Customizable button component
- Card - Content container component
- Theme Provider - Dark/light theme support
# Backend
cd backend
bun run start
# Frontend
cd frontend
bun run build
bun run start- Framework: Next.js 14 (App Router)
- Styling: TailwindCSS + shadcn/ui
- Language: TypeScript
- Runtime: Bun
- Framework: Express.js
- ORM: Drizzle ORM
- Database: PostgreSQL
- Language: TypeScript
- Runtime: Bun
- Containerization: Docker & Docker Compose
- Database: PostgreSQL
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow TypeScript best practices
- Use conventional commit messages
- Ensure all tests pass
- Update documentation as needed
# Start all services with Docker
docker-compose up -d
# Stop all services
docker-compose downbun run dev # Development server
bun run start # Production server
bun run db:generate # Generate migrations
bun run db:migrate # Apply migrations
bun run db:studio # Database GUIbun run dev # Development server
bun run build # Production build
bun run start # Production server
bun run lint # Lint code- Next.js Documentation
- Express.js Guide
- Drizzle ORM Docs
- TailwindCSS Documentation
- shadcn/ui Components
This project is licensed under the MIT License. See the LICENSE file for details.
For support and questions:
- Create an issue in the repository
- Contact the development team
- Check the documentation
Built with β€οΈ by the LOSLC Dev Team