As a fair warning, this article is very technical, diving into the how we built Papertrail thus far. Feel free to reach out with questions or comments on our discord here (coming soon).
So, whats in our stack?
- T3
- Nextjs (App dir)
- Tailwind
- tRPC
- NextAuth
- TS
- Planetscale
- Github / Github Actions
- Drizzle
This stack contains most of the usual suspects like Nextjs, Tailwind and tRPC, but we threw a little bit of bleeding edge into the mix by using the new App router. This has already proven to be much more efficient, with my favorite parts being the way routing now works compared to the pages directory. Layouts, for example, are much easier to create for route paths. The largest issue with using App dir however, is how little documentation and support there is from third party libraries. This required some deep diving into how these libraries worked under the hood so that we could integrate them into our project.
Using the T3 Stack with App Directory
The first obstacle was migrating over the T3 stack from the pages directory to the app directory. By default T3 uses the pages directory and integrates all the aforementioned libraries into the project. This article helped get a basis for how we could accomplish the hardest parts like tRPC and NextAuth on the client side. Essentially, we create a Providers
component that takes in a children
prop. Then, we can wrap the entire application in our client providers from our /app/layout.tsx
.
// app/layout.tsx
import "~/styles/globals.css";
import Header from "./Header";
import { Providers } from "./providers";
export const metadata = {
title: "Papertrail",
description: "A place to read together",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>
<Header />
<main>
<div className="mx-auto max-w-7xl pb-20 sm:px-6 lg:px-8">
{children}
</div>
</main>
</Providers>
</body>
</html>
);
}
// app/providers.tsx
export const Providers: React.FC<{ children: React.ReactNode }> = ({
children,
}: {
children: React.ReactNode;
}) => {
...
...
return (
<trpc.Provider client={trpcClient} queryClient={queryClient}>
<QueryClientProvider client={queryClient}>
<SessionProvider>
{children}
</SessionProvider>
</QueryClientProvider>
</trpc.Provider>
);
};
This will then give us access to NextAuth sessions and tRPC client side. Implementing the RSC route.ts
file was much more trivial, since that documentation seems to be available for most libraries we used.
Custom NextAuth
Since we are using NextAuth for our authentication layer, we needed a way to customize the user creation flow. Papertrail will use a mixture of emails and unique usernames to identify users to each other, but internally use generated UUID’s. When using the default adapters and schema with NextAuth, it will populate the name
field for a new user with their OAuth name from any given provider. Since we are using multiple providers, and usernames are something we want to let people select, a name conflict was in the realm of possibilities. The solution to this was actually surprisingly simple, albeit undocumented. When creating an adapter (in our case DrizzleAdapter
), the adapter returns an object of all the different methods NextAuth needs to function. Documentation on the list of all these methods can be found here. To override the default logic, we simply copy the original method from the adapter source code, and stop it from inserting into the name field
adapter: {
...DrizzleAdapter(db),
async createUser(data) {
const id = crypto.randomUUID();
await db
.insert(Users)
.values({ email: data.email, image: data.image, id });
return (await db
.select()
.from(Users)
.where(eq(Users.id, id))
.then((res) => res[0])) as AdapterUser;
},
},
Notion as a CMS
We use a notion database as a CMS and integrate it into our site with help from this post. After setting up that, we use tailwinds prose
class to custom style the markdown we get from notion, and rebuild the site every night with any new blog posts via a build hook on Vercel triggered from Upstash. For code highlighting in code blocks like above, we use rehype, which adds styles during build time.
CICD and Project Management
Another impressive part of our infrastructure is our CICD and issue workflow. For CICD we use github combined with the default Vercel integrations. Each PR represents 1 or more issues, with a branch name that references the issue id and title. When a PR is opened Vercel will automatically build and deploy a preview of the code on that branch so we can make sure everything is working as it should before we push to production. Another action will also attempt to make a deploy request on planet scale from the stage database to main. If there are no schema changes, nothing happens. When a PR is merged another action will trigger merging the db deploy request.
To manage issues and the project as a whole, we decided to try out linear. Linear has quickly become my favorite app to use for issue tracking and project management. When we copy a branch name generated from linear, the task is automatically marked as in progress. When we reference the task in a PR, it gets moved to “awaiting merge”. When a PR is merged, the task moves to completed. Linear makes the project management process so seamless and automated, I sometimes find myself looking for an excuse to use it.