diff --git a/apps/docs/pages/jwt/server/nestjs.mdx b/apps/docs/pages/jwt/server/nestjs.mdx
new file mode 100644
index 0000000..92717ae
--- /dev/null
+++ b/apps/docs/pages/jwt/server/nestjs.mdx
@@ -0,0 +1,495 @@
+import { FileTree, Steps } from "nextra/components";
+
+## Use the CLI (not yet supported)
+
+```bash copy
+npx ryo-auth@latest add jwt-nestjs-prisma
+```
+
+## Manual Installation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+### Install dependencies
+
+```bash npm2yarn copy
+npm i @nestjs/config @prisma/client argon2 class-transformer class-validator cookie-parser
+```
+
+### Install dev dependencies
+
+```bash npm2yarn copy
+npm i --dev prisma @types/cookie-parser @types/jsonwebtoken
+```
+
+### Setup your Prisma schema
+
+```prisma showLineNumbers filename="prisma/schema.prisma" copy
+generator client {
+ provider = "prisma-client-js"
+}
+
+datasource db {
+ provider = "postgresql"
+ url = env("DATABASE_URL")
+}
+
+model User {
+ id String @id @default(uuid())
+ email String @unique
+ password String
+ name String
+ createdAt DateTime @default(now())
+ updatedAt DateTime @updatedAt
+}
+```
+
+### App bootstrap
+
+```ts showLineNumbers filename="main.ts" copy
+import { NestFactory } from "@nestjs/core";
+import { AppModule } from "./app.module";
+import { ValidationPipe } from "@nestjs/common";
+import * as cookieParser from "cookie-parser";
+
+async function bootstrap() {
+ const app = await NestFactory.create(AppModule);
+ app.useGlobalPipes(new ValidationPipe());
+ app.use(cookieParser());
+ await app.listen(3000);
+}
+bootstrap();
+```
+
+### Setup env variables
+
+```bash showLineNumbers filename=".env" copy
+DATABASE_URL="postgresql://username:password@localhost:5432/RollYourOwnAuth"
+
+NODE_ENV="development"
+
+JWT_ACCESS_TOKEN_SECRET="" # Run `openssl rand -base64 32` in your CLI to generate a secret
+JWT_REFRESH_TOKEN_SECRET="" # Run `openssl rand -base64 32` in your CLI to generate a secret
+JWT_ACCESS_EXPIRES_IN=30m
+JWT_REFRESH_EXPIRES_IN=30d
+
+ACCESS_TOKEN_COOKIE_MAX_AGE=1800000
+REFRESH_TOKEN_COOKIE_NAME=__refreshToken__
+REFRESH_TOKEN_COOKIE_MAX_AGE=2592000000
+ACCESS_TOKEN_COOKIE_NAME=__accessToken__
+```
+
+### Prisma config
+
+```ts showLineNumbers filename="config/index.ts" copy
+import { PrismaClient } from "@prisma/client";
+
+export const prisma = new PrismaClient();
+```
+
+### App module
+
+```ts showLineNumbers filename="app.module.ts" copy
+import { Module } from "@nestjs/common";
+import { AppController } from "./app.controller";
+import { AppService } from "./app.service";
+import { AuthModule } from "./auth/auth.module";
+import { UsersModule } from "./users/users.module";
+import { ConfigModule } from "@nestjs/config";
+
+@Module({
+ imports: [
+ AuthModule,
+ UsersModule,
+ ConfigModule.forRoot({
+ isGlobal: true,
+ }),
+ ],
+ controllers: [],
+ providers: [],
+})
+export class AppModule {}
+```
+
+### Create user dto
+
+```ts showLineNumbers filename="users/dto/create.user.dto.ts" copy
+import { IsString, IsEmail, MinLength, ValidateIf, IsIn, IsDefined } from "class-validator";
+
+export class CreateUserDto {
+ @IsString({ message: "Full name is required" })
+ name: string;
+
+ @IsEmail({}, { message: "Not a valid email" })
+ email: string;
+
+ @IsString({ message: "Password is required" })
+ @MinLength(6, { message: "Password must be at least 6 characters long" })
+ password: string;
+
+ @IsString()
+ @IsDefined()
+ @IsIn([Math.random()], {
+ message: "Passwords do not match",
+ })
+ @ValidateIf((o) => o.password !== o.confirmPassword)
+ confirmPassword: string;
+}
+```
+
+### Users module
+
+```ts showLineNumbers filename="users/users.module.ts" copy
+import { Module } from "@nestjs/common";
+import { UsersService } from "./users.service";
+
+@Module({
+ providers: [UsersService],
+})
+export class UsersModule {}
+```
+
+### Users service
+
+```ts showLineNumbers filename="users/users.service.ts" copy
+import { HttpException, HttpStatus, Injectable } from "@nestjs/common";
+import { prisma } from "src/config";
+import { CreateUserDto } from "./dto/create-user.dto";
+import * as argon2 from "argon2";
+import { Prisma, User } from "@prisma/client";
+
+@Injectable()
+export class UsersService {
+ async createUser(userDetails: CreateUserDto): Promise {
+ const hashedPasword = await argon2.hash(userDetails.password);
+
+ try {
+ const user = await prisma.user.create({
+ data: {
+ name: userDetails.name,
+ email: userDetails.email,
+ password: hashedPasword,
+ },
+ });
+
+ return user;
+ } catch (error) {
+ if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
+ throw new HttpException("Email already exists", HttpStatus.CONFLICT);
+ }
+ }
+ }
+
+ async findUserById(id: string): Promise {
+ const user = await prisma.user.findUnique({
+ where: {
+ id,
+ },
+ });
+
+ return user;
+ }
+
+ async findUserByEmail(email: string): Promise {
+ const user = await prisma.user.findUnique({
+ where: {
+ email,
+ },
+ });
+
+ if (!user) {
+ throw new HttpException("User not found", HttpStatus.NOT_FOUND);
+ }
+ return user;
+ }
+}
+```
+
+### Extend express request type to include user info
+
+```ts showLineNumbers filename="types/global.d.ts" copy
+import { Request as ExpressRequest } from "express";
+declare module "express" {
+ interface Request extends ExpressRequest {
+ user: {
+ id: string;
+ };
+ }
+}
+```
+
+### Auth module
+
+```ts showLineNumbers filename="auth/auth.module.ts" copy
+import { Module } from "@nestjs/common";
+import { AuthController } from "./auth.controller";
+import { AuthService } from "./auth.service";
+import { UsersService } from "src/users/users.service";
+
+@Module({
+ controllers: [AuthController],
+ providers: [AuthService, UsersService],
+})
+export class AuthModule {}
+```
+
+### Auth guard
+
+```ts showLineNumbers filename="auth/auth.guard.ts" copy
+import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from "@nestjs/common";
+import { ConfigService } from "@nestjs/config";
+import { Request } from "express";
+import * as jwt from "jsonwebtoken";
+
+@Injectable()
+export class AuthGuard implements CanActivate {
+ constructor(private configService: ConfigService) {}
+
+ async canActivate(context: ExecutionContext): Promise {
+ const request: Request = context.switchToHttp().getRequest();
+ const refreshTokenCookieName = this.configService.get("ACCESS_TOKEN_COOKIE_NAME");
+
+ const token: string = request.cookies[refreshTokenCookieName];
+ try {
+ const deodedPayload = jwt.verify(
+ token,
+ this.configService.get("JWT_ACCESS_TOKEN_SECRET")
+ ) as Request["user"];
+
+ // 💡 We're assigning the payload to the request object here
+ // so that we can access it in our route handlers
+ // 💡 and to make it typesafe we extended express request type in 'src/types/global.d.ts"
+ request.user = deodedPayload;
+ } catch {
+ throw new UnauthorizedException();
+ }
+ return true;
+ }
+}
+```
+
+### Login dto
+
+```ts showLineNumbers filename="auth/dto/login.dto.ts" copy
+import { IsString, IsEmail } from "class-validator";
+
+export class LoginDto {
+ @IsEmail({}, { message: "Not a valid email" })
+ email: string;
+
+ @IsString({ message: "Password is required" })
+ password: string;
+}
+```
+
+### Auth controller
+
+```ts showLineNumbers filename="auth/auth.controller.ts" copy
+import { Body, Controller, Get, HttpCode, HttpStatus, Post, Req, Res, UseGuards } from "@nestjs/common";
+import { AuthService } from "./auth.service";
+import { CreateUserDto } from "src/users/dto/create-user.dto";
+import { Request, Response } from "express";
+import { LoginDto } from "./dto/login.dto";
+import { ConfigService } from "@nestjs/config";
+import { AuthGuard } from "./auth.guard";
+import { UsersService } from "src/users/users.service";
+
+@Controller("auth")
+export class AuthController {
+ constructor(
+ private readonly authService: AuthService,
+ private readonly configService: ConfigService,
+ private readonly usersService: UsersService
+ ) {}
+
+ @Post("signup")
+ @HttpCode(HttpStatus.CREATED)
+ async signup(@Body() createUserDto: CreateUserDto, @Res({ passthrough: true }) res: Response) {
+ const { access_token, refresh_token } = await this.authService.signup(createUserDto);
+
+ res.cookie(this.configService.get("REFRESH_TOKEN_COOKIE_NAME"), refresh_token, {
+ secure: this.configService.get("NODE_ENV") === "production",
+ httpOnly: true,
+ sameSite: "lax",
+ maxAge: this.configService.get("REFRESH_TOKEN_COOKIE_MAX_AGE"),
+ });
+
+ res.cookie(this.configService.get("ACCESS_TOKEN_COOKIE_NAME"), access_token, {
+ secure: this.configService.get("NODE_ENV") === "production",
+ httpOnly: true,
+ sameSite: "lax",
+ maxAge: this.configService.get("ACCESS_TOKEN_COOKIE_MAX_AGE"),
+ });
+
+ return { access_token };
+ }
+
+ @Post("login")
+ @HttpCode(HttpStatus.OK)
+ async login(@Body() loginDto: LoginDto, @Res({ passthrough: true }) res: Response) {
+ const { access_token, refresh_token } = await this.authService.login(loginDto);
+
+ res.cookie(this.configService.get("REFRESH_TOKEN_COOKIE_NAME"), refresh_token, {
+ secure: this.configService.get("NODE_ENV") === "production",
+ httpOnly: true,
+ sameSite: "lax",
+ maxAge: this.configService.get("REFRESH_TOKEN_COOKIE_MAX_AGE"),
+ });
+
+ res.cookie(this.configService.get("ACCESS_TOKEN_COOKIE_NAME"), access_token, {
+ secure: this.configService.get("NODE_ENV") === "production",
+ httpOnly: true,
+ sameSite: "lax",
+ maxAge: this.configService.get("ACCESS_TOKEN_COOKIE_MAX_AGE"),
+ });
+
+ return { access_token };
+ }
+
+ @Post("refresh")
+ @HttpCode(HttpStatus.OK)
+ async refresh(@Req() req: Request, @Res({ passthrough: true }) res: Response) {
+ const old_refresh_token: string = req.cookies[this.configService.get("REFRESH_TOKEN_COOKIE_NAME")];
+
+ if (!old_refresh_token) {
+ res.status(HttpStatus.UNAUTHORIZED).json({ message: "Unauthorized!" });
+ }
+
+ const { access_token, refresh_token: new_refresh_token } = await this.authService.refresh(old_refresh_token);
+
+ res.cookie(this.configService.get("REFRESH_TOKEN_COOKIE_NAME"), new_refresh_token, {
+ secure: this.configService.get("NODE_ENV") === "production",
+ httpOnly: true,
+ sameSite: "lax",
+ maxAge: this.configService.get("REFRESH_TOKEN_COOKIE_MAX_AGE"),
+ });
+
+ res.cookie(this.configService.get("ACCESS_TOKEN_COOKIE_NAME"), access_token, {
+ secure: this.configService.get("NODE_ENV") === "production",
+ httpOnly: true,
+ sameSite: "lax",
+ maxAge: this.configService.get("ACCESS_TOKEN_COOKIE_MAX_AGE"),
+ });
+
+ return { access_token };
+ }
+
+ @Get("logout")
+ @HttpCode(HttpStatus.OK)
+ async logout(@Res({ passthrough: true }) res: Response) {
+ res.clearCookie(this.configService.get("ACCESS_TOKEN_COOKIE_NAME"))
+ .clearCookie(this.configService.get("REFRESH_TOKEN_COOKIE_NAME"))
+ .end();
+ }
+
+ @UseGuards(AuthGuard)
+ @Get("profile")
+ @HttpCode(HttpStatus.OK)
+ async profile(@Req() req: Request) {
+ const { id, email, name } = await this.usersService.findUserById(req.user.id);
+
+ return { id, email, name };
+ }
+}
+```
+
+### Auth service
+
+```ts showLineNumbers filename="auth/auth.service.ts" copy
+import { Injectable } from "@nestjs/common";
+import { UsersService } from "src/users/users.service";
+import * as jwt from "jsonwebtoken";
+import * as argon2 from "argon2";
+import { ConfigService } from "@nestjs/config";
+import { CreateUserDto } from "src/users/dto/create-user.dto";
+import { LoginDto } from "./dto/login.dto";
+import { Request } from "express";
+
+@Injectable()
+export class AuthService {
+ constructor(
+ private readonly usersService: UsersService,
+ private readonly configService: ConfigService
+ ) {}
+
+ async signup(userDetails: CreateUserDto) {
+ const user = await this.usersService.createUser(userDetails);
+
+ const access_token = jwt.sign({ id: user.id }, this.configService.get("JWT_ACCESS_TOKEN_SECRET"), {
+ expiresIn: this.configService.get("JWT_ACCESS_EXPIRES_IN"),
+ });
+
+ const refresh_token = jwt.sign({ id: user.id }, this.configService.get("JWT_REFRESH_TOKEN_SECRET"), {
+ expiresIn: this.configService.get("JWT_REFRESH_EXPIRES_IN"),
+ });
+
+ return { access_token, refresh_token };
+ }
+
+ async login(credentials: LoginDto) {
+ const user = await this.usersService.findUserByEmail(credentials.email);
+
+ await argon2.verify(user.password, credentials.password).catch();
+
+ const access_token = jwt.sign({ id: user.id }, this.configService.get("JWT_ACCESS_TOKEN_SECRET"), {
+ expiresIn: this.configService.get("JWT_ACCESS_EXPIRES_IN"),
+ });
+
+ const refresh_token = jwt.sign({ id: user.id }, this.configService.get("JWT_REFRESH_TOKEN_SECRET"), {
+ expiresIn: this.configService.get("JWT_REFRESH_EXPIRES_IN"),
+ });
+
+ return { access_token, refresh_token };
+ }
+
+ async refresh(old_refresh_token: string) {
+ const decoded = jwt.verify(
+ old_refresh_token,
+ this.configService.get("JWT_REFRESH_TOKEN_SECRET")
+ ) as Request["user"];
+ const access_token = jwt.sign({ id: decoded.id }, this.configService.get("JWT_ACCESS_TOKEN_SECRET"), {
+ expiresIn: this.configService.get("JWT_ACCESS_EXPIRES_IN"),
+ });
+
+ const refresh_token = jwt.sign({ id: decoded.id }, this.configService.get("JWT_REFRESH_TOKEN_SECRET"), {
+ expiresIn: this.configService.get("JWT_REFRESH_EXPIRES_IN"),
+ });
+
+ return { access_token, refresh_token };
+ }
+}
+```
+
+
+
+## Official example
+
+- [rest-nestjs-prisma-jwt-postgresql](https://github.com/smakosh/roll-your-own-auth/tree/main/examples/rest-nestjs-prisma-jwt-postgres)