diff --git a/config/setup/index.ts b/config/setup/index.ts
index 785c933..b5d3019 100644
--- a/config/setup/index.ts
+++ b/config/setup/index.ts
@@ -64,14 +64,14 @@ async function setup(): Promise<void> {
 }
 
 setup()
-	.catch((error: Error) => {
+	.catch((error) => {
 		logger.error(error);
 		process.exit(1);
 	})
-	.finally(() => {
-		cassandra.shutdown().catch((error: Error) => {
-			logger.error(["Error shutting down Cassandra client:", error]);
-		});
-
-		process.exit(0);
+	.finally(async () => {
+		try {
+			await cassandra.shutdown();
+		} catch (error) {
+			logger.error(["Error shutting down Cassandra client:", error as Error]);
+		}
 	});
diff --git a/config/setup/tables/guilds/index.ts b/config/setup/tables/guilds/index.ts
index f7ea061..2d1cba8 100644
--- a/config/setup/tables/guilds/index.ts
+++ b/config/setup/tables/guilds/index.ts
@@ -13,7 +13,7 @@ async function createTable() {
 
 			system_channel_id TEXT,
 			rules_channel_id TEXT,
-			anouncements_channel_id TEXT,
+			announcements_channel_id TEXT,
 
 			created_at TIMESTAMP,
 			updated_at TIMESTAMP
@@ -31,4 +31,16 @@ async function createTable() {
 	`);
 }
 
-export { createTable };
+async function dropTable() {
+	const client = cassandra.getClient();
+
+	await client.execute(`
+		DROP TABLE IF EXISTS guilds;
+	`);
+
+	await client.execute(`
+		DROP TABLE IF EXISTS guilds_by_owner;
+	`);
+}
+
+export { createTable, dropTable };
diff --git a/config/setup/tables/guilds/invites.ts b/config/setup/tables/guilds/invites.ts
index 67a5c32..7477f4d 100644
--- a/config/setup/tables/guilds/invites.ts
+++ b/config/setup/tables/guilds/invites.ts
@@ -17,4 +17,12 @@ async function createTable() {
 	`);
 }
 
-export { createTable };
+async function dropTable() {
+	const client = cassandra.getClient();
+
+	await client.execute(`
+		DROP TABLE IF EXISTS guild_invites;
+	`);
+}
+
+export { createTable, dropTable };
diff --git a/config/setup/tables/guilds/members.ts b/config/setup/tables/guilds/members.ts
index 694aba2..90318dd 100644
--- a/config/setup/tables/guilds/members.ts
+++ b/config/setup/tables/guilds/members.ts
@@ -29,4 +29,16 @@ async function createTable() {
 	`);
 }
 
-export { createTable };
+async function dropTable() {
+	const client = cassandra.getClient();
+
+	await client.execute(`
+		DROP TABLE IF EXISTS guild_members;
+	`);
+
+	await client.execute(`
+		DROP TABLE IF EXISTS members_by_user;
+	`);
+}
+
+export { createTable, dropTable };
diff --git a/config/setup/tables/users.ts b/config/setup/tables/users.ts
index e7dbb95..838228a 100644
--- a/config/setup/tables/users.ts
+++ b/config/setup/tables/users.ts
@@ -24,4 +24,18 @@ async function createTable() {
 	`);
 }
 
-export { createTable };
+async function dropTable() {
+	await cassandra.getClient().execute(`
+		DROP TABLE IF EXISTS users;
+	`);
+
+	await cassandra.getClient().execute(`
+		DROP INDEX IF EXISTS users_username_idx;
+	`);
+
+	await cassandra.getClient().execute(`
+		DROP INDEX IF EXISTS users_email_idx;
+	`);
+}
+
+export { createTable, dropTable };
diff --git a/config/setup/teardown.ts b/config/setup/teardown.ts
new file mode 100644
index 0000000..96406ee
--- /dev/null
+++ b/config/setup/teardown.ts
@@ -0,0 +1,64 @@
+import { readdir, stat } from "node:fs/promises";
+import { extname, join, resolve } from "node:path";
+import { pathToFileURL } from "node:url";
+import { cassandra as cassandraConfig, verifyRequiredVariables } from "@config";
+import { logger } from "@creations.works/logger";
+import { cassandra } from "@lib/cassandra";
+
+async function dropTables(dir: string): Promise<void> {
+	const entries = await readdir(dir);
+
+	for (const entry of entries) {
+		const fullPath = join(dir, entry);
+		const stats = await stat(fullPath);
+
+		if (stats.isDirectory()) {
+			await dropTables(fullPath);
+			continue;
+		}
+
+		if (extname(entry) !== ".ts") continue;
+
+		const modulePath = pathToFileURL(fullPath).href;
+		const mod = await import(modulePath);
+
+		if (typeof mod.dropTable === "function") {
+			await mod.dropTable();
+			logger.info(`Ran dropTable from ${fullPath}`);
+		} else {
+			logger.warn(`No dropTable export found in ${fullPath}`);
+		}
+	}
+}
+
+async function teardown(): Promise<void> {
+	verifyRequiredVariables();
+	await cassandra.connect({ withKeyspace: true });
+
+	const keyspace = cassandraConfig.keyspace;
+
+	if (!keyspace) {
+		logger.error("No Cassandra keyspace configured in environment.");
+		process.exit(1);
+	}
+
+	logger.info(`Dropping all tables in keyspace "${keyspace}"...`);
+
+	const tablesDir = resolve("config", "setup", "tables");
+	await dropTables(tablesDir);
+
+	logger.info("Teardown complete.");
+}
+
+teardown()
+	.catch((error) => {
+		logger.error(error);
+		process.exit(1);
+	})
+	.finally(async () => {
+		try {
+			await cassandra.shutdown();
+		} catch (error) {
+			logger.error(["Error shutting down Cassandra client:", error as Error]);
+		}
+	});
diff --git a/package.json b/package.json
index 215861a..8bf88ec 100644
--- a/package.json
+++ b/package.json
@@ -8,7 +8,8 @@
 		"lint": "bunx biome check",
 		"lint:fix": "bunx biome check --fix",
 		"cleanup": "rm -rf logs node_modules bun.lockdb",
-		"setup": "bun run config/setup/index.ts"
+		"setup": "bun run config/setup/index.ts",
+		"teardown": "bun run config/setup/teardown.ts"
 	},
 	"devDependencies": {
 		"@types/bun": "^1.2.11",
diff --git a/src/routes/guild/join[id].ts b/src/routes/guild/join[invite].ts
similarity index 94%
rename from src/routes/guild/join[id].ts
rename to src/routes/guild/join[invite].ts
index b441b76..e2ce865 100644
--- a/src/routes/guild/join[id].ts
+++ b/src/routes/guild/join[invite].ts
@@ -18,7 +18,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
 		});
 	}
 
-	const { id: invite } = request.params;
+	const { invite } = request.params;
 
 	if (!invite) {
 		return jsonResponse(400, {
@@ -88,7 +88,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
 			`INSERT INTO guild_members (
 				guild_id, user_id, roles, joined_at, is_banned, invite_id
 			) VALUES (?, ?, ?, ?, ?, ?)`,
-			[guildId, user.id, ["member"], now, false, invite],
+			[guildId, user.id, [], now, false, invite],
 			{ prepare: true },
 		);
 
@@ -96,7 +96,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
 			`INSERT INTO members_by_user (
 				user_id, guild_id, roles, joined_at, is_banned, invite_id
 			) VALUES (?, ?, ?, ?, ?, ?)`,
-			[user.id, guildId, ["member"], now, false, invite],
+			[user.id, guildId, [], now, false, invite],
 			{ prepare: true },
 		);
 
@@ -117,7 +117,6 @@ async function handler(request: ExtendedRequest): Promise<Response> {
 			message: "Joined guild successfully",
 			data: {
 				guild_id: guildId,
-				role: "member",
 			},
 		});
 	} catch (error) {
diff --git a/src/routes/user/[id].ts b/src/routes/user/[id].ts
new file mode 100644
index 0000000..1407ef7
--- /dev/null
+++ b/src/routes/user/[id].ts
@@ -0,0 +1,67 @@
+import { cassandra } from "@lib/cassandra";
+import { jsonResponse } from "@lib/http";
+
+function toSafeUser(user: User, sessionId?: string): UserSafe | UserPrivate {
+	const base: UserSafe = {
+		id: user.id,
+		username: user.username,
+		display_name: user.display_name,
+		avatar_url: user.avatar_url,
+		is_verified: user.is_verified,
+		created_at: user.created_at,
+		updated_at: user.updated_at,
+	};
+
+	return sessionId === user.id ? { ...base, email: user.email } : base;
+}
+
+const routeDef: RouteDef = {
+	method: "GET",
+	accepts: "*/*",
+	returns: "application/json;charset=utf-8",
+};
+
+async function handler(request: ExtendedRequest): Promise<Response> {
+	const { id: userId } = request.params;
+
+	if (!userId) {
+		return jsonResponse(400, { error: "Missing user ID in request." });
+	}
+
+	try {
+		const client = cassandra.getClient();
+		const query = `
+			SELECT id, username, display_name, email, avatar_url, is_verified, created_at, updated_at
+			FROM users
+			WHERE id = ?
+		`;
+		const result = await client.execute(query, [userId], { prepare: true });
+
+		if (result.rowLength === 0) {
+			return jsonResponse(404, {
+				error: "User not found.",
+				message: "No user exists with the given ID.",
+			});
+		}
+
+		const row = result.first();
+		const user: User = {
+			id: row.id,
+			username: row.username,
+			display_name: row.display_name,
+			email: row.email,
+			password: "", // this is never actually used or returned
+			avatar_url: row.avatar_url,
+			is_verified: row.is_verified,
+			created_at: row.created_at,
+			updated_at: row.updated_at,
+		};
+
+		return jsonResponse(200, { user: toSafeUser(user, request.session?.id) });
+	} catch (err) {
+		console.error(err);
+		return jsonResponse(500, { error: "Internal server error." });
+	}
+}
+
+export { handler, routeDef };
diff --git a/src/routes/user/logout.ts b/src/routes/user/logout.ts
index cf968b7..09a4bd7 100644
--- a/src/routes/user/logout.ts
+++ b/src/routes/user/logout.ts
@@ -17,8 +17,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
 			await redis.del(key);
 		}
 
-		return new Response(null, {
-			status: 204,
+		return jsonResponse(204, {
 			headers: {
 				"Set-Cookie":
 					"session=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Strict",
@@ -43,8 +42,7 @@ async function handler(request: ExtendedRequest): Promise<Response> {
 
 	await sessionManager.invalidateSession(request);
 
-	return new Response(null, {
-		status: 204,
+	return jsonResponse(204, {
 		headers: {
 			"Set-Cookie":
 				"session=; Max-Age=0; Path=/; HttpOnly; Secure; SameSite=Strict",
diff --git a/types/tables/user.d.ts b/types/tables/user.d.ts
index ac4a01b..8b7917f 100644
--- a/types/tables/user.d.ts
+++ b/types/tables/user.d.ts
@@ -14,3 +14,16 @@ type UserInsert = Pick<
 	User,
 	"id" | "username" | "email" | "password" | "created_at" | "updated_at"
 >;
+
+type UserSafe = Pick<
+	User,
+	| "id"
+	| "username"
+	| "display_name"
+	| "avatar_url"
+	| "is_verified"
+	| "created_at"
+	| "updated_at"
+>;
+
+type UserPrivate = Omit<User, "password">;