Roll Your Own Auth
SessionsServerExpress.jsORMs

Drizzle

Express.js sessions with Drizzle and SQLite

Use the CLI

npx ryo-auth@latest add session-express-drizzle

Manual Installation

drizzle.config.ts
dev.db
index.ts
schema.ts
loginHandler.ts
signupHandler.ts
meHandler.ts
logoutHandler.ts
server.ts

Install dependencies

npm i dotenv express express-session cors redis passport connect-redis argon2 zod cookie

Install dev dependencies

npm i --dev better-sqlite3 drizzle-orm drizzle-kit @types/passport @types/express-session @types/express @types/cookie

Setup your Drizzle schema and config

/src/db/schema.ts
import { sql } from "drizzle-orm";
import { sqliteTable, integer, text } from "drizzle-orm/sqlite-core";

export const users = sqliteTable("users", {
  id: integer("id").primaryKey({ autoIncrement: true }),
  name: text("name").notNull(),
  email: text("email").notNull().unique(),
  password: text("password").notNull(),
  updatedAt: text("updated_at").default(sql`(CURRENT_TIMESTAMP)`),
  createdAt: text("create_at").default(sql`(CURRENT_TIMESTAMP)`),
});
/src/db/index.ts
import { drizzle } from "drizzle-orm/better-sqlite3";
import Database from "better-sqlite3";
import * as schema from "./schema";

const sqlite = new Database("./src/db/dev.db");
export const db = drizzle({ client: sqlite, schema });
drizzle.config.ts
import "dotenv/config";
import { defineConfig } from "drizzle-kit";

export default defineConfig({
  out: "./src/db/drizzle",
  schema: "./src/db/schema.ts",
  dialect: "sqlite",
  dbCredentials: {
    url: "file:./src/db/dev.db",
  },
});

Add imports

server.ts
import { config } from "dotenv";
import express, { Request, Response } from "express";
import session from "express-session";
import cors from "cors";
import { createClient } from "redis";
import passport from "passport";
import RedisStore from "connect-redis";

import { authRouter } from "@/features/auth/routes";

Setup and load env variables

.env
# Server
NODE_ENV="development"
HOST="localhost"
SCHEME="http"
PORT="4000"
CORS_ORIGIN="http://localhost:3000,http://localhost:4000"

# Redis
REDIS_HOST=redis
REDIS_PORT=6379

# API
SESSION_COOKIE_NAME="RollYourOwnAuth"
SESSION_SECRET="21fcYrNnK1OyR+UCTSfYwYMLAAcrtTOvrmj6QEouBAA="
SESSION_MAX_AGE=86400000
server.ts
config();

Setup Redis

server.ts
const app = express();
app.set("trust proxy", 1);

const redisClient = createClient();
redisClient.connect().catch(console.error);
const redisStore = new RedisStore({
  client: redisClient,
  prefix: "auth:",
  disableTouch: true,
});

redisClient.on("error", (error: Error) => {
  logger.error(error.message);
});
redisClient.on("connect", function () {
  console.log(
    `Redis connected at ${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`
  );
});

App uses()

server.ts
app.use(
  cors({
    origin: (origin, callback) => {
      const origins = String(process.env.CORS_ORIGIN).split(",");
      if (!origin || origins.includes(String(origin))) {
        callback(null, true);
      } else {
        callback(new Error("Not allowed."), false);
      }
    },
    credentials: true,
    optionsSuccessStatus: 200,
  })
);
app.use(
  session({
    name: process.env.SESSION_COOKIE_NAME,
    secret: String(process.env.SESSION_SECRET),
    store: redisStore,
    cookie: {
      secure: process.env.NODE_ENV === "production",
      httpOnly: true,
      maxAge: Number(process.env.SESSION_MAX_AGE),
      sameSite: "lax",
    },
    resave: false,
    saveUninitialized: false,
  })
);
app.use(passport.initialize());
app.use(passport.session());

Add Auth router

server.ts
app.get("/", (_req: Request, res: Response) =>
  res.send("Roll Your Own Auth API")
);

app.use("/auth", authRouter);

app.listen(process.env.PORT, () => {
  console.log(
    `🚀 Server ready at ${process.env.SCHEME}://${process.env.HOST}:${process.env.PORT}`
  );
});

Auth routes

/routes/index.ts
import express from "express";

import { validate } from "@/features/auth/middlewares/validate";
import { signupSchema } from "@/features/auth/validation/signup";
import { loginSchema } from "@/features/auth/validation/login";

import { logoutHandler } from "@/features/auth/controllers/logoutHandler";
import { meHandler } from "@/features/auth/controllers/meHandler";
import { signupHandler } from "@/features/auth/controllers/signupHandler";
import { loginHandler } from "@/features/auth/controllers/loginHandler";

const authRouter = express.Router();

authRouter.post("/signup", validate(signupSchema), signupHandler);
authRouter.post("/login", validate(loginSchema), loginHandler);
authRouter.get("/logout", logoutHandler);
authRouter.get("/me", meHandler);

export { authRouter };

/login controller

/controllers/loginHandler.ts
import { Request, Response } from "express";
import argon2 from "argon2";
import { eq } from "drizzle-orm";

import { CustomSessionData } from "@/features/auth/types";
import { db } from "@/db";
import { users } from "@/db/schema";

export const loginHandler = async (req: Request, res: Response) => {
  const credentials = req.body;

  const user = await db.query.users.findFirst({
    where: eq(users.email, credentials.email),
  });

  if (!user) {
    return res.status(404).json({
      message: "User not found",
    });
  }

  const passwordMatch = await argon2.verify(user.password, credentials.password).catch(() => false);

  if (!passwordMatch) {
    return res.status(401).json({
      message: "Invalid credentials",
    });
  }

  (req.session as CustomSessionData).userId = String(user.id);

  const userData = {
    id: user.id,
    name: user.name,
    email: user.email,
  };

  return res.status(200).json(userData);
};

/signup controller

/controllers/signupHandler.ts
import { Request, Response } from "express";
import argon2 from "argon2";

import { CustomSessionData } from "@/features/auth/types";
import { db } from "@/db";
import { users } from "@/db/schema";
import { eq } from "drizzle-orm";

export const signupHandler = async (req: Request, res: Response) => {
  const userDetails = req.body;

  const existingUser = await db.query.users.findFirst({
    where: eq(users.email, userDetails.email)
  });

  if (existingUser?.id) {
    return res.status(409).json({message: "Email already exists, please use a different email or login."})
  }

  const hashedPasword = await argon2.hash(userDetails.password);

  const user = await db.insert(users).values({
    name: userDetails.name,
    email: userDetails.email,
    password: hashedPasword
  }).returning({ id: users.id });

  (req.session as CustomSessionData).userId = String(user[0].id);

  return res.status(200).json(user[0]);
};

/me controller

/controllers/meHandler.ts
import { Request, Response } from "express";
import { eq } from "drizzle-orm";

import { CustomSessionData } from "@/features/auth/types";
import { db } from "@/db";
import { users } from "@/db/schema";

export const meHandler = async (req: Request, res: Response) => {
  const session: CustomSessionData = req.session;
  const sessionId: string | undefined = session.userId;
  const userId = (req?.user as any)?.id;

  if (!sessionId && !userId) {
    return res.status(400).json({ message: "User already logged out." });
  }

  const remainingTime = session.cookie.maxAge || 0;

  if (remainingTime <= 0) {
    return res.json({ message: "Session has expired." }).status(401);
  }

  const user = await db.query.users.findFirst({
    where: eq(users.id, sessionId || userId),
    columns: {
      password: false,
    },
  });

  return res.status(200).json(user);
};

/logout controller

/controllers/logoutHandler.ts
import { Request, Response } from "express";

export const logoutHandler = async (req: Request, res: Response) => {
  req.session.destroy((err) => {
    if (process.env.SESSION_COOKIE_NAME) {
      res.clearCookie(process.env.SESSION_COOKIE_NAME);
    }
    if (err) {
      res
        .json({
          message: err.message,
        })
        .status(400);
    }

    res
      .json({
        message: "User logged out succcessfully",
      })
      .status(200);
  });
};

You have to install and start Redis locally

Official example

On this page