From 46c05ca3a98611df08ad67386fb15ddc5c3b8c05 Mon Sep 17 00:00:00 2001
From: creations <creations@creations.works>
Date: Sun, 2 Mar 2025 17:36:45 -0500
Subject: [PATCH] start of the 50th remake of a file host doomed to never be
 finished

---
 .editorconfig                   |  12 ++
 .env                            |  17 +++
 .gitattributes                  |   1 +
 .gitignore                      |   2 +
 README.md                       |   3 +
 config/environment.ts           |  27 ++++
 config/sql/settings.ts          | 177 +++++++++++++++++++++++
 config/sql/users.ts             | 174 ++++++++++++++++++++++
 eslint.config.js                | 132 +++++++++++++++++
 package.json                    |  35 +++++
 public/assets/favicon.ico       | Bin 0 -> 15406 bytes
 src/helpers/auth.ts             |  50 +++++++
 src/helpers/char.ts             |  13 ++
 src/helpers/ejs.ts              |  26 ++++
 src/helpers/logger.ts           | 205 ++++++++++++++++++++++++++
 src/helpers/redis.ts            | 204 ++++++++++++++++++++++++++
 src/helpers/sessions.ts         | 162 +++++++++++++++++++++
 src/index.ts                    |  54 +++++++
 src/routes/api/auth/login.ts    | 161 +++++++++++++++++++++
 src/routes/api/auth/logout.ts   |  50 +++++++
 src/routes/api/auth/register.ts | 197 +++++++++++++++++++++++++
 src/routes/api/index.ts         |  15 ++
 src/routes/index.ts             |  17 +++
 src/server.ts                   | 247 ++++++++++++++++++++++++++++++++
 src/views/index.ejs             |   8 ++
 src/websocket.ts                |  34 +++++
 tsconfig.json                   |  51 +++++++
 types/bun.d.ts                  |  15 ++
 types/config.d.ts               |  10 ++
 types/ejs.d.ts                  |   3 +
 types/logger.d.ts               |   9 ++
 types/routes.d.ts               |  15 ++
 types/session.d.ts              |  29 ++++
 33 files changed, 2155 insertions(+)
 create mode 100644 .editorconfig
 create mode 100644 .env
 create mode 100644 .gitattributes
 create mode 100644 .gitignore
 create mode 100644 README.md
 create mode 100644 config/environment.ts
 create mode 100644 config/sql/settings.ts
 create mode 100644 config/sql/users.ts
 create mode 100644 eslint.config.js
 create mode 100644 package.json
 create mode 100644 public/assets/favicon.ico
 create mode 100644 src/helpers/auth.ts
 create mode 100644 src/helpers/char.ts
 create mode 100644 src/helpers/ejs.ts
 create mode 100644 src/helpers/logger.ts
 create mode 100644 src/helpers/redis.ts
 create mode 100644 src/helpers/sessions.ts
 create mode 100644 src/index.ts
 create mode 100644 src/routes/api/auth/login.ts
 create mode 100644 src/routes/api/auth/logout.ts
 create mode 100644 src/routes/api/auth/register.ts
 create mode 100644 src/routes/api/index.ts
 create mode 100644 src/routes/index.ts
 create mode 100644 src/server.ts
 create mode 100644 src/views/index.ejs
 create mode 100644 src/websocket.ts
 create mode 100644 tsconfig.json
 create mode 100644 types/bun.d.ts
 create mode 100644 types/config.d.ts
 create mode 100644 types/ejs.d.ts
 create mode 100644 types/logger.d.ts
 create mode 100644 types/routes.d.ts
 create mode 100644 types/session.d.ts

diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..980ef21
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,12 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = tab
+indent_size = 4
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = true
+insert_final_newline = true
diff --git a/.env b/.env
new file mode 100644
index 0000000..951e1cb
--- /dev/null
+++ b/.env
@@ -0,0 +1,17 @@
+# NODE_ENV=development
+HOST=0.0.0.0
+PORT=8080
+
+PGHOST=10.0.0.40
+PGPORT=5432
+PGUSERNAME=postgres
+PGPASSWORD=postgres
+PGDATABASE=postgres
+
+REDIS_HOST=10.0.0.40
+REDIS_PORT=6379
+# REDIS_USERNAME=redis
+# REDIS_PASSWORD=redis
+
+JWT_SECRET=467e5faeda7780bc442f41b8c421d645b5666543d1ae145ad4717f0aff890eb30b971e0700e2bccd3988d653413126b1c63793a49525770b586a6a10b604542e6627c8004090a8ddcd4cd02e9d829a9834850740c02631f7bbd95919be4228065d9aaf35f32eefa28af54470e0a0bc9338fccc22dd0896e3d434ded40f0bcc1c30c24aca39a11417e502e260167759d21fb54fa18dd74db8ec50c0f5f06365862e7f412c2075110f0a9718095e5b260942d38a06c744a0b18bcc11e2c7d730890d5ae933579e11b117f295b372832348849cf448c495424e5a6570e066bd33871aa9c374cc000a512d63e24463ba68554e8f0102de310f38905d794cf8b60424
+JWT_EXPIRES=1d
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..6313b56
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1 @@
+* text=auto eol=lf
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..58b5bae
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/node_modules
+bun.lock
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1b56686
--- /dev/null
+++ b/README.md
@@ -0,0 +1,3 @@
+# bun frontend template
+
+a simle bun frontend starting point i made and use
diff --git a/config/environment.ts b/config/environment.ts
new file mode 100644
index 0000000..7adf22f
--- /dev/null
+++ b/config/environment.ts
@@ -0,0 +1,27 @@
+export const environment: Environment = {
+	port: parseInt(process.env.PORT || "8080", 10),
+	host: process.env.HOST || "0.0.0.0",
+	development:
+		process.env.NODE_ENV === "development" ||
+		process.argv.includes("--dev"),
+};
+
+export const redisConfig: {
+	host: string;
+	port: number;
+	username?: string | undefined;
+	password?: string | undefined;
+} = {
+	host: process.env.REDIS_HOST || "localhost",
+	port: parseInt(process.env.REDIS_PORT || "6379", 10),
+	username: process.env.REDIS_USERNAME || undefined,
+	password: process.env.REDIS_PASSWORD || undefined,
+};
+
+export const jwt: {
+	secret: string;
+	expiresIn: string;
+} = {
+	secret: process.env.JWT_SECRET || "",
+	expiresIn: process.env.JWT_EXPIRES || "1d",
+};
diff --git a/config/sql/settings.ts b/config/sql/settings.ts
new file mode 100644
index 0000000..11c4748
--- /dev/null
+++ b/config/sql/settings.ts
@@ -0,0 +1,177 @@
+import { logger } from "@helpers/logger";
+import { type ReservedSQL, sql } from "bun";
+
+const defaultSettings: { key: string; value: string }[] = [
+	{ key: "default_role", value: "user" },
+	{ key: "default_timezone", value: "UTC" },
+	{ key: "server_timezone", value: "UTC" },
+	{ key: "enable_registration", value: "false" },
+	{ key: "enable_invitations", value: "true" },
+	{ key: "allow_user_invites", value: "false" },
+	{ key: "require_email_verification", value: "false" },
+];
+
+export async function createTable(reservation?: ReservedSQL): Promise<void> {
+	let selfReservation: boolean = false;
+
+	if (!reservation) {
+		reservation = await sql.reserve();
+		selfReservation = true;
+	}
+
+	try {
+		await reservation`
+		CREATE TABLE IF NOT EXISTS settings (
+			"key" VARCHAR(64) PRIMARY KEY NOT NULL UNIQUE,
+			"value" TEXT NOT NULL,
+			created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
+			updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL
+		);`;
+
+		for (const setting of defaultSettings) {
+			await reservation`
+			INSERT INTO settings ("key", "value")
+			VALUES (${setting.key}, ${setting.value})
+			ON CONFLICT ("key")
+			DO NOTHING;`;
+		}
+	} catch (error) {
+		logger.error(["Could not create the settings table:", error as Error]);
+		throw error;
+	} finally {
+		if (selfReservation) {
+			reservation.release();
+		}
+	}
+}
+
+export async function drop(
+	cascade: boolean,
+	reservation?: ReservedSQL,
+): Promise<void> {
+	let selfReservation: boolean = false;
+
+	if (!reservation) {
+		reservation = await sql.reserve();
+		selfReservation = true;
+	}
+
+	try {
+		await reservation`DROP TABLE IF EXISTS settings ${cascade ? "CASCADE" : ""};`;
+	} catch (error) {
+		logger.error(["Could not drop the settings table:", error as Error]);
+		throw error;
+	} finally {
+		if (selfReservation) {
+			reservation.release();
+		}
+	}
+}
+
+// * Validation functions
+
+export async function getSetting(
+	key: string,
+	reservation?: ReservedSQL,
+): Promise<string | null> {
+	let selfReservation: boolean = false;
+
+	if (!reservation) {
+		reservation = await sql.reserve();
+		selfReservation = true;
+	}
+
+	try {
+		const result: { value: string }[] =
+			await reservation`SELECT value FROM settings WHERE "key" = ${key};`;
+
+		if (result.length === 0) {
+			return null;
+		}
+
+		return result[0].value;
+	} catch (error) {
+		logger.error(["Could not get the setting:", error as Error]);
+		throw error;
+	} finally {
+		if (selfReservation) {
+			reservation.release();
+		}
+	}
+}
+
+export async function setSetting(
+	key: string,
+	value: string,
+	reservation?: ReservedSQL,
+): Promise<void> {
+	let selfReservation: boolean = false;
+
+	if (!reservation) {
+		reservation = await sql.reserve();
+		selfReservation = true;
+	}
+
+	try {
+		await reservation`
+		INSERT INTO settings ("key", "value")
+		VALUES (${key}, ${value})
+		ON CONFLICT ("key")
+		DO UPDATE SET "value" = ${value};`;
+	} catch (error) {
+		logger.error(["Could not set the setting:", error as Error]);
+		throw error;
+	} finally {
+		if (selfReservation) {
+			reservation.release();
+		}
+	}
+}
+
+export async function deleteSetting(
+	key: string,
+	reservation?: ReservedSQL,
+): Promise<void> {
+	let selfReservation: boolean = false;
+
+	if (!reservation) {
+		reservation = await sql.reserve();
+		selfReservation = true;
+	}
+
+	try {
+		await reservation`DELETE FROM settings WHERE "key" = ${key};`;
+	} catch (error) {
+		logger.error(["Could not delete the setting:", error as Error]);
+		throw error;
+	} finally {
+		if (selfReservation) {
+			reservation.release();
+		}
+	}
+}
+
+export async function getAllSettings(
+	reservation?: ReservedSQL,
+): Promise<{ key: string; value: string }[]> {
+	let selfReservation: boolean = false;
+
+	if (!reservation) {
+		reservation = await sql.reserve();
+		selfReservation = true;
+	}
+
+	try {
+		const result: { key: string; value: string }[] =
+			await reservation`SELECT "key", "value" FROM settings;`;
+
+		return result;
+	} catch (error) {
+		logger.error(["Could not get all settings:", error as Error]);
+		throw error;
+	} finally {
+		if (selfReservation) {
+			reservation.release();
+		}
+	}
+}
diff --git a/config/sql/users.ts b/config/sql/users.ts
new file mode 100644
index 0000000..24f90ff
--- /dev/null
+++ b/config/sql/users.ts
@@ -0,0 +1,174 @@
+import { logger } from "@helpers/logger";
+import { type ReservedSQL, sql } from "bun";
+
+export async function createTable(reservation?: ReservedSQL): Promise<void> {
+	let selfReservation: boolean = false;
+
+	if (!reservation) {
+		reservation = await sql.reserve();
+		selfReservation = true;
+	}
+
+	try {
+		await reservation`
+		CREATE TABLE IF NOT EXISTS users (
+			id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
+			authorization_token UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(),
+			username VARCHAR(20) NOT NULL UNIQUE,
+			email VARCHAR(254) NOT NULL UNIQUE,
+			email_verified boolean NOT NULL DEFAULT false,
+			password TEXT NOT NULL,
+			avatar boolean NOT NULL DEFAULT false,
+			roles TEXT[] NOT NULL DEFAULT ARRAY['user'],
+			timezone VARCHAR(64) DEFAULT NULL,
+			invited_by UUID REFERENCES users(id) ON DELETE SET NULL,
+			created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
+			last_seen TIMESTAMPTZ DEFAULT NOW() NOT NULL
+		);`;
+	} catch (error) {
+		logger.error(["Could not create the users table:", error as Error]);
+		throw error;
+	} finally {
+		if (selfReservation) {
+			reservation.release();
+		}
+	}
+}
+
+export async function drop(
+	cascade: boolean,
+	reservation?: ReservedSQL,
+): Promise<void> {
+	let selfReservation: boolean = false;
+
+	if (!reservation) {
+		reservation = await sql.reserve();
+		selfReservation = true;
+	}
+
+	try {
+		await reservation`DROP TABLE IF EXISTS users ${cascade ? "CASCADE" : ""};`;
+	} catch (error) {
+		logger.error(["Could not drop the users table:", error as Error]);
+		throw error;
+	} finally {
+		if (selfReservation) {
+			reservation.release();
+		}
+	}
+}
+
+// * Validation functions
+
+// ? should support non english characters but wont mess up the url
+export const userNameRestrictions: {
+	length: { min: number; max: number };
+	regex: RegExp;
+} = {
+	length: { min: 3, max: 20 },
+	regex: /^[\p{L}0-9._-]+$/u,
+};
+
+export const passwordRestrictions: {
+	length: { min: number; max: number };
+	regex: RegExp;
+} = {
+	length: { min: 12, max: 64 },
+	regex: /^(?=.*\p{Ll})(?=.*\p{Lu})(?=.*\d)(?=.*[^\w\s]).{12,64}$/u,
+};
+
+export const emailRestrictions: { regex: RegExp } = {
+	regex: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
+};
+
+export const inviteRestrictions: { min: number; max: number; regex: RegExp } = {
+	min: 4,
+	max: 15,
+	regex: /^[a-zA-Z0-9]+$/,
+};
+
+export function isValidUsername(username: string): {
+	valid: boolean;
+	error?: string;
+} {
+	if (username.length < userNameRestrictions.length.min) {
+		return { valid: false, error: "Username is too short" };
+	}
+
+	if (username.length > userNameRestrictions.length.max) {
+		return { valid: false, error: "Username is too long" };
+	}
+
+	if (!userNameRestrictions.regex.test(username)) {
+		return { valid: false, error: "Username contains invalid characters" };
+	}
+
+	return { valid: true };
+}
+
+export function isValidPassword(password: string): {
+	valid: boolean;
+	error?: string;
+} {
+	if (password.length < passwordRestrictions.length.min) {
+		return {
+			valid: false,
+			error: `Password must be at least ${passwordRestrictions.length.min} characters long`,
+		};
+	}
+
+	if (password.length > passwordRestrictions.length.max) {
+		return {
+			valid: false,
+			error: `Password can't be longer than ${passwordRestrictions.length.max} characters`,
+		};
+	}
+
+	if (!passwordRestrictions.regex.test(password)) {
+		return {
+			valid: false,
+			error: "Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character",
+		};
+	}
+
+	return { valid: true };
+}
+
+export function isValidEmail(email: string): {
+	valid: boolean;
+	error?: string;
+} {
+	if (!emailRestrictions.regex.test(email)) {
+		return { valid: false, error: "Invalid email address" };
+	}
+
+	return { valid: true };
+}
+
+export function isValidInvite(invite: string): {
+	valid: boolean;
+	error?: string;
+} {
+	if (invite.length < inviteRestrictions.min) {
+		return {
+			valid: false,
+			error: `Invite code must be at least ${inviteRestrictions.min} characters long`,
+		};
+	}
+
+	if (invite.length > inviteRestrictions.max) {
+		return {
+			valid: false,
+			error: `Invite code can't be longer than ${inviteRestrictions.max} characters`,
+		};
+	}
+
+	if (!inviteRestrictions.regex.test(invite)) {
+		return {
+			valid: false,
+			error: "Invite code contains invalid characters",
+		};
+	}
+
+	return { valid: true };
+}
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..d43df76
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,132 @@
+import pluginJs from "@eslint/js";
+import tseslintPlugin from "@typescript-eslint/eslint-plugin";
+import tsParser from "@typescript-eslint/parser";
+import prettier from "eslint-plugin-prettier";
+import promisePlugin from "eslint-plugin-promise";
+import simpleImportSort from "eslint-plugin-simple-import-sort";
+import unicorn from "eslint-plugin-unicorn";
+import unusedImports from "eslint-plugin-unused-imports";
+import globals from "globals";
+
+/** @type {import('eslint').Linter.FlatConfig[]} */
+export default [
+	{
+		files: ["**/*.{js,mjs,cjs}"],
+		languageOptions: {
+			globals: globals.node,
+		},
+		...pluginJs.configs.recommended,
+		plugins: {
+			"simple-import-sort": simpleImportSort,
+			"unused-imports": unusedImports,
+			promise: promisePlugin,
+			prettier: prettier,
+			unicorn: unicorn,
+		},
+		rules: {
+			"eol-last": ["error", "always"],
+			"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1 }],
+			"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
+			"simple-import-sort/imports": "error",
+			"simple-import-sort/exports": "error",
+			"unused-imports/no-unused-imports": "error",
+			"unused-imports/no-unused-vars": [
+				"warn",
+				{
+					vars: "all",
+					varsIgnorePattern: "^_",
+					args: "after-used",
+					argsIgnorePattern: "^_",
+				},
+			],
+			"promise/always-return": "error",
+			"promise/no-return-wrap": "error",
+			"promise/param-names": "error",
+			"promise/catch-or-return": "error",
+			"promise/no-nesting": "warn",
+			"promise/no-promise-in-callback": "warn",
+			"promise/no-callback-in-promise": "warn",
+			"prettier/prettier": [
+				"error",
+				{
+					useTabs: true,
+					tabWidth: 4,
+				},
+			],
+			indent: ["error", "tab", { SwitchCase: 1 }],
+			"unicorn/filename-case": [
+				"error",
+				{
+					case: "camelCase",
+				},
+			],
+		},
+	},
+	{
+		files: ["**/*.{ts,tsx}"],
+		languageOptions: {
+			parser: tsParser,
+			globals: globals.node,
+		},
+		plugins: {
+			"@typescript-eslint": tseslintPlugin,
+			"simple-import-sort": simpleImportSort,
+			"unused-imports": unusedImports,
+			promise: promisePlugin,
+			prettier: prettier,
+			unicorn: unicorn,
+		},
+		rules: {
+			...tseslintPlugin.configs.recommended.rules,
+			quotes: ["error", "double"],
+			"eol-last": ["error", "always"],
+			"no-multiple-empty-lines": ["error", { max: 1, maxEOF: 1 }],
+			"no-mixed-spaces-and-tabs": ["error", "smart-tabs"],
+			"simple-import-sort/imports": "error",
+			"simple-import-sort/exports": "error",
+			"unused-imports/no-unused-imports": "error",
+			"unused-imports/no-unused-vars": [
+				"warn",
+				{
+					vars: "all",
+					varsIgnorePattern: "^_",
+					args: "after-used",
+					argsIgnorePattern: "^_",
+				},
+			],
+			"promise/always-return": "error",
+			"promise/no-return-wrap": "error",
+			"promise/param-names": "error",
+			"promise/catch-or-return": "error",
+			"promise/no-nesting": "warn",
+			"promise/no-promise-in-callback": "warn",
+			"promise/no-callback-in-promise": "warn",
+			"prettier/prettier": [
+				"error",
+				{
+					useTabs: true,
+					tabWidth: 4,
+				},
+			],
+			indent: ["error", "tab", { SwitchCase: 1 }],
+			"unicorn/filename-case": [
+				"error",
+				{
+					case: "camelCase",
+				},
+			],
+			"@typescript-eslint/explicit-function-return-type": ["error"],
+			"@typescript-eslint/explicit-module-boundary-types": ["error"],
+			"@typescript-eslint/typedef": [
+				"error",
+				{
+					arrowParameter: true,
+					variableDeclaration: true,
+					propertyDeclaration: true,
+					memberVariableDeclaration: true,
+					parameter: true,
+				},
+			],
+		},
+	},
+];
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..d05b594
--- /dev/null
+++ b/package.json
@@ -0,0 +1,35 @@
+{
+	"name": "bun_frontend_template",
+	"module": "src/index.ts",
+	"type": "module",
+	"scripts": {
+		"start": "bun run src/index.ts",
+		"dev": "bun run --watch src/index.ts --dev",
+		"lint": "eslint",
+		"lint:fix": "bun lint --fix",
+		"cleanup": "rm -rf logs node_modules bun.lockdb"
+	},
+	"devDependencies": {
+		"@eslint/js": "^9.21.0",
+		"@types/bun": "^1.2.4",
+		"@types/ejs": "^3.1.5",
+		"@typescript-eslint/eslint-plugin": "^8.25.0",
+		"@typescript-eslint/parser": "^8.25.0",
+		"eslint": "^9.21.0",
+		"eslint-plugin-prettier": "^5.2.3",
+		"eslint-plugin-promise": "^7.2.1",
+		"eslint-plugin-simple-import-sort": "^12.1.1",
+		"eslint-plugin-unicorn": "^56.0.1",
+		"eslint-plugin-unused-imports": "^4.1.4",
+		"globals": "^15.15.0",
+		"prettier": "^3.5.2"
+	},
+	"peerDependencies": {
+		"typescript": "^5.7.3"
+	},
+	"dependencies": {
+		"ejs": "^3.1.10",
+		"fast-jwt": "^5.0.5",
+		"redis": "^4.7.0"
+	}
+}
diff --git a/public/assets/favicon.ico b/public/assets/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..69ec50db0ae55e9b54fd9fbf88c34bb1c420fb63
GIT binary patch
literal 15406
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

literal 0
HcmV?d00001

diff --git a/src/helpers/auth.ts b/src/helpers/auth.ts
new file mode 100644
index 0000000..be59ff9
--- /dev/null
+++ b/src/helpers/auth.ts
@@ -0,0 +1,50 @@
+import { isUUID } from "@helpers/char";
+import { logger } from "@helpers/logger";
+import { type ReservedSQL, sql } from "bun";
+
+export async function authByToken(
+	request: ExtendedRequest,
+	reservation?: ReservedSQL,
+): Promise<ApiUserSession | null> {
+	let selfReservation: boolean = false;
+
+	if (!reservation) {
+		reservation = await sql.reserve();
+		selfReservation = true;
+	}
+
+	const authorizationHeader: string | null =
+		request.headers.get("Authorization");
+
+	if (!authorizationHeader || !authorizationHeader.startsWith("Bearer "))
+		return null;
+
+	const authorizationToken: string = authorizationHeader.slice(7).trim();
+	if (!authorizationToken || !isUUID(authorizationToken)) return null;
+
+	try {
+		const result: UserSession[] =
+			await reservation`SELECT id, username, email, roles avatar, timezone, authorization_token FROM users WHERE authorization_token = ${authorizationToken};`;
+
+		if (result.length === 0) return null;
+
+		return {
+			id: result[0].id,
+			username: result[0].username,
+			email: result[0].email,
+			email_verified: result[0].email_verified,
+			roles: result[0].roles,
+			avatar: result[0].avatar,
+			timezone: result[0].timezone,
+			authorization_token: result[0].authorization_token,
+			is_api: true,
+		};
+	} catch (error) {
+		logger.error(["Could not authenticate by token:", error as Error]);
+		return null;
+	} finally {
+		if (selfReservation) {
+			reservation.release();
+		}
+	}
+}
diff --git a/src/helpers/char.ts b/src/helpers/char.ts
new file mode 100644
index 0000000..7ec0bff
--- /dev/null
+++ b/src/helpers/char.ts
@@ -0,0 +1,13 @@
+export function timestampToReadable(timestamp?: number): string {
+	const date: Date =
+		timestamp && !isNaN(timestamp) ? new Date(timestamp) : new Date();
+	if (isNaN(date.getTime())) return "Invalid Date";
+	return date.toISOString().replace("T", " ").replace("Z", "");
+}
+
+export function isUUID(uuid: string): boolean {
+	const regex: RegExp =
+		/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
+
+	return regex.test(uuid);
+}
diff --git a/src/helpers/ejs.ts b/src/helpers/ejs.ts
new file mode 100644
index 0000000..6b03dd0
--- /dev/null
+++ b/src/helpers/ejs.ts
@@ -0,0 +1,26 @@
+import { renderFile } from "ejs";
+import { resolve } from "path";
+
+export async function renderEjsTemplate(
+	viewName: string | string[],
+	data: EjsTemplateData,
+	headers?: Record<string, string | number | boolean>,
+): Promise<Response> {
+	let templatePath: string;
+
+	if (Array.isArray(viewName)) {
+		templatePath = resolve("src", "views", ...viewName);
+	} else {
+		templatePath = resolve("src", "views", viewName);
+	}
+
+	if (!templatePath.endsWith(".ejs")) {
+		templatePath += ".ejs";
+	}
+
+	const html: string = await renderFile(templatePath, data);
+
+	return new Response(html, {
+		headers: { "Content-Type": "text/html", ...headers },
+	});
+}
diff --git a/src/helpers/logger.ts b/src/helpers/logger.ts
new file mode 100644
index 0000000..331be1d
--- /dev/null
+++ b/src/helpers/logger.ts
@@ -0,0 +1,205 @@
+import { environment } from "@config/environment";
+import { timestampToReadable } from "@helpers/char";
+import type { Stats } from "fs";
+import {
+	createWriteStream,
+	existsSync,
+	mkdirSync,
+	statSync,
+	WriteStream,
+} from "fs";
+import { EOL } from "os";
+import { basename, join } from "path";
+
+class Logger {
+	private static instance: Logger;
+	private static log: string = join(__dirname, "../../logs");
+
+	public static getInstance(): Logger {
+		if (!Logger.instance) {
+			Logger.instance = new Logger();
+		}
+
+		return Logger.instance;
+	}
+
+	private writeToLog(logMessage: string): void {
+		if (environment.development) return;
+
+		const date: Date = new Date();
+		const logDir: string = Logger.log;
+		const logFile: string = join(
+			logDir,
+			`${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}.log`,
+		);
+
+		if (!existsSync(logDir)) {
+			mkdirSync(logDir, { recursive: true });
+		}
+
+		let addSeparator: boolean = false;
+
+		if (existsSync(logFile)) {
+			const fileStats: Stats = statSync(logFile);
+			if (fileStats.size > 0) {
+				const lastModified: Date = new Date(fileStats.mtime);
+				if (
+					lastModified.getFullYear() === date.getFullYear() &&
+					lastModified.getMonth() === date.getMonth() &&
+					lastModified.getDate() === date.getDate() &&
+					lastModified.getHours() !== date.getHours()
+				) {
+					addSeparator = true;
+				}
+			}
+		}
+
+		const stream: WriteStream = createWriteStream(logFile, { flags: "a" });
+
+		if (addSeparator) {
+			stream.write(`${EOL}${date.toISOString()}${EOL}`);
+		}
+
+		stream.write(`${logMessage}${EOL}`);
+		stream.close();
+	}
+
+	private extractFileName(stack: string): string {
+		const stackLines: string[] = stack.split("\n");
+		let callerFile: string = "";
+
+		for (let i: number = 2; i < stackLines.length; i++) {
+			const line: string = stackLines[i].trim();
+			if (line && !line.includes("Logger.") && line.includes("(")) {
+				callerFile = line.split("(")[1]?.split(")")[0] || "";
+				break;
+			}
+		}
+
+		return basename(callerFile);
+	}
+
+	private getCallerInfo(stack: unknown): {
+		filename: string;
+		timestamp: string;
+	} {
+		const filename: string =
+			typeof stack === "string" ? this.extractFileName(stack) : "unknown";
+
+		const readableTimestamp: string = timestampToReadable();
+
+		return { filename, timestamp: readableTimestamp };
+	}
+
+	public info(message: string | string[], breakLine: boolean = false): void {
+		const stack: string = new Error().stack || "";
+		const { filename, timestamp } = this.getCallerInfo(stack);
+
+		const joinedMessage: string = Array.isArray(message)
+			? message.join(" ")
+			: message;
+
+		const logMessageParts: ILogMessageParts = {
+			readableTimestamp: { value: timestamp, color: "90" },
+			level: { value: "[INFO]", color: "32" },
+			filename: { value: `(${filename})`, color: "36" },
+			message: { value: joinedMessage, color: "0" },
+		};
+
+		this.writeToLog(`${timestamp} [INFO] (${filename}) ${joinedMessage}`);
+		this.writeConsoleMessageColored(logMessageParts, breakLine);
+	}
+
+	public warn(message: string | string[], breakLine: boolean = false): void {
+		const stack: string = new Error().stack || "";
+		const { filename, timestamp } = this.getCallerInfo(stack);
+
+		const joinedMessage: string = Array.isArray(message)
+			? message.join(" ")
+			: message;
+
+		const logMessageParts: ILogMessageParts = {
+			readableTimestamp: { value: timestamp, color: "90" },
+			level: { value: "[WARN]", color: "33" },
+			filename: { value: `(${filename})`, color: "36" },
+			message: { value: joinedMessage, color: "0" },
+		};
+
+		this.writeToLog(`${timestamp} [WARN] (${filename}) ${joinedMessage}`);
+		this.writeConsoleMessageColored(logMessageParts, breakLine);
+	}
+
+	public error(
+		message: string | Error | (string | Error)[],
+		breakLine: boolean = false,
+	): void {
+		const stack: string = new Error().stack || "";
+		const { filename, timestamp } = this.getCallerInfo(stack);
+
+		const messages: (string | Error)[] = Array.isArray(message)
+			? message
+			: [message];
+		const joinedMessage: string = messages
+			.map((msg: string | Error): string =>
+				typeof msg === "string" ? msg : msg.message,
+			)
+			.join(" ");
+
+		const logMessageParts: ILogMessageParts = {
+			readableTimestamp: { value: timestamp, color: "90" },
+			level: { value: "[ERROR]", color: "31" },
+			filename: { value: `(${filename})`, color: "36" },
+			message: { value: joinedMessage, color: "0" },
+		};
+
+		this.writeToLog(`${timestamp} [ERROR] (${filename}) ${joinedMessage}`);
+		this.writeConsoleMessageColored(logMessageParts, breakLine);
+	}
+
+	public custom(
+		bracketMessage: string,
+		bracketMessage2: string,
+		message: string | string[],
+		color: string,
+		breakLine: boolean = false,
+	): void {
+		const stack: string = new Error().stack || "";
+		const { timestamp } = this.getCallerInfo(stack);
+
+		const joinedMessage: string = Array.isArray(message)
+			? message.join(" ")
+			: message;
+
+		const logMessageParts: ILogMessageParts = {
+			readableTimestamp: { value: timestamp, color: "90" },
+			level: { value: bracketMessage, color },
+			filename: { value: `${bracketMessage2}`, color: "36" },
+			message: { value: joinedMessage, color: "0" },
+		};
+
+		this.writeToLog(
+			`${timestamp} ${bracketMessage} (${bracketMessage2}) ${joinedMessage}`,
+		);
+		this.writeConsoleMessageColored(logMessageParts, breakLine);
+	}
+
+	public space(): void {
+		console.log();
+	}
+
+	private writeConsoleMessageColored(
+		logMessageParts: ILogMessageParts,
+		breakLine: boolean = false,
+	): void {
+		const logMessage: string = Object.keys(logMessageParts)
+			.map((key: string) => {
+				const part: ILogMessagePart = logMessageParts[key];
+				return `\x1b[${part.color}m${part.value}\x1b[0m`;
+			})
+			.join(" ");
+		console.log(logMessage + (breakLine ? EOL : ""));
+	}
+}
+
+const logger: Logger = Logger.getInstance();
+export { logger };
diff --git a/src/helpers/redis.ts b/src/helpers/redis.ts
new file mode 100644
index 0000000..5cd1e54
--- /dev/null
+++ b/src/helpers/redis.ts
@@ -0,0 +1,204 @@
+import { redisConfig } from "@config/environment";
+import { logger } from "@helpers/logger";
+import { createClient, type RedisClientType } from "redis";
+
+class RedisJson {
+	private static instance: RedisJson | null = null;
+	private client: RedisClientType | null = null;
+
+	private constructor() {}
+
+	public static async initialize(): Promise<RedisJson> {
+		if (!RedisJson.instance) {
+			RedisJson.instance = new RedisJson();
+			RedisJson.instance.client = createClient({
+				socket: {
+					host: redisConfig.host,
+					port: redisConfig.port,
+				},
+				username: redisConfig.username || undefined,
+				password: redisConfig.password || undefined,
+			});
+
+			RedisJson.instance.client.on("error", (err: Error) => {
+				logger.error([
+					"Error connecting to Redis:",
+					err,
+					redisConfig.host,
+				]);
+
+				process.exit(1);
+			});
+
+			RedisJson.instance.client.on("connect", () => {
+				logger.info([
+					"Connected to Redis on",
+					`${redisConfig.host}:${redisConfig.port}`,
+				]);
+			});
+
+			await RedisJson.instance.client.connect();
+		}
+
+		return RedisJson.instance;
+	}
+
+	public static getInstance(): RedisJson {
+		if (!RedisJson.instance || !RedisJson.instance.client) {
+			throw new Error(
+				"Redis instance not initialized. Call initialize() first.",
+			);
+		}
+		return RedisJson.instance;
+	}
+
+	public async disconnect(): Promise<void> {
+		if (!this.client) {
+			logger.error("Redis client is not initialized.");
+			process.exit(1);
+		}
+		try {
+			await this.client.disconnect();
+			this.client = null;
+			logger.info("Redis disconnected successfully.");
+		} catch (error) {
+			logger.error("Error disconnecting Redis client:");
+			logger.error(error as Error);
+			throw error;
+		}
+	}
+
+	public async get(
+		type: "JSON" | "STRING",
+		key: string,
+		path?: string,
+	): Promise<
+		string | number | boolean | Record<string, unknown> | null | unknown
+	> {
+		if (!this.client) {
+			logger.error("Redis client is not initialized.");
+			throw new Error("Redis client is not initialized.");
+		}
+		try {
+			if (type === "JSON") {
+				const value: unknown = await this.client.json.get(key, {
+					path,
+				});
+
+				if (value instanceof Date) {
+					return value.toISOString();
+				}
+
+				return value;
+			} else if (type === "STRING") {
+				const value: string | null = await this.client.get(key);
+				return value;
+			} else {
+				throw new Error(`Invalid type: ${type}`);
+			}
+		} catch (error) {
+			logger.error(`Error getting value from Redis for key: ${key}`);
+			logger.error(error as Error);
+			throw error;
+		}
+	}
+
+	public async set(
+		type: "JSON" | "STRING",
+		key: string,
+		value: unknown,
+		expiresInSeconds?: number,
+		path?: string,
+	): Promise<void> {
+		if (!this.client) {
+			logger.error("Redis client is not initialized.");
+			throw new Error("Redis client is not initialized.");
+		}
+		try {
+			if (type === "JSON") {
+				await this.client.json.set(key, path || "$", value as string);
+
+				if (expiresInSeconds) {
+					await this.client.expire(key, expiresInSeconds);
+				}
+			} else if (type === "STRING") {
+				if (expiresInSeconds) {
+					await this.client.set(key, value as string, {
+						EX: expiresInSeconds,
+					});
+				} else {
+					await this.client.set(key, value as string);
+				}
+			} else {
+				throw new Error(`Invalid type: ${type}`);
+			}
+		} catch (error) {
+			logger.error(`Error setting value in Redis for key: ${key}`);
+			logger.error(error as Error);
+			throw error;
+		}
+	}
+
+	public async delete(type: "JSON" | "STRING", key: string): Promise<void> {
+		if (!this.client) {
+			logger.error("Redis client is not initialized.");
+			throw new Error("Redis client is not initialized.");
+		}
+		try {
+			if (type === "JSON") {
+				await this.client.json.del(key);
+			} else if (type === "STRING") {
+				await this.client.del(key);
+			} else {
+				throw new Error(`Invalid type: ${type}`);
+			}
+		} catch (error) {
+			logger.error(`Error deleting value from Redis for key: ${key}`);
+			logger.error(error as Error);
+			throw error;
+		}
+	}
+
+	public async expire(key: string, seconds: number): Promise<void> {
+		if (!this.client) {
+			logger.error("Redis client is not initialized.");
+			throw new Error("Redis client is not initialized.");
+		}
+		try {
+			await this.client.expire(key, seconds);
+		} catch (error) {
+			logger.error([
+				`Error expiring key in Redis: ${key}`,
+				error as Error,
+			]);
+			throw error;
+		}
+	}
+
+	public async keys(pattern: string): Promise<string[]> {
+		if (!this.client) {
+			logger.error("Redis client is not initialized.");
+			throw new Error("Redis client is not initialized.");
+		}
+		try {
+			const keys: string[] = await this.client.keys(pattern);
+			return keys;
+		} catch (error) {
+			logger.error([
+				`Error getting keys from Redis for pattern: ${pattern}`,
+				error as Error,
+			]);
+			throw error;
+		}
+	}
+}
+
+export const redis: {
+	initialize: () => Promise<RedisJson>;
+	getInstance: () => RedisJson;
+} = {
+	initialize: RedisJson.initialize,
+	getInstance: RedisJson.getInstance,
+};
+
+export { RedisJson };
diff --git a/src/helpers/sessions.ts b/src/helpers/sessions.ts
new file mode 100644
index 0000000..de686a2
--- /dev/null
+++ b/src/helpers/sessions.ts
@@ -0,0 +1,162 @@
+import { jwt } from "@config/environment";
+import { environment } from "@config/environment";
+import { redis } from "@helpers/redis";
+import { createDecoder, createSigner, createVerifier } from "fast-jwt";
+
+type Signer = (payload: UserSession, options?: UserSession) => string;
+type Verifier = (token: string, options?: UserSession) => UserSession;
+type Decoder = (token: string, options?: UserSession) => UserSession;
+
+class SessionManager {
+	private signer: Signer;
+	private verifier: Verifier;
+	private decoder: Decoder;
+
+	constructor() {
+		this.signer = createSigner({
+			key: jwt.secret,
+			expiresIn: jwt.expiresIn,
+		});
+		this.verifier = createVerifier({ key: jwt.secret });
+		this.decoder = createDecoder();
+	}
+
+	public async createSession(
+		payload: UserSession,
+		userAgent: string,
+	): Promise<string> {
+		const token: string = this.signer(payload);
+		const sessionKey: string = `session:${payload.id}:${token}`;
+
+		await redis
+			.getInstance()
+			.set(
+				"JSON",
+				sessionKey,
+				{ ...payload, userAgent },
+				this.getExpirationInSeconds(),
+			);
+
+		const cookie: string = this.generateCookie(token);
+		return cookie;
+	}
+
+	public async getSession(request: Request): Promise<UserSession | null> {
+		const cookie: string | null = request.headers.get("Cookie");
+		if (!cookie) return null;
+
+		const token: string | null =
+			cookie.match(/session=([^;]+)/)?.[1] || null;
+		if (!token) return null;
+
+		const userSessions: string[] = await redis
+			.getInstance()
+			.keys("session:*:" + token);
+		if (!userSessions.length) return null;
+
+		const sessionData: unknown = await redis
+			.getInstance()
+			.get("JSON", userSessions[0]);
+		if (!sessionData) return null;
+
+		const payload: UserSession & { userAgent: string } =
+			sessionData as UserSession & { userAgent: string };
+		return payload;
+	}
+
+	public async verifySession(token: string): Promise<UserSession> {
+		const userSessions: string[] = await redis
+			.getInstance()
+			.keys("session:*:" + token);
+		if (!userSessions.length)
+			throw new Error("Session not found or expired");
+
+		const sessionData: unknown = await redis
+			.getInstance()
+			.get("JSON", userSessions[0]);
+		if (!sessionData) throw new Error("Session not found or expired");
+
+		const payload: UserSession = this.verifier(token);
+		return payload;
+	}
+
+	public async decodeSession(token: string): Promise<UserSession> {
+		const payload: UserSession = this.decoder(token);
+		return payload;
+	}
+
+	public async invalidateSession(request: Request): Promise<void> {
+		const cookie: string | null = request.headers.get("Cookie");
+		if (!cookie) return;
+
+		const token: string | null =
+			cookie.match(/session=([^;]+)/)?.[1] || null;
+		if (!token) return;
+
+		const userSessions: string[] = await redis
+			.getInstance()
+			.keys("session:*:" + token);
+		if (!userSessions.length) return;
+
+		await redis.getInstance().delete("JSON", userSessions[0]);
+	}
+
+	private generateCookie(
+		token: string,
+		maxAge: number = this.getExpirationInSeconds(),
+		options?: {
+			secure?: boolean;
+			httpOnly?: boolean;
+			sameSite?: "Strict" | "Lax" | "None";
+			path?: string;
+			domain?: string;
+		},
+	): string {
+		const {
+			secure = !environment.development,
+			httpOnly = true,
+			sameSite = environment.development ? "Lax" : "None",
+			path = "/",
+			domain,
+		} = options || {};
+
+		let cookie: string = `session=${encodeURIComponent(token)}; Path=${path}; Max-Age=${maxAge}`;
+
+		if (httpOnly) cookie += "; HttpOnly";
+
+		if (secure) cookie += "; Secure";
+
+		if (sameSite) cookie += `; SameSite=${sameSite}`;
+
+		if (domain) cookie += `; Domain=${domain}`;
+
+		return cookie;
+	}
+
+	private getExpirationInSeconds(): number {
+		const match: RegExpMatchArray | null =
+			jwt.expiresIn.match(/^(\d+)([smhd])$/);
+		if (!match) {
+			throw new Error("Invalid expiresIn format in jwt config");
+		}
+
+		const [, value, unit] = match;
+		const num: number = parseInt(value, 10);
+
+		switch (unit) {
+			case "s":
+				return num;
+			case "m":
+				return num * 60;
+			case "h":
+				return num * 3600;
+			case "d":
+				return num * 86400;
+			default:
+				throw new Error("Invalid time unit in expiresIn");
+		}
+	}
+}
+
+const sessionManager: SessionManager = new SessionManager();
+export { sessionManager };
diff --git a/src/index.ts b/src/index.ts
new file mode 100644
index 0000000..7cddd5d
--- /dev/null
+++ b/src/index.ts
@@ -0,0 +1,54 @@
+import { logger } from "@helpers/logger";
+import { type ReservedSQL, sql } from "bun";
+import { readdir } from "fs/promises";
+import { resolve } from "path";
+
+import { serverHandler } from "@/server";
+
+import { redis } from "./helpers/redis";
+
+async function initializeDatabase(): Promise<void> {
+	const sqlDir: string = resolve("config", "sql");
+	const files: string[] = await readdir(sqlDir);
+
+	const reservation: ReservedSQL = await sql.reserve();
+	for (const file of files) {
+		if (file.endsWith(".ts")) {
+			const { createTable } = await import(resolve(sqlDir, file));
+
+			await createTable(reservation);
+		}
+	}
+
+	reservation.release();
+}
+
+async function main(): Promise<void> {
+	try {
+		try {
+			await sql`SELECT 1;`;
+
+			logger.info([
+				"Connected to PostgreSQL on",
+				`${process.env.PGHOST}:${process.env.PGPORT}`,
+			]);
+		} catch (error) {
+			logger.error([
+				"Could not establish a connection to PostgreSQL:",
+				error as Error,
+			]);
+			process.exit(1);
+		}
+
+		await redis.initialize();
+		serverHandler.initialize();
+		await initializeDatabase();
+	} catch (error) {
+		throw error;
+	}
+}
+
+main().catch((error: Error) => {
+	logger.error(["Error initializing the server:", error]);
+	process.exit(1);
+});
diff --git a/src/routes/api/auth/login.ts b/src/routes/api/auth/login.ts
new file mode 100644
index 0000000..e191151
--- /dev/null
+++ b/src/routes/api/auth/login.ts
@@ -0,0 +1,161 @@
+import {
+	isValidEmail,
+	isValidPassword,
+	isValidUsername,
+} from "@config/sql/users";
+import { password as bunPassword, type ReservedSQL, sql } from "bun";
+
+import { logger } from "@/helpers/logger";
+import { sessionManager } from "@/helpers/sessions";
+
+const routeDef: RouteDef = {
+	method: "POST",
+	accepts: "application/json",
+	returns: "application/json",
+	needsBody: "json",
+};
+
+async function handler(
+	request: ExtendedRequest,
+	requestBody: unknown,
+): Promise<Response> {
+	if (request.session) {
+		if ((request.session as ApiUserSession).is_api) {
+			return Response.json(
+				{
+					success: false,
+					code: 403,
+					error: "You cannot log in while using an authorization token",
+				},
+				{ status: 403 },
+			);
+		}
+
+		return Response.json(
+			{
+				success: false,
+				code: 403,
+				error: "Already logged in",
+			},
+			{ status: 403 },
+		);
+	}
+
+	const { username, email, password } = requestBody as {
+		username: string;
+		email: string;
+		password: string;
+	};
+
+	if (!password || (!username && !email)) {
+		return Response.json(
+			{
+				success: false,
+				code: 400,
+				error: "Expected username or email, and password",
+			},
+			{ status: 400 },
+		);
+	}
+
+	const errors: string[] = [];
+	const validations: UserValidation[] = [
+		{ check: isValidUsername(username), field: "Username" },
+		{ check: isValidEmail(email), field: "Email" },
+		{ check: isValidPassword(password), field: "Password" },
+	];
+
+	validations.forEach(({ check }: UserValidation): void => {
+		if (!check.valid && check.error) {
+			errors.push(check.error);
+		}
+	});
+
+	if (errors.length > 0) {
+		return Response.json(
+			{
+				success: false,
+				code: 400,
+				errors,
+			},
+			{ status: 400 },
+		);
+	}
+
+	const reservation: ReservedSQL = await sql.reserve();
+	let user: User | null = null;
+
+	try {
+		user = await reservation`
+				SELECT * FROM users
+				WHERE (username = ${username} OR email = ${email})
+				LIMIT 1;
+			`.then((rows: User[]): User | null => rows[0]);
+
+		if (!user) {
+			await bunPassword.verify("fake", await bunPassword.hash("fake"));
+
+			return Response.json(
+				{
+					success: false,
+					code: 401,
+					error: "Invalid username, email, or password",
+				},
+				{ status: 401 },
+			);
+		}
+
+		const passwordMatch: boolean = await bunPassword.verify(
+			password,
+			user.password,
+		);
+
+		if (!passwordMatch) {
+			return Response.json(
+				{
+					success: false,
+					code: 401,
+					error: "Invalid username, email, or password",
+				},
+				{ status: 401 },
+			);
+		}
+	} catch (error) {
+		logger.error(["Error logging in", error as Error]);
+
+		return Response.json(
+			{
+				success: false,
+				code: 500,
+				error: "An error occurred while logging in",
+			},
+			{ status: 500 },
+		);
+	} finally {
+		if (reservation) reservation.release();
+	}
+
+	const sessionCookie: string = await sessionManager.createSession(
+		{
+			id: user.id,
+			username: user.username,
+			email: user.email,
+			email_verified: user.email_verified,
+			roles: user.roles,
+			avatar: user.avatar,
+			timezone: user.timezone,
+			authorization_token: user.authorization_token,
+		},
+		request.headers.get("User-Agent") || "",
+	);
+
+	return Response.json(
+		{
+			success: true,
+			code: 200,
+		},
+		{ status: 200, headers: { "Set-Cookie": sessionCookie } },
+	);
+}
+
+export { handler, routeDef };
diff --git a/src/routes/api/auth/logout.ts b/src/routes/api/auth/logout.ts
new file mode 100644
index 0000000..4ef42a1
--- /dev/null
+++ b/src/routes/api/auth/logout.ts
@@ -0,0 +1,50 @@
+import { sessionManager } from "@/helpers/sessions";
+
+const routeDef: RouteDef = {
+	method: "POST",
+	accepts: "*/*",
+	returns: "application/json",
+};
+
+async function handler(request: ExtendedRequest): Promise<Response> {
+	if (!request.session) {
+		return Response.json(
+			{
+				success: false,
+				code: 403,
+				error: "You are not logged in",
+			},
+			{ status: 403 },
+		);
+	}
+
+	if ((request.session as ApiUserSession).is_api) {
+		return Response.json(
+			{
+				success: false,
+				code: 403,
+				error: "You cannot logout while using an authorization token",
+			},
+			{ status: 403 },
+		);
+	}
+
+	sessionManager.invalidateSession(request);
+
+	return Response.json(
+		{
+			success: true,
+			code: 200,
+			message: "Successfully logged out",
+		},
+		{
+			status: 200,
+			headers: {
+				"Set-Cookie":
+					"session=; Path=/; Max-Age=0; HttpOnly; Secure; SameSite=Strict",
+			},
+		},
+	);
+}
+
+export { handler, routeDef };
diff --git a/src/routes/api/auth/register.ts b/src/routes/api/auth/register.ts
new file mode 100644
index 0000000..de6e16b
--- /dev/null
+++ b/src/routes/api/auth/register.ts
@@ -0,0 +1,197 @@
+import { getSetting } from "@config/sql/settings";
+import {
+	isValidEmail,
+	isValidInvite,
+	isValidPassword,
+	isValidUsername,
+} from "@config/sql/users";
+import { password as bunPassword, type ReservedSQL, sql } from "bun";
+import type { UUID } from "crypto";
+
+import { logger } from "@/helpers/logger";
+import { sessionManager } from "@/helpers/sessions";
+
+const routeDef: RouteDef = {
+	method: "POST",
+	accepts: "application/json",
+	returns: "application/json",
+	needsBody: "json",
+};
+
+async function handler(
+	request: ExtendedRequest,
+	requestBody: unknown,
+): Promise<Response> {
+	const { username, email, password, invite } = requestBody as {
+		username: string;
+		email: string;
+		password: string;
+		invite?: string;
+	};
+
+	if (!username || !email || !password) {
+		return Response.json(
+			{
+				success: false,
+				code: 400,
+				error: "Expected username, email, and password",
+			},
+			{ status: 400 },
+		);
+	}
+
+	const errors: string[] = [];
+
+	const validations: UserValidation[] = [
+		{ check: isValidUsername(username), field: "Username" },
+		{ check: isValidEmail(email), field: "Email" },
+		{ check: isValidPassword(password), field: "Password" },
+	];
+
+	validations.forEach(({ check }: UserValidation): void => {
+		if (!check.valid && check.error) {
+			errors.push(check.error);
+		}
+	});
+
+	const reservation: ReservedSQL = await sql.reserve();
+	let firstUser: boolean = false;
+	let invitedBy: UUID | null = null;
+	let roles: string[] = [];
+
+	try {
+		const registrationEnabled: boolean =
+			(await getSetting("registrationEnabled", reservation)) === "true";
+		const invitationsEnabled: boolean =
+			(await getSetting("invitationsEnabled", reservation)) === "true";
+
+		firstUser =
+			Number(
+				(await reservation`SELECT COUNT(*) AS count FROM users;`)[0]
+					?.count,
+			) === 0;
+
+		if (!firstUser && invite) {
+			const inviteValidation: { valid: boolean; error?: string } =
+				isValidInvite(invite);
+			if (!inviteValidation.valid && inviteValidation.error) {
+				errors.push(inviteValidation.error);
+			}
+		}
+
+		if (
+			(!firstUser && !registrationEnabled && !invite) ||
+			(!firstUser && invite && !invitationsEnabled)
+		) {
+			errors.push("Registration is disabled");
+		}
+
+		roles.push("user");
+		if (firstUser) {
+			roles.push("admin");
+		}
+
+		const { usernameExists, emailExists } = await reservation`
+			SELECT
+				EXISTS(SELECT 1 FROM users WHERE LOWER(username) = LOWER(${username})) AS usernameExists,
+				EXISTS(SELECT 1 FROM users WHERE LOWER(email) = LOWER(${email})) AS emailExists;
+		`;
+
+		if (usernameExists) errors.push("Username already exists");
+		if (emailExists) errors.push("Email already exists");
+		if (invite) {
+			invitedBy = (
+				await reservation`SELECT user_id FROM invites WHERE invite = ${invite};`
+			)[0]?.id;
+			if (!invitedBy) errors.push("Invalid invite code");
+		}
+	} catch (error) {
+		errors.push("An error occurred while checking for existing users");
+		logger.error(["Error checking for existing users:", error as Error]);
+	}
+
+	if (errors.length > 0) {
+		return Response.json(
+			{
+				success: false,
+				code: 400,
+				errors,
+			},
+			{ status: 400 },
+		);
+	}
+
+	let user: User | null = null;
+	const hashedPassword: string = await bunPassword.hash(password, {
+		algorithm: "argon2id",
+	});
+	const defaultTimezone: string =
+		(await getSetting("default_timezone", reservation)) || "UTC";
+
+	try {
+		user = (
+			await reservation`
+				INSERT INTO users (username, email, password, invited_by, roles, timezone)
+				VALUES (${username}, ${email}, ${hashedPassword}, ${invitedBy}, ARRAY[${roles.join(",")}]::TEXT[], ${defaultTimezone})
+				RETURNING *;
+			`
+		)[0];
+
+		if (!user) {
+			logger.error("User was not created");
+			return Response.json(
+				{
+					success: false,
+					code: 500,
+					error: "An error occurred with the user registration",
+				},
+				{ status: 500 },
+			);
+		}
+
+		if (invitedBy) {
+			await reservation`DELETE FROM invites WHERE invite = ${invite};`;
+		}
+	} catch (error) {
+		logger.error([
+			"Error inserting user into the database:",
+			error as Error,
+		]);
+		return Response.json(
+			{
+				success: false,
+				code: 500,
+				error: "An error occurred while creating the user",
+			},
+			{ status: 500 },
+		);
+	} finally {
+		reservation.release();
+	}
+
+	const sessionCookie: string = await sessionManager.createSession(
+		{
+			id: user.id,
+			username: user.username,
+			email: user.email,
+			email_verified: user.email_verified,
+			roles: user.roles,
+			avatar: user.avatar,
+			timezone: user.timezone,
+			authorization_token: user.authorization_token,
+		},
+		request.headers.get("User-Agent") || "",
+	);
+
+	return Response.json(
+		{
+			success: true,
+			code: 201,
+			message: "User Registered",
+			id: user.id,
+		},
+		{ status: 201, headers: { "Set-Cookie": sessionCookie } },
+	);
+}
+
+export { handler, routeDef };
diff --git a/src/routes/api/index.ts b/src/routes/api/index.ts
new file mode 100644
index 0000000..9461be4
--- /dev/null
+++ b/src/routes/api/index.ts
@@ -0,0 +1,15 @@
+const routeDef: RouteDef = {
+	method: "GET",
+	accepts: "*/*",
+	returns: "application/json",
+};
+
+async function handler(): Promise<Response> {
+	// TODO: Put something useful here
+
+	return Response.json({
+		message: "Hello, World!",
+	});
+}
+
+export { handler, routeDef };
diff --git a/src/routes/index.ts b/src/routes/index.ts
new file mode 100644
index 0000000..9204e19
--- /dev/null
+++ b/src/routes/index.ts
@@ -0,0 +1,17 @@
+import { renderEjsTemplate } from "@helpers/ejs";
+
+const routeDef: RouteDef = {
+	method: "GET",
+	accepts: "*/*",
+	returns: "text/html",
+};
+
+async function handler(): Promise<Response> {
+	const ejsTemplateData: EjsTemplateData = {
+		title: "Hello, World!",
+	};
+
+	return await renderEjsTemplate("index", ejsTemplateData);
+}
+
+export { handler, routeDef };
diff --git a/src/server.ts b/src/server.ts
new file mode 100644
index 0000000..6506490
--- /dev/null
+++ b/src/server.ts
@@ -0,0 +1,247 @@
+import { environment } from "@config/environment";
+import { logger } from "@helpers/logger";
+import {
+	type BunFile,
+	FileSystemRouter,
+	type MatchedRoute,
+	type Serve,
+} from "bun";
+import { resolve } from "path";
+
+import { webSocketHandler } from "@/websocket";
+
+import { authByToken } from "./helpers/auth";
+import { sessionManager } from "./helpers/sessions";
+
+class ServerHandler {
+	private router: FileSystemRouter;
+
+	constructor(
+		private port: number,
+		private host: string,
+	) {
+		this.router = new FileSystemRouter({
+			style: "nextjs",
+			dir: "./src/routes",
+			origin: `http://${this.host}:${this.port}`,
+		});
+	}
+
+	public initialize(): void {
+		const server: Serve = Bun.serve({
+			port: this.port,
+			hostname: this.host,
+			fetch: this.handleRequest.bind(this),
+			websocket: {
+				open: webSocketHandler.handleOpen.bind(webSocketHandler),
+				message: webSocketHandler.handleMessage.bind(webSocketHandler),
+				close: webSocketHandler.handleClose.bind(webSocketHandler),
+			},
+		});
+
+		logger.info(
+			`Server running at http://${server.hostname}:${server.port}`,
+			true,
+		);
+
+		this.logRoutes();
+	}
+
+	private logRoutes(): void {
+		logger.info("Available routes:");
+
+		const sortedRoutes: [string, string][] = Object.entries(
+			this.router.routes,
+		).sort(([pathA]: [string, string], [pathB]: [string, string]) =>
+			pathA.localeCompare(pathB),
+		);
+
+		for (const [path, filePath] of sortedRoutes) {
+			logger.info(`Route: ${path}, File: ${filePath}`);
+		}
+	}
+
+	private async serveStaticFile(pathname: string): Promise<Response> {
+		try {
+			let filePath: string;
+
+			if (pathname === "/favicon.ico") {
+				filePath = resolve("public", "assets", "favicon.ico");
+			} else {
+				filePath = resolve(`.${pathname}`);
+			}
+
+			const file: BunFile = Bun.file(filePath);
+
+			if (await file.exists()) {
+				const fileContent: ArrayBuffer = await file.arrayBuffer();
+				const contentType: string =
+					file.type || "application/octet-stream";
+
+				return new Response(fileContent, {
+					headers: { "Content-Type": contentType },
+				});
+			} else {
+				logger.warn(`File not found: ${filePath}`);
+				return new Response("Not Found", { status: 404 });
+			}
+		} catch (error) {
+			logger.error([
+				`Error serving static file: ${pathname}`,
+				error as Error,
+			]);
+			return new Response("Internal Server Error", { status: 500 });
+		}
+	}
+
+	private async handleRequest(
+		request: ExtendedRequest,
+		server: BunServer,
+	): Promise<Response> {
+		request.startPerf = performance.now();
+
+		const pathname: string = new URL(request.url).pathname;
+		if (pathname.startsWith("/public") || pathname === "/favicon.ico") {
+			return await this.serveStaticFile(pathname);
+		}
+
+		const match: MatchedRoute | null = this.router.match(request);
+		let requestBody: unknown = {};
+		let response: Response;
+
+		if (match) {
+			const { filePath, params, query } = match;
+
+			try {
+				const routeModule: RouteModule = await import(filePath);
+				const contentType: string | null =
+					request.headers.get("Content-Type");
+				const actualContentType: string | null = contentType
+					? contentType.split(";")[0].trim()
+					: null;
+
+				if (
+					routeModule.routeDef.needsBody === "json" &&
+					actualContentType === "application/json"
+				) {
+					try {
+						requestBody = await request.json();
+					} catch {
+						requestBody = {};
+					}
+				} else if (
+					routeModule.routeDef.needsBody === "multipart" &&
+					actualContentType === "multipart/form-data"
+				) {
+					try {
+						requestBody = await request.formData();
+					} catch {
+						requestBody = {};
+					}
+				}
+
+				if (routeModule.routeDef.method !== request.method) {
+					response = Response.json(
+						{
+							success: false,
+							code: 405,
+							error: `Method ${request.method} Not Allowed, expected ${routeModule.routeDef.method}`,
+						},
+						{ status: 405 },
+					);
+				} else {
+					const expectedContentType: string | null =
+						routeModule.routeDef.accepts;
+
+					const matchesAccepts: boolean =
+						expectedContentType === "*/*" ||
+						actualContentType === expectedContentType;
+
+					if (!matchesAccepts) {
+						response = Response.json(
+							{
+								success: false,
+								code: 406,
+								error: `Content-Type ${contentType} Not Acceptable, expected ${expectedContentType}`,
+							},
+							{ status: 406 },
+						);
+					} else {
+						request.params = params;
+						request.query = query;
+
+						request.session =
+							(await sessionManager.getSession(request)) ||
+							(await authByToken(request));
+
+						response = await routeModule.handler(
+							request,
+							requestBody,
+							server,
+						);
+
+						if (routeModule.routeDef.returns !== "*/*") {
+							response.headers.set(
+								"Content-Type",
+								routeModule.routeDef.returns,
+							);
+						}
+					}
+				}
+			} catch (error: unknown) {
+				logger.error([
+					`Error handling route ${request.url}:`,
+					error as Error,
+				]);
+
+				response = Response.json(
+					{
+						success: false,
+						code: 500,
+						error: "Internal Server Error",
+					},
+					{ status: 500 },
+				);
+			}
+		} else {
+			response = Response.json(
+				{
+					success: false,
+					code: 404,
+					error: "Not Found",
+				},
+				{ status: 404 },
+			);
+		}
+
+		const headers: Headers = response.headers;
+		let ip: string | null = server.requestIP(request)?.address || null;
+
+		if (!ip) {
+			ip =
+				headers.get("CF-Connecting-IP") ||
+				headers.get("X-Real-IP") ||
+				headers.get("X-Forwarded-For") ||
+				null;
+		}
+
+		logger.custom(
+			`[${request.method}]`,
+			`(${response.status})`,
+			[
+				request.url,
+				`${(performance.now() - request.startPerf).toFixed(2)}ms`,
+				ip || "unknown",
+			],
+			"90",
+		);
+
+		return response;
+	}
+}
+const serverHandler: ServerHandler = new ServerHandler(
+	environment.port,
+	environment.host,
+);
+
+export { serverHandler };
diff --git a/src/views/index.ejs b/src/views/index.ejs
new file mode 100644
index 0000000..e8f6e0b
--- /dev/null
+++ b/src/views/index.ejs
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+</head>
+<body>
+	<h1> hello </h1>
+</body>
+</html>
diff --git a/src/websocket.ts b/src/websocket.ts
new file mode 100644
index 0000000..ce87fe8
--- /dev/null
+++ b/src/websocket.ts
@@ -0,0 +1,34 @@
+import { logger } from "@helpers/logger";
+import { type ServerWebSocket } from "bun";
+
+class WebSocketHandler {
+	public handleMessage(ws: ServerWebSocket, message: string): void {
+		logger.info(`WebSocket received: ${message}`);
+		try {
+			ws.send(`You said: ${message}`);
+		} catch (error) {
+			logger.error(["WebSocket send error", error as Error]);
+		}
+	}
+
+	public handleOpen(ws: ServerWebSocket): void {
+		logger.info("WebSocket connection opened.");
+		try {
+			ws.send("Welcome to the WebSocket server!");
+		} catch (error) {
+			logger.error(["WebSocket send error", error as Error]);
+		}
+	}
+
+	public handleClose(
+		ws: ServerWebSocket,
+		code: number,
+		reason: string,
+	): void {
+		logger.warn(`WebSocket closed with code ${code}, reason: ${reason}`);
+	}
+}
+
+const webSocketHandler: WebSocketHandler = new WebSocketHandler();
+
+export { webSocketHandler };
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..ac5f2c7
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,51 @@
+{
+	"compilerOptions": {
+		"baseUrl": "./",
+		"paths": {
+			"@/*": [
+				"src/*"
+			],
+			"@config/*": [
+				"config/*"
+			],
+			"@types/*": [
+				"types/*"
+			],
+			"@helpers/*": [
+				"src/helpers/*"
+			]
+		},
+		"typeRoots": [
+			"./src/types",
+			"./node_modules/@types"
+		],
+		// Enable latest features
+		"lib": [
+			"ESNext",
+			"DOM"
+		],
+		"target": "ESNext",
+		"module": "ESNext",
+		"moduleDetection": "force",
+		"jsx": "react-jsx",
+		"allowJs": true,
+		// Bundler mode
+		"moduleResolution": "bundler",
+		"allowImportingTsExtensions": true,
+		"verbatimModuleSyntax": true,
+		"noEmit": true,
+		// Best practices
+		"strict": true,
+		"skipLibCheck": true,
+		"noFallthroughCasesInSwitch": true,
+		// Some stricter flags (disabled by default)
+		"noUnusedLocals": false,
+		"noUnusedParameters": false,
+		"noPropertyAccessFromIndexSignature": false,
+	},
+	"include": [
+		"src",
+		"types",
+		"config"
+	],
+}
diff --git a/types/bun.d.ts b/types/bun.d.ts
new file mode 100644
index 0000000..414c40f
--- /dev/null
+++ b/types/bun.d.ts
@@ -0,0 +1,15 @@
+import type { Server } from "bun";
+
+type Query = Record<string, string>;
+type Params = Record<string, string>;
+
+declare global {
+	type BunServer = Server;
+
+	type ExtendedRequest = Request & {
+		startPerf: number;
+		query: Query;
+		params: Params;
+		session: UserSession | ApiUserSession | null;
+	};
+}
diff --git a/types/config.d.ts b/types/config.d.ts
new file mode 100644
index 0000000..86a49f6
--- /dev/null
+++ b/types/config.d.ts
@@ -0,0 +1,10 @@
+type Environment = {
+	port: number;
+	host: string;
+	development: boolean;
+};
+
+type UserValidation = {
+	check: { valid: boolean; error?: string };
+	field: string;
+};
diff --git a/types/ejs.d.ts b/types/ejs.d.ts
new file mode 100644
index 0000000..486a4a4
--- /dev/null
+++ b/types/ejs.d.ts
@@ -0,0 +1,3 @@
+interface EjsTemplateData {
+	[key: string]: string | number | boolean | object | undefined | null;
+}
diff --git a/types/logger.d.ts b/types/logger.d.ts
new file mode 100644
index 0000000..ff6a601
--- /dev/null
+++ b/types/logger.d.ts
@@ -0,0 +1,9 @@
+type ILogMessagePart = { value: string; color: string };
+
+type ILogMessageParts = {
+	level: ILogMessagePart;
+	filename: ILogMessagePart;
+	readableTimestamp: ILogMessagePart;
+	message: ILogMessagePart;
+	[key: string]: ILogMessagePart;
+};
diff --git a/types/routes.d.ts b/types/routes.d.ts
new file mode 100644
index 0000000..eb67a3c
--- /dev/null
+++ b/types/routes.d.ts
@@ -0,0 +1,15 @@
+type RouteDef = {
+	method: string;
+	accepts: string | null;
+	returns: string;
+	needsBody?: "multipart" | "json";
+};
+
+type RouteModule = {
+	handler: (
+		request: Request,
+		requestBody: unknown,
+		server: BunServer,
+	) => Promise<Response> | Response;
+	routeDef: RouteDef;
+};
diff --git a/types/session.d.ts b/types/session.d.ts
new file mode 100644
index 0000000..9f5d7b7
--- /dev/null
+++ b/types/session.d.ts
@@ -0,0 +1,29 @@
+type UserSession = {
+	id: string;
+	username: string;
+	email: string;
+	email_verified: boolean;
+	roles: string[];
+	avatar: boolean;
+	timezone: string;
+	authorization_token: string;
+};
+
+type ApiUserSession = UserSession & {
+	is_api: boolean;
+};
+
+type User = {
+	id: UUID;
+	authorization_token: UUID;
+	username: string;
+	email: string;
+	email_verified: boolean;
+	password: string;
+	avatar: boolean;
+	roles: string[];
+	timezone: string;
+	invited_by: UUID;
+	created_at: Date;
+	last_seen: Date;
+};