Skip to content

Week 17.1 Building Paytm

Up until now, our discussions have primarily revolved around theoretical concepts. In this lecture, Harkirat takes a practical approach by guiding us through the hands-on process of building a Paytm like application

The stack for this project includes Next.js for the frontend and backend (or a separate backend), Express for auxiliary backends, Turborepo for managing the monorepo, a PostgreSQL database, Prisma as the ORM, and Tailwind for styling.

While there are no specific notes provided for this section, a mini guide is outlined below to assist you in navigating through the process of building the application. Therefore, it is strongly advised to actively follow along during the lecture for a hands-on learning experience.

Untitled

Introduction

In this hands-on lecture, we will be building a Paytm-like application. The first step is to plan the features and design the user interface (UI) and user experience (UX). For the UX, we can either follow first principles or take inspiration from existing successful websites. As for the UI, while there are tools available, finding a good one can be challenging.

Next, we will focus on the high-level design, including the authentication provider, database, backend stack, frontend stack, and the modules we’ll have (such as common, UI, and backend). We’ll also decide on the cloud platform for deployment. Additionally, we’ll delve into the low-level design, including schema design, route signatures, and frontend components. While entity-relationship diagrams can be useful, they may not be necessary unless you’re a highly visual person.

Feature planning

User login

  1. Auth (In this case, probably email/phone)
  2. On ramp from bank, off ramp to bank
  3. Support transfers via phone number/name
  4. Support scanning a QR code for transferring to merchants

Merchant login

  1. Login with google
  2. Generate a QR Code for acceptance
  3. Merchants get an alert/notification on payment
  4. Merchant gets money offramped to bank every 2 days

UI/UX (End User)

Login

Untitled

Untitled

Landing Page

Untitled

User Home Page

Untitled

User Transfer Page

Untitled

Untitled

Untitled

UI/UX (Merchant)

Untitled

Untitled

Untitled

Architecture

Untitled

https://www.notion.so/image/https%3A%2F%2Fprod-files-secure.s3.us-west-2.amazonaws.com%2F085e8ad8-528e-47d7-8922-a23dc4016453%2F888fec18-4140-4f67-9820-7878a4897e84%2FScreenshot_2024-03-23_at_6.35.20_PM.png?table=block&id=c791d5db-4676-4d03-b8e6-587f9854dab5&cache=v2

Hot paths

  1. Send money to someone
  2. Withdraw balance of merchant
  3. Withdraw balance of user back to bank
  4. Webhooks from banks to transfer in money

This is ~1 month job for a 2 engineer team.

We can cut scope in either

  1. UI
  2. Number of features we support (remove merchant altogether)
  3. Number of services we need (merge bank server, do withdrawals directly and not in a queue assuming banks are always up)

Stack

  1. Frontend and Backend - Next.js (or Backend)
  2. Express - Auxilary backends
  3. Turborepo
  4. Postgres Database
  5. Prisma ORM
  6. Tailwind

Bootstrap the app

  • Init turborepo
Terminal window
npx create-turbo@latest
  • Rename the two Next apps to
    1. user-app
    2. merchant-app
  • Add tailwind to it.
Terminal window
cd apps/user-app
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
cd ../merchant-app
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

Update tailwind.config.js

/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"../../packages/ui/**/*.{js,ts,jsx,tsx,mdx}"
],
theme: {
extend: {},
},
plugins: [],
}

Update global.css

@tailwind base;
@tailwind components;
@tailwind utilities;

Adding prisma

Ref - https://turbo.build/repo/docs/handbook/tonols/prisma

  1. Create a new db folder
  2. Initialise package.json
Terminal window
npm init -y
npx tsc --init
  1. Update package.json
{
"name": "@repo/db",
"version": "0.0.0",
"dependencies": {
"@prisma/client": "^5.11.0"
},
"devDependencies": {
"prisma": "5.11.0"
},
"exports": {
"./client": "./index.ts"
}
}
  1. Update tsconfig.json
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
  1. Init prisma
npx prisma init
  1. Start DB locally/on neon.db/on aiven
  2. Update .env with the new database URL
  3. Add a basic schema
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
}
  1. MIgrate DB
npx prisma migrate
  1. Update index.ts
export * from '@prisma/client';
  1. Try adding api/user/route.ts
import { NextResponse } from "next/server"
import { PrismaClient } from "@repo/db/client";
const client = new PrismaClient();
export const GET = async () => {
await client.user.create({
data: {
email: "asd",
name: "adsads"
}
})
return NextResponse.json({
message: "hi there"
})
}

Add a recoil/store module

  • Create store package
Terminal window
cd packages
mkdir store
npm init -y
npx tsc --init
  • Install dependencies
Terminal window
npm i recoil
  • Update tsconfig.json
{
"extends": "@repo/typescript-config/react-library.json",
"compilerOptions": {
"outDir": "dist"
},
"include": ["src"],
"exclude": ["node_modules", "dist"]
}
  • Update package.json
{
"name": "@repo/store",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"recoil": "^0.7.7"
}
}
  • Create a simple atom called balance in src/atoms/balance.ts
import { atom } from "recoil";
export const balanceAtom = atom<number>({
key: "balance",
default: 0,
})
  • Create a simple hook called src/hooks/useBalance.ts
import { useRecoilValue } from "recoil"
import { balanceAtom } from "../atoms/balance"
export const useBalance = () => {
const value = useRecoilValue(balanceAtom);
return value;
}
  • Add export to package.json
"exports": {
"./useBalance": "./src/hooks/useBalance"
}

Import recoil in the next.js apps

  • Install recoil in them
Terminal window
npm i recoil
  • Add a providers.tsx file
"use client"
import { RecoilRoot } from "recoil";
export const Providers = ({children}: {children: React.ReactNode}) => {
return <RecoilRoot>
{children}
</RecoilRoot>
}
  • Update layout.tsx
return (
<html lang="en">
<Providers>
<body className={inter.className}>{children}</body>
</Providers>
</html>
);
  • Create a simple client component and try using the useBalance hook in there
"use client";
import { useBalance } from "@repo/store/useBalance";
export default function() {
const balance = useBalance();
return <div>
hi there {balance}
</div>
}

If you see this, we’ll try to debug it end of class. Should still work in dev mode

Untitled

Add next-auth

Database

Update the database schema

generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
email String? @unique
name String?
number String @unique
password String
}
model Merchant {
id Int @id @default(autoincrement())
email String @unique
name String?
auth_type AuthType
}
enum AuthType {
Google
Github
}

User-app

  • Go to apps/user-app
  • Initialize next-auth
Terminal window
npm install next-auth
  • Initialize a simple next auth config in lib/auth.ts
import db from "@repo/db/client";
import CredentialsProvider from "next-auth/providers/credentials"
import bcrypt from "bcrypt";
export const authOptions = {
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
phone: { label: "Phone number", type: "text", placeholder: "1231231231" },
password: { label: "Password", type: "password" }
},
// TODO: User credentials type from next-aut
async authorize(credentials: any) {
// Do zod validation, OTP validation here
const hashedPassword = await bcrypt.hash(credentials.password, 10);
const existingUser = await db.user.findFirst({
where: {
number: credentials.phone
}
});
if (existingUser) {
const passwordValidation = await bcrypt.compare(credentials.password, existingUser.password);
if (passwordValidation) {
return {
id: existingUser.id.toString(),
name: existingUser.name,
email: existingUser.number
}
}
return null;
}
try {
const user = await db.user.create({
data: {
number: credentials.phone,
password: hashedPassword
}
});
return {
id: user.id.toString(),
name: user.name,
email: user.number
}
} catch(e) {
console.error(e);
}
return null
},
})
],
secret: process.env.JWT_SECRET || "secret",
callbacks: {
// TODO: can u fix the type here? Using any is bad
async session({ token, session }: any) {
session.user.id = token.sub
return session
}
}
}
  • Create a /api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth"
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
  • Update .env
Terminal window
JWT_SECRET=test
NEXTAUTH_URL=http://localhost:3001

Merchant-app

Create lib/auth.ts

import GoogleProvider from "next-auth/providers/google";
export const authOptions = {
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID || "",
clientSecret: process.env.GOOGLE_CLIENT_SECRET || ""
})
],
}
  • Create a /api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth"
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
NEXTAUTH_URL=http://localhost:3000
NEXTAUTH_SECRET=kiratsecr3t

Add auth

Client side

  • Wrap the apps around SessionProvider context from the next-auth package
  • Go to merchant-app/providers.tsx
"use client"
import { RecoilRoot } from "recoil";
import { SessionProvider } from "next-auth/react";
export const Providers = ({children}: {children: React.ReactNode}) => {
return <RecoilRoot>
<SessionProvider>
{children}
</SessionProvider>
</RecoilRoot>
}
  • Do the same for user-app/providers.tsx

Server side

Create apps/user-app/app/api/user.route.ts

import { getServerSession } from "next-auth"
import { NextResponse } from "next/server";
import { authOptions } from "../../lib/auth";
export const GET = async () => {
const session = await getServerSession(authOptions);
if (session.user) {
return NextResponse.json({
user: session.user
})
}
return NextResponse.json({
message: "You are not logged in"
}, {
status: 403
})
}

Ensure login works as exptected

Add an Appbar component

  • Update the Button component in UI
"use client";
import { ReactNode } from "react";
interface ButtonProps {
children: ReactNode;
onClick: () => void;
}
export const Button = ({ onClick, children }: ButtonProps) => {
return (
<button onClick={onClick} type="button" className="text-white bg-gray-800 hover:bg-gray-900 focus:outline-none focus:ring-4 focus:ring-gray-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2">
{children}
</button>
);
};
  • Create a ui/Appbar component that is highly generic (doesnt know anything about the user/how to logout).
import { Button } from "./button";
interface AppbarProps {
user?: {
name?: string | null;
},
// TODO: can u figure out what the type should be here?
onSignin: any,
onSignout: any
}
export const Appbar = ({
user,
onSignin,
onSignout
}: AppbarProps) => {
return <div className="flex justify-between border-b px-4">
<div className="text-lg flex flex-col justify-center">
PayTM
</div>
<div className="flex flex-col justify-center pt-2">
<Button onClick={user ? onSignout : onSignin}>{user ? "Logout" : "Login"}</Button>
</div>
</div>
}

Checkpoint

We are here - https://github.com/100xdevs-cohort-2/paytm-project-starter-monorepo