Skip to main content

Saheb App Design System

Screenshot 2026-01-01 at 14.04.11.png

Purpose of This Documentation

This presentation explains the design and technical decisions behind our application features.


I will be covering:


1. Technology Stack

  • Backend framework

  • Web frontend framework

  • Mobile frontend framework

  • Database technology (PostgreSQL, MySQL, etc.)

  • UI libraries

  • Supporting libraries and tools (state management, datafetching, utilities)


2. Project Structure

  • Backend folder structure

  • Frontend folder structure

  • Mobile folder structure

3. Language Support

  • Multi-language database translation strategy

  • App language and RTL (Right-to-Left) support on web and mobile


4. Core Domain Logic

    • Qibla calculation logic

    • Prayer time calculation methods

    • Hijri time caulculation methods


    5. Media Management

    • Audio resource handling (Sermon / موعظة)

    • Video hosting and streaming strategy


    6. Notifications & Communication

    • Timezone-aware push notification system

    • Email notification architecture

    • Notification reliability and crash handling

    • Scheduling notificaiton with cron


    8. Application Standards

    • Request logging

    • User event and activity tracking

    • Error and crash reporting

    • API versioning


    How Each Section Is Documented

    For every section in this documentation, I consistently answer the following questions:

    • What solution did I choose?

    • Why did I choose this solution?

    • I may cover what are the pros and cons of the solution for critical sections?

    • What alternative approaches I considered?

    • We may also answer questions related to a specific question

    1. Technology Stack

    a. Backend Framework


    What solution did I choose?
    I chose Nodejs + Expressjs + NestJS as the backend framework.


    Why did I choose this solution?

    • Team is more experienced with
    • Widely used on our code base
    • Built with TypeScript by default


    Pros

    • Clear module-based architecture

    • Strong TypeScript support

    • Easy integration with databases, authentication, and queues

    • Express Rich community

    • Supports modular development and scalability

    • Fits well for APIs serving web and mobile apps

    • One backend server for both mobile and web


    Cons

    • Slightly more boilerplate

    • Integration Complexity with Some Legacy Libraries


    Alternative Approaches

    • Express.js

    • Fastify

    • Laravel (PHP)

    • Spring Boot (Java)


    b. Web Frontend Framework


    What solution did I choose?
    I chose Next.js using the App Directory.


    Why did I choose this solution?

    • Built-in support for modern React features

    • File-based routing with the App Directory

    • Good integration with backend APIs

    • Built-in layouts, and loading states, server errors handlings

    Pros

    • Clear and scalable project structure

    • Server-side rendering and static generation

    • Strong support for SEO and web performance

    • Server Components


    Cons

    • Some ecosystem libraries are still adapting

    • Build & Deployment Complexity

    • Rapidly Changing Features


    Alternative Approaches

    • React ( Vite )

    • React (Tanstack start )

    • Nuxt.js

    • Angular

    c. Mobile Frontend Framework


    What solution did I choose?
    I chose React Native with Expo.


    Why did I choose this solution?

    • Allows building iOS Android apps  and web from a single codebase

    • Quick testing on simulators, emulators, and real devices
    • Built-in support for push notifications, sensors, and device APIs

    • Large community and ecosystem

    • Deployment built-in (EAS Hosting)
    • Routing and Authentification built in
    • Assets management
    • AI intergration (expo mcp server)

    Cons

    • Some native modules require custom development (Ejecting from Expo)


    Alternative Approaches

    • Pure React Native (without Expo)

    • Flutter

    • Native iOS (Swift) / Android (Kotlin) development

    d. Database Technology


    What solution did I choose?
    I chose PostgreSQL as the database for our application.


    Why did I choose this solution?

    • Team is more experienced with
    • Good ecosystem and tooling support
    • Reliable and stable relational database

    • Widely used in the industry with large community and resources

    • Works well with TypeORM for Nestjs integration


    Pros

    • Good ecosystem and tooling support

    • Open-source and actively maintained

    • Supports advanced features like JSON, full-text search, and indexes

    • Strong support for transactions


    Cons

    • Can be more complex to set up than simpler databases (like SQLite)

    • Overkill for very small projects

    e. UI Libraries


    1. Web Frontend UI Approach

    What solution did I choose?
    I chose shadcn/ui integrated with Tailwind CSS, and Figma designs for reference.


    Why did I choose this solution?

    • Tailwind provide CSS utility classes to speed up development, so instead of writing custom CSS files, I can style components directly using small, reusable classes
      • Without Tailwind (Traditional CSS):
        • <button class="btn">
            Save
          </button>
        • .btn {
            background-color: blue;
            color: white;
            padding: 8px 16px;
            border-radius: 6px;
          }
      • With Tailwind:
        • <button class="bg-blue-600 text-white px-4 py-2 rounded-md">
            Save
          </button>
          
    • Provides a ready-to-use component library built on Tailwind CSS

    image.png

    image.png

    • Open Code: The top layer of your component code is open for modification:
      • Other UI libraries:
        • import Button from '@mui/material/Button';
          <Button variant="contained">Contained</Button>
      • Shadcn:
        • import * as React from "react"
          import { Slot } from "@radix-ui/react-slot"
          import { cva, type VariantProps } from "class-variance-authority"
          
          import { cn } from "@/lib/utils"
          
          const buttonVariants = cva(
            "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
            {
              variants: {
                variant: {
                  default: "bg-primary text-primary-foreground hover:bg-primary/90",
                  destructive:
                    "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
                  outline:
                    "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
                  secondary:
                    "bg-secondary text-secondary-foreground hover:bg-secondary/80",
                  ghost:
                    "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
                  link: "text-primary underline-offset-4 hover:underline",
                },
                size: {
                  default: "h-9 px-4 py-2 has-[>svg]:px-3",
                  sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
                  lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
                  icon: "size-9",
                  "icon-sm": "size-8",
                  "icon-lg": "size-10",
                },
              },
              defaultVariants: {
                variant: "default",
                size: "default",
              },
            }
          )
          
          function Button({
            className,
            variant = "default",
            size = "default",
            asChild = false,
            ...props
          }: React.ComponentProps<"button"> &
            VariantProps<typeof buttonVariants> & {
              asChild?: boolean
            }) {
            const Comp = asChild ? Slot : "button"
          
            return (
              <Comp
                data-slot="button"
                data-variant={variant}
                data-size={size}
                className={cn(buttonVariants({ variant, size, className }))}
                {...props}
              />
            )
          }
          
          export { Button, buttonVariants }
    • Distribution: A flat-file schema and command-line tool make it easy to distribute components.
      • npx shadcn@latest add button
    • AI-Ready: Open code for LLMs to read, understand, and improve (mcp server).
      • {
          "registries": {
            "@acme": "https://acme.com/r/{name}.json"
          }
        }
    • Figma designs give a visual reference for UI consistency

    • Huge ecosystem built around shadcn.
      1. shadcn/studio
      2. shadcn text editor
      3. react-dnd-kit-tailwind-shadcn-ui
      4. shadcn-chat
    • Fits well with React and Next.js

    • Very customizable :
      1. you can change the whole app theme by changing only globall.css file :
      2. change component source code.

    Pros

    • Lightweight and modern styling approach


    Cons

    • Learning curve for Tailwind if new to utility-first CSS


    Alternative Approaches

    • Material-UI (MUI)

    • Ant Design

    • Chakra UI

    • Custom components from scratch

    1. Mobile UI Approach:

    What solution did I choose?
    I chose not to use any mobile component library.
    I use NativeWind for styling, along with Figma MCP, Figma Make, and Figma Dev Tools.

    Why did I choose this solution?

    • The app uses a fully custom UI designed in Figma, so prebuilt component libraries are not a good fit
    • NativeWind works like Tailwind CSS, making styling simple and consistent

    • Utility-based styling works well with LLMs for generating and adjusting UI code

    • Figma MCP and Figma Make help convert design decisions into implementation guidance

    • Figma Dev Tools allow developers and LLMs to inspect spacing, colors, and layout accurately

    • NativeWind allows theme-based styling (colors, spacing, dark mode)

    Cons

    • No ready-made components out of the box

    • More manual work during initial development

    f. Supporting Libraries & Tools

    Zustand (State Management)

    What solution did I choose?
    I chose Zustand for global state management.

    Why did I choose this solution?

    • Simple and lightweight API

    • Minimal boilerplate compared to other state managers

    • Works well with React and React Native

    Alternative Approaches

    • Redux Toolkit

    • Jotai


    React Query (Server State & Data Fetching)

    What solution did I choose?
    I chose React Query for data fetching and server state management.

    Why did I choose this solution?

    • Handles caching, loading, and error states automatically

    • provide all request states by default ( isPending, isError, isFetching , isLoadin,isFetching)
    • .Reduces manual API state handling

    • Works well with REST APIs

    • Improves app performance

    • Simplifies async data logic

    Alternative Approaches

    • SWR

    • Manual data fetching

    • Redux Toolkit Query


    Axios (HTTP Client)

    What solution did I choose?
    I chose Axios for making HTTP requests.

    Why did I choose this solution?

    • Simple API

    • Supports request and response interceptors. (eg for handling access tokens and refresh tokens)

    • Works well with authentication and error handling

    Alternative Approaches

    • Native fetch API

    • Ky


    Postman (API Testing)

    What solution did I choose?
    I chose Postman for API testing and debugging.

    Why did I choose this solution?

    • Easy to test APIs during development

    • Easy to share requests and collections with the team
    • Supports environments and collections

    • Postman is simpler and more familiar to the team

    Pros

    • Simple and visual API testing

    • Useful for collaboration

    • Saves request history

    Cons

    • Not part of production code

    • Can become outdated if not maintained

    Alternative Approaches

    • Swagger

    Why Postman Over Swagger:

    • Collaboration & Manual Collection Import

      • Easy to share requests and collections with the team

    • Environment Variables

      • Supports dev, staging, prod easily for testing

    • Ease of Use

      • Postman is simpler and more familiar to the team

       

    • Swagger is mainly for documentation and simple API testing, while Postman is primarily for API testing and debugging

    next-intl (Internationalization)

    What solution did I choose?
    I chose next-intl for internationalization in the web app.

    Why did I choose this solution?

    • Designed specifically for Next.js

    • Supports server and client components

    • Easy locale-based routing

    • Already using it in Irchademy

    Alternative Approaches

    • react-i18next


    React Hook Form + Zod (Forms & Validation)

    What solution did I choose?
    I chose React Hook Form with Zod for form handling and validation.

    Why did I choose this solution?

    • Clean validation logic

    • Works well with shadcn (shdacn support it by default)
    • Good developer experience

    • Schema-based validation

    • Strong TypeScript support

    Alternative Approaches

    • Formik + Yup

    • Native form handling

    Moment.js (Date & Time Handling)

    What solution did I choose?
    I chose Moment.js for handling and formatting dates and times.

    Why did I choose this solution?

    • Simplifies date formatting and manipulation

    • Easy to use for common date operations

    • Widely known and documented

    • Handles time zones and localization
    • Large ecosystem and examples

    Alternative Approaches

    • Day.js

    • date-fns

    2. Application Architecture & Project Structure

    a. Backend Folder Structure (NestJS)

    What solution did I choose?

    saheb-backend-skeleton/
    ├── src/
    │   ├── common/          
    │   │   ├── decorators/
    │   │   │   └── public.decorator.ts       # for example: @Public decorator
    │   │   ├── entities/
    │   │   │   └── base.entity.ts            # for example: Base entity
    │   │   └── guards/
    │   │       └── roles.guard.ts            # for example: Role-based guard
    │   ├── configs/
    │   │   └── database.config.ts            # for example: DB configuration
    │   ├── db/
    │   │   └── migrations
    │   ├── modules/
    │   │   ├── auth/                         
    │   │   │   └── auth.controller.ts        # for example: Auth endpoints
    │   │   └── users/
    │   │       └── users.service.ts          # for example: User logic
    │   └── main.ts                            # Application entry point
    ├── docker-compose.yml
    ├── Dockerfile
    └── .env.example
    

    Why did I choose this solution:

    • Keeps code organized by feature (config, modules, etc.)

    • Easy to maintain and scale

    • Follows NestJS best practices

    b. Frontend Folder Structure (Next.js)

    What solution did I choose?

    saheb-frontend-skeleton/
    ├── src/
    │   ├── app/
    │   │   ├── (dashboard)/dashboard/
    │   │   │   ├── page.tsx                   # for example: server component
    │   │   │   └── _client.tsx               # for example: client component
    │   │   └── login/
    │   │       └── page.tsx                   # for example: login page
    │   ├── components/
    │   │   └── ui/button.tsx                  # for example: UI component
    │   ├── i18n/
    │   │   └── locales/en.json                # for example: English translations
    │   ├── lib/
    │   │   └── api-client.ts                  # for example: Axios setup
    │   └── store/
    │       └── auth.ts                        # for example: Zustand auth store
    ├── public/                                # Static assets
    ├── .env.example
    ├── Dockerfile
    └── next.config.ts
    

    Why did I choose this solution:

    • Make the page a server component by default

      • Allows running server-side logic before rendering the page

      • Useful for auth checks, data fetching, or redirection

    C. Mobile Folder Structure (React Native + Expo)

    ─ assets/                        # Images, icons, fonts, etc.
    ─ scripts/                       # Build or helper scripts
    ─ src/
    │   ├── app/                      # App entry points / pages
    │   │   ├── _layout.tsx           # Main layout for all screens
    │   │   ├── index.tsx             # Home page
    │   │   ├── events.tsx            # Events page
    │   │   └── settings.tsx          # Settings page
    │   ├── components/               # Reusable UI components
    │   │   ├── Table/
    │   │   │   ├── Cell.tsx          # Example table cell
    │   │   │   └── index.tsx
    │   │   ├── BarChart.tsx
    │   │   └── Button.tsx
    │   ├── screens/                  # Screens composed of components
    │   │   ├── Home/
    │   │   │   ├── Card.tsx          # Component used only in Home
    │   │   │   └── index.tsx         # Home screen
    │   │   ├── Events.tsx            # Events screen
    │   │   └── Settings.tsx          # Settings screen
    │   ├── utils/                     # Reusable helper functions
    │   └── hooks/                     # Custom React hooks
    │       └── useTheme.ts
    ├── app.json                       # Expo config
    ├── eas.json                       # Expo EAS build config
    └── package.json                   # Dependencies
    

    Why did I choose this solution:

    • Pages are in app/ → follows Expo Router rules for automatic routing

    • Folders in app/ → organize related pages together (stacks, tabs, etc.)

    • /screens/ folder → keeps page-specific components separate and organized

    • Easy to maintain and scale → adding new pages or layouts is simple

    • Clear separation of concerns → components, hooks, utils, and assets are all in their own folders

    3. Localization & Language Support

    a. Multi-language database translation strategy

    1. What solution did I choose?
      • One-Table-Fits-All  (global translation table):

    drawing-1-1767311627.png
    2. why did I choose this solution:
    • The main table only stores entity-specific data (id, price, etc.).
    • Adding a new language doesn’t require schema changes.\
    • Each translation can be reused by multiple products if needed (e.g., name_translation_id = 101 could be shared across entities — though usually IDs are unique per text).
    • Adding metadata like font, direction, or is_active is easy via the languages table.
    3. Cons
    • To fetch a product with translations, you need at least 2 joins

    • Large translations table (all entities’ text in all languages) can become huge

    • Requires proper indexing: (id, language_code) and (language_code, value) for fast lookup

    • Caching may be needed for high-traffic

    • Hard to maintain and Debug

    4. Alternatives
    1. Translation Table
    drawing-1-1767311468.png
    2. Pros:
    • Each entity has its own translation table (product_translations)

    • Easy to understand, manage and debug with for any developer

    • No magic IDs, no indirection

    • Very safe data model

    • One join only

    3. Cons:
    • Schema Duplication: Every translatable entity needs its own table

    • Adding Fields Requires Schema Changes.

     

    b. App language and RTL (Right-to-Left) support on web and mobile
    What solution did we choose?

    Mobile

    • react-i18next for translations

    • expo-localization to detect device language

    • I18nManager for RTL handling

    • NativeWind for RTL-friendly styling

    Web

    • next-intl for internationalization

    • Built-in RTL support using ShadCN UI and Radix UI


    Why did we choose this solution?
    • Shared translation concepts between web and mobile

    • Automatic language detection from the device or browser

    • Full support for Arabic (RTL) and Latin (LTR) layouts

    4. Core Domain Logic

    1. Qibla Calculation Logic
      Purpose:
        Calculate the exact direction of the Kaaba from the user’s location
        Technology Used:
          adhan-js
          Why did I choose this option
            Adhan is a well tested and well documented library for calculating Islamic prayer times in JavaScript using Node or a web browser Provides Vriety of Calculation methods Have Other related solutions (Prayers Time Calculation, Sunnah Times)
            Example:
            var coordinates = new Coordinates(35.78056, -78.6389);
            var qiblaDirection = Qibla(coordinates);
            key formula
              rotationNeeded = qiblaDirection - phoneHeading
              2. Prayer Time Calculation Methods
                Purpose

                  Calculate daily prayer times based on location and date.

                  Technology Used:
                    adhan-js
                    Why did I choose this option

                      Multiple calculation methods

                      Different madhabs

                      High astronomical accuracy

                      Support Manual Adjusments (adding for example 5 to each or specific prayer)
                      Prayer Time Calculation Methods

                        Muslim World League
                        Standard method used in many countries worldwide.

                        Egyptian General Authority of Survey
                        Official method used in Egypt and some nearby regions.

                        University of Islamic Sciences, Karachi
                        Commonly used in Pakistan, Bangladesh, and parts of India.

                        Umm al-Qura University (Makkah)
                        Official method used in Saudi Arabia.

                        Dubai
                        Method used in the UAE with regional time adjustments.

                        Moonsighting Committee
                        Used mainly in North America, based on moon sighting criteria.

                        North America (ISNA)
                        Standard method used by Islamic Society of North America.

                        Kuwait
                        Method used by the Ministry of Awqaf in Kuwait.

                        Qatar
                        Official method used in Qatar, similar to Umm al-Qura.

                        Singapore
                        Method used in Southeast Asia with rounding rules.

                        Tehran (Institute of Geophysics)
                        Method used in Iran with regional astronomical adjustments.

                        Turkey (Diyanet)
                        Official Turkish religious authority method.

                        Other / Custom
                        Allows manual configuration of calculation parameters.

                        Supported Madhabs

                          Shafi (Shafi‘i / Standard)
                          Used by the majority of Muslims worldwide
                          Asr starts when an object’s shadow is equal to its height

                          Hanafi
                          Commonly used in South Asia and parts of the Middle East
                          Asr starts when an object’s shadow is twice its height

                          Examples:

                            Full Example

                              import {
                                Coordinates,
                                CalculationMethod,
                                PrayerTimes,
                                SunnahTimes,
                                Prayer,
                                Qibla,
                              } from 'adhan';
                              import moment from 'moment-timezone';
                              
                              const coordinates = new Coordinates(35.7897507, -78.6912485);
                              const params = CalculationMethod.MoonsightingCommittee();
                              const date = new Date();
                              const prayerTimes = new PrayerTimes(coordinates, date, params);
                              const sunnahTimes = new SunnahTimes(prayerTimes);
                              
                              moment(prayerTimes.fajr).tz('Africa/Casablanca').format('h:mm A');
                              Prayer time date adjustment example:
                                const date = new Date(2015, 11, 1);
                                const params = CalculationMethod.MuslimWorldLeague();
                                params.madhab = Madhab.Shafi;
                                const p = new PrayerTimes(new Coordinates(35.775, -78.6336), date, params);
                                console.log(moment(p.fajr).tz('America/New_York').format('h:mm A')); # 5:35 AM
                                params.adjustments.fajr = 10;
                                const p2 = new PrayerTimes(new Coordinates(35.775, -78.6336), date, params);
                                console.log(moment(p2.fajr).tz('America/New_York').format('h:mm A')) # 5:45 AM