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

  • 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):

 

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

4. Alternatives
1. Translation Table
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.

4. Core Domain Logic