This is a T3 Stack project bootstrapped with create-t3-app.
First install bun:
npm install -g bunThen run the script:
npm run seed
# or
yarn seed
# or
pnpm run seedWhy even use tRPC?
Since we are using typescript it's nice to have typesense on everything so you don't make dumb mistakes.
tRPC lets you call backend functions directly from your frontend as if they were local functions, with full TypeScript type safety.
This means you don't actually have to write REST endpoints or manual API routes:
// backend: app/api/users/route.ts
export async function GET() {
const users = await db.user.findMany();
return Response.json(users);
}
// frontend: You make a fetch request
const response = await fetch("/api/users");
const users = await response.json(); // there's no type safety hereInstead, with tRPC we know what exactly is going to be returned even from the frontend:
// backend: server/api/routers/user.ts
export const userRouter = createTRPCRouter({
getAll: publicProcedure.query(async ({ ctx }) => {
return ctx.db.user.findMany();
}),
});
// frontend: You call it like a function
const { data: users } = trpc.user.getAll.useQuery();
// typescript knows the exact type of the userHowever, note that we still need API endpoints for things like:
- webhooks
- public APIs to fetch data from
- file uploads (even though tRPC can handle them, it's easier defining a normal API route)
TanStack Query manages server state like fetching, caching, synchronizing, and updating data from APIs so you don't have to write all the loading states, error handling, and cache invalidation logic yourself.
For those who have worked with Tanstack Query you might be wondering where and how tRPC is integrated into Tanstack. You can view some of the config files in the trpc folder.
tRPC is built upon Tanstack Query. This means that we can use their built in utils to invalidate or get query data.
const utils = trpc.useUtils();
// Invalidate all user queries
utils.user.invalidate();
// Invalidate specific query
utils.user.getById.invalidate({ id: "123" });
// Prefetch
await utils.user.getAll.prefetch();
// Set query data
utils.user.getById.setData({ id: "123" }, newUserData);
// Get query data
const userData = utils.user.getById.getData({ id: "123" });We can even get and view tanstack query keys if needed for prefetch operations:
import { getQueryKey } from "@trpc/react-query";
// Get the query key
const queryKey = getQueryKey(trpc.user.getById, { id: "123" });
// Invalidate
queryClient.invalidateQueries({ queryKey });
// Prefetch
await queryClient.prefetchQuery({
queryKey,
queryFn: () => trpc.user.getById.fetch({ id: "123" }),
});
// Set data manually
queryClient.setQueryData(queryKey, newUserData);This project uses drizzle for DB migrations and schema managment, rather than writing raw SQL and doing manual migrations to Supabase
We are able to:
- define our schema in TS so it's readable and resuable
- automatically generate migrations when this schema changes
- sync these migrations with our db
To generate the schema required by Better Auth, run the following command:
npx @better-auth/cli@latest generate| Environment | Purpose | Database | Env File |
|---|---|---|---|
| Local | Each dev’s personal Supabase project | Supabase (personal instance) | .env.local |
| Dev | Shared dev environment for integration testing | Supabase (team project) | .env.dev |
| Prod | Production database for real data | Supabase (production project) | .env.prod |
| Command | Description |
|---|---|
generate |
Generate a new migration file when schema changes |
migrate |
Apply all pending migrations to the selected database |
push |
Force sync schema with the database (used only locally) |
drop |
Delete all tables in the target database (used only locally) |
There are three workflows when working on a feature that involves changing the schema:
- Locally testing
# 1. Make schema changes in src/db/schema.ts
# 2. Generate migration
yarn db:generate
# 3. Apply migration to your personal Supabase database
yarn db:migrate
# 4. Run your app and test new DB features locallyYou can use yarn db:push:local for quick testing of schema changes against your personal Supabase project because it directly syncs your schema to your local DB without generating migrations.
However:
- Never use push on shared dev or prod databases.
- Always finalize your schema and then run:
yarn db:generate
yarn db:migratebefore committing, so migrations stay in version control.
Never run push or drop on the shared dev or prod databases because those commands can delete or overwrite data.
- After the merge to main, we have to add these changes made in the feature branch to dev
git pull
yarn db:migrate- Deploying confirmed changes to prod
# only after these changes have been verified to be good
yarn db:migrateBefore running the seed script, ensure the following setup is complete:
- Bun installed and available in your terminal
- Local Supabase instance running
- Database migrations have been applied. Run:
yarn db:migrateOnce your database schema is up to date, run the seed script to create a default administrator user:
yarn seedAfter running the script, the terminal will output a generated admin email and password.
Use these credentials on the /login page to sign in.
Once logged in, the session persists across the app, allowing you to test authentication, protected routes, and role-based access during development.
I've added a plugin typescript-plugin-css-modules but you also might have to follow this tutorial here and add it to your VSCode settings.
To learn more about the T3 Stack, take a look at the following resources:
- Documentation
- Learn the T3 Stack — Check out these awesome tutorials
You can check out the create-t3-app GitHub repository — your feedback and contributions are welcome!
Follow our deployment guides for Vercel, Netlify and Docker for more information.