aboutsummaryrefslogtreecommitdiff
path: root/packages/web/src/server
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-02-03 19:56:19 -0800
committerFuwn <[email protected]>2026-02-03 19:56:37 -0800
commit6be34a75cd060444734080ac14d3a33eea630f15 (patch)
treebeda873457cf5d6f73e97391c902acf8a9b69712 /packages/web/src/server
parentfeat(iku): Support comment-aware whitespace checking (diff)
downloadarchived-imemio-6be34a75cd060444734080ac14d3a33eea630f15.tar.xz
archived-imemio-6be34a75cd060444734080ac14d3a33eea630f15.zip
feat(web): Initialise T3 Stack
Diffstat (limited to 'packages/web/src/server')
-rw-r--r--packages/web/src/server/api/root.ts23
-rw-r--r--packages/web/src/server/api/routers/post.ts38
-rw-r--r--packages/web/src/server/api/trpc.ts134
-rw-r--r--packages/web/src/server/auth/config.ts66
-rw-r--r--packages/web/src/server/auth/index.ts8
-rw-r--r--packages/web/src/server/db/index.ts17
-rw-r--r--packages/web/src/server/db/schema.ts100
7 files changed, 386 insertions, 0 deletions
diff --git a/packages/web/src/server/api/root.ts b/packages/web/src/server/api/root.ts
new file mode 100644
index 0000000..374285c
--- /dev/null
+++ b/packages/web/src/server/api/root.ts
@@ -0,0 +1,23 @@
+import { postRouter } from "~/server/api/routers/post";
+import { createCallerFactory, createTRPCRouter } from "~/server/api/trpc";
+
+/**
+ * This is the primary router for your server.
+ *
+ * All routers added in /api/routers should be manually added here.
+ */
+export const appRouter = createTRPCRouter({
+ post: postRouter,
+});
+
+// export type definition of API
+export type AppRouter = typeof appRouter;
+
+/**
+ * Create a server-side caller for the tRPC API.
+ * @example
+ * const trpc = createCaller(createContext);
+ * const res = await trpc.post.all();
+ * ^? Post[]
+ */
+export const createCaller = createCallerFactory(appRouter);
diff --git a/packages/web/src/server/api/routers/post.ts b/packages/web/src/server/api/routers/post.ts
new file mode 100644
index 0000000..2bd03a4
--- /dev/null
+++ b/packages/web/src/server/api/routers/post.ts
@@ -0,0 +1,38 @@
+import { z } from "zod";
+import {
+ createTRPCRouter,
+ protectedProcedure,
+ publicProcedure,
+} from "~/server/api/trpc";
+import { posts } from "~/server/db/schema";
+
+export const postRouter = createTRPCRouter({
+ hello: publicProcedure
+ .input(z.object({ text: z.string() }))
+ .query(({ input }) => {
+ return {
+ greeting: `Hello ${input.text}`,
+ };
+ }),
+
+ create: protectedProcedure
+ .input(z.object({ name: z.string().min(1) }))
+ .mutation(async ({ ctx, input }) => {
+ await ctx.db.insert(posts).values({
+ name: input.name,
+ createdById: ctx.session.user.id,
+ });
+ }),
+
+ getLatest: protectedProcedure.query(async ({ ctx }) => {
+ const post = await ctx.db.query.posts.findFirst({
+ orderBy: (posts, { desc }) => [desc(posts.createdAt)],
+ });
+
+ return post ?? null;
+ }),
+
+ getSecretMessage: protectedProcedure.query(() => {
+ return "you can now see this secret message!";
+ }),
+});
diff --git a/packages/web/src/server/api/trpc.ts b/packages/web/src/server/api/trpc.ts
new file mode 100644
index 0000000..65c86f4
--- /dev/null
+++ b/packages/web/src/server/api/trpc.ts
@@ -0,0 +1,134 @@
+/**
+ * YOU PROBABLY DON'T NEED TO EDIT THIS FILE, UNLESS:
+ * 1. You want to modify request context (see Part 1).
+ * 2. You want to create a new middleware or type of procedure (see Part 3).
+ *
+ * TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
+ * need to use are documented accordingly near the end.
+ */
+
+import { initTRPC, TRPCError } from "@trpc/server";
+import superjson from "superjson";
+import { ZodError } from "zod";
+import { auth } from "~/server/auth";
+import { db } from "~/server/db";
+
+/**
+ * 1. CONTEXT
+ *
+ * This section defines the "contexts" that are available in the backend API.
+ *
+ * These allow you to access things when processing a request, like the database, the session, etc.
+ *
+ * This helper generates the "internals" for a tRPC context. The API handler and RSC clients each
+ * wrap this and provides the required context.
+ *
+ * @see https://trpc.io/docs/server/context
+ */
+export const createTRPCContext = async (opts: { headers: Headers }) => {
+ const session = await auth();
+
+ return {
+ db,
+ session,
+ ...opts,
+ };
+};
+
+/**
+ * 2. INITIALIZATION
+ *
+ * This is where the tRPC API is initialized, connecting the context and transformer. We also parse
+ * ZodErrors so that you get typesafety on the frontend if your procedure fails due to validation
+ * errors on the backend.
+ */
+const t = initTRPC.context<typeof createTRPCContext>().create({
+ transformer: superjson,
+ errorFormatter({ shape, error }) {
+ return {
+ ...shape,
+ data: {
+ ...shape.data,
+ zodError:
+ error.cause instanceof ZodError ? error.cause.flatten() : null,
+ },
+ };
+ },
+});
+
+/**
+ * Create a server-side caller.
+ *
+ * @see https://trpc.io/docs/server/server-side-calls
+ */
+export const createCallerFactory = t.createCallerFactory;
+
+/**
+ * 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
+ *
+ * These are the pieces you use to build your tRPC API. You should import these a lot in the
+ * "/src/server/api/routers" directory.
+ */
+
+/**
+ * This is how you create new routers and sub-routers in your tRPC API.
+ *
+ * @see https://trpc.io/docs/router
+ */
+export const createTRPCRouter = t.router;
+
+/**
+ * Middleware for timing procedure execution and adding an artificial delay in development.
+ *
+ * You can remove this if you don't like it, but it can help catch unwanted waterfalls by simulating
+ * network latency that would occur in production but not in local development.
+ */
+const timingMiddleware = t.middleware(async ({ next, path }) => {
+ const start = Date.now();
+
+ if (t._config.isDev) {
+ // artificial delay in dev
+ const waitMs = Math.floor(Math.random() * 400) + 100;
+
+ await new Promise((resolve) => setTimeout(resolve, waitMs));
+ }
+
+ const result = await next();
+ const end = Date.now();
+
+ console.log(`[TRPC] ${path} took ${end - start}ms to execute`);
+
+ return result;
+});
+
+/**
+ * Public (unauthenticated) procedure
+ *
+ * This is the base piece you use to build new queries and mutations on your tRPC API. It does not
+ * guarantee that a user querying is authorized, but you can still access user session data if they
+ * are logged in.
+ */
+export const publicProcedure = t.procedure.use(timingMiddleware);
+
+/**
+ * Protected (authenticated) procedure
+ *
+ * If you want a query or mutation to ONLY be accessible to logged in users, use this. It verifies
+ * the session is valid and guarantees `ctx.session.user` is not null.
+ *
+ * @see https://trpc.io/docs/procedures
+ */
+export const protectedProcedure = t.procedure
+ .use(timingMiddleware)
+ .use(({ ctx, next }) => {
+ if (!ctx.session?.user) {
+ throw new TRPCError({ code: "UNAUTHORIZED" });
+ }
+
+ return next({
+ ctx: {
+ // infers the `session` as non-nullable
+ session: { ...ctx.session, user: ctx.session.user },
+ },
+ });
+ });
diff --git a/packages/web/src/server/auth/config.ts b/packages/web/src/server/auth/config.ts
new file mode 100644
index 0000000..b3307cc
--- /dev/null
+++ b/packages/web/src/server/auth/config.ts
@@ -0,0 +1,66 @@
+import { DrizzleAdapter } from "@auth/drizzle-adapter";
+import type { DefaultSession, NextAuthConfig } from "next-auth";
+import DiscordProvider from "next-auth/providers/discord";
+import { db } from "~/server/db";
+import {
+ accounts,
+ sessions,
+ users,
+ verificationTokens,
+} from "~/server/db/schema";
+
+/**
+ * Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
+ * object and keep type safety.
+ *
+ * @see https://next-auth.js.org/getting-started/typescript#module-augmentation
+ */
+declare module "next-auth" {
+ interface Session extends DefaultSession {
+ user: {
+ id: string;
+ // ...other properties
+ // role: UserRole;
+ } & DefaultSession["user"];
+ }
+
+ // interface User {
+ // // ...other properties
+ // // role: UserRole;
+ // }
+}
+
+/**
+ * Options for NextAuth.js used to configure adapters, providers, callbacks, etc.
+ *
+ * @see https://next-auth.js.org/configuration/options
+ */
+export const authConfig = {
+ providers: [
+ DiscordProvider,
+ /**
+ * ...add more providers here.
+ *
+ * Most other providers require a bit more work than the Discord provider. For example, the
+ * GitHub provider requires you to add the `refresh_token_expires_in` field to the Account
+ * model. Refer to the NextAuth.js docs for the provider you want to use. Example:
+ *
+ * @see https://next-auth.js.org/providers/github
+ */
+ ],
+ adapter: DrizzleAdapter(db, {
+ usersTable: users,
+ accountsTable: accounts,
+ sessionsTable: sessions,
+ verificationTokensTable: verificationTokens,
+ }),
+ callbacks: {
+ session: ({ session, user }) => ({
+ ...session,
+ user: {
+ ...session.user,
+ id: user.id,
+ },
+ }),
+ },
+} satisfies NextAuthConfig;
diff --git a/packages/web/src/server/auth/index.ts b/packages/web/src/server/auth/index.ts
new file mode 100644
index 0000000..21f0ee0
--- /dev/null
+++ b/packages/web/src/server/auth/index.ts
@@ -0,0 +1,8 @@
+import NextAuth from "next-auth";
+import { cache } from "react";
+import { authConfig } from "./config";
+
+const { auth: uncachedAuth, handlers, signIn, signOut } = NextAuth(authConfig);
+const auth = cache(uncachedAuth);
+
+export { auth, handlers, signIn, signOut };
diff --git a/packages/web/src/server/db/index.ts b/packages/web/src/server/db/index.ts
new file mode 100644
index 0000000..f6e5086
--- /dev/null
+++ b/packages/web/src/server/db/index.ts
@@ -0,0 +1,17 @@
+import { drizzle } from "drizzle-orm/postgres-js";
+import postgres from "postgres";
+import { env } from "~/env";
+import * as schema from "./schema";
+
+/**
+ * Cache the database connection in development. This avoids creating a new connection on every HMR
+ * update.
+ */
+const globalForDb = globalThis as unknown as {
+ conn: postgres.Sql | undefined;
+};
+const conn = globalForDb.conn ?? postgres(env.DATABASE_URL);
+
+if (env.NODE_ENV !== "production") globalForDb.conn = conn;
+
+export const db = drizzle(conn, { schema });
diff --git a/packages/web/src/server/db/schema.ts b/packages/web/src/server/db/schema.ts
new file mode 100644
index 0000000..08c1551
--- /dev/null
+++ b/packages/web/src/server/db/schema.ts
@@ -0,0 +1,100 @@
+import { relations } from "drizzle-orm";
+import { index, pgTableCreator, primaryKey } from "drizzle-orm/pg-core";
+import type { AdapterAccount } from "next-auth/adapters";
+
+/**
+ * This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
+ * database instance for multiple projects.
+ *
+ * @see https://orm.drizzle.team/docs/goodies#multi-project-schema
+ */
+export const createTable = pgTableCreator((name) => `web_${name}`);
+export const posts = createTable(
+ "post",
+ (d) => ({
+ id: d.integer().primaryKey().generatedByDefaultAsIdentity(),
+ name: d.varchar({ length: 256 }),
+ createdById: d
+ .varchar({ length: 255 })
+ .notNull()
+ .references(() => users.id),
+ createdAt: d
+ .timestamp({ withTimezone: true })
+ .$defaultFn(() => /* @__PURE__ */ new Date())
+ .notNull(),
+ updatedAt: d.timestamp({ withTimezone: true }).$onUpdate(() => new Date()),
+ }),
+ (t) => [
+ index("created_by_idx").on(t.createdById),
+ index("name_idx").on(t.name),
+ ],
+);
+export const users = createTable("user", (d) => ({
+ id: d
+ .varchar({ length: 255 })
+ .notNull()
+ .primaryKey()
+ .$defaultFn(() => crypto.randomUUID()),
+ name: d.varchar({ length: 255 }),
+ email: d.varchar({ length: 255 }).notNull(),
+ emailVerified: d
+ .timestamp({
+ mode: "date",
+ withTimezone: true,
+ })
+ .$defaultFn(() => /* @__PURE__ */ new Date()),
+ image: d.varchar({ length: 255 }),
+}));
+export const usersRelations = relations(users, ({ many }) => ({
+ accounts: many(accounts),
+}));
+export const accounts = createTable(
+ "account",
+ (d) => ({
+ userId: d
+ .varchar({ length: 255 })
+ .notNull()
+ .references(() => users.id),
+ type: d.varchar({ length: 255 }).$type<AdapterAccount["type"]>().notNull(),
+ provider: d.varchar({ length: 255 }).notNull(),
+ providerAccountId: d.varchar({ length: 255 }).notNull(),
+ refresh_token: d.text(),
+ access_token: d.text(),
+ expires_at: d.integer(),
+ token_type: d.varchar({ length: 255 }),
+ scope: d.varchar({ length: 255 }),
+ id_token: d.text(),
+ session_state: d.varchar({ length: 255 }),
+ }),
+ (t) => [
+ primaryKey({ columns: [t.provider, t.providerAccountId] }),
+ index("account_user_id_idx").on(t.userId),
+ ],
+);
+export const accountsRelations = relations(accounts, ({ one }) => ({
+ user: one(users, { fields: [accounts.userId], references: [users.id] }),
+}));
+export const sessions = createTable(
+ "session",
+ (d) => ({
+ sessionToken: d.varchar({ length: 255 }).notNull().primaryKey(),
+ userId: d
+ .varchar({ length: 255 })
+ .notNull()
+ .references(() => users.id),
+ expires: d.timestamp({ mode: "date", withTimezone: true }).notNull(),
+ }),
+ (t) => [index("t_user_id_idx").on(t.userId)],
+);
+export const sessionsRelations = relations(sessions, ({ one }) => ({
+ user: one(users, { fields: [sessions.userId], references: [users.id] }),
+}));
+export const verificationTokens = createTable(
+ "verification_token",
+ (d) => ({
+ identifier: d.varchar({ length: 255 }).notNull(),
+ token: d.varchar({ length: 255 }).notNull(),
+ expires: d.timestamp({ mode: "date", withTimezone: true }).notNull(),
+ }),
+ (t) => [primaryKey({ columns: [t.identifier, t.token] })],
+);