New
Apps 1,895Makers 1,890This week 1,690
Promote your app−30%
0views in
the last 30 days

vichudo-app

Typesafe full-stack app template: Next.js 16, tRPC 11, Prisma 7 and Better Auth

Built with Claude Code

by vichudo · Unclaimed · from CLChile · added Oct 29, 2026
no ratings0 comments

T3 App

A typesafe full-stack app: Next.js 16, tRPC, Prisma, Better Auth, Tailwind CSS and shadcn/ui, checked by TypeScript 7, oxlint and oxfmt.

Create an app

pnpm create vichudo-app@latest my-app

That is the whole setup. It copies this template, names the project, gives it its own local database, writes a .env with a fresh auth secret, fetches the latest shadcn/ui components, creates a git repository and installs dependencies. Then follow the steps it prints, which are Quick start without the first two commands.

It asks one question: all shadcn/ui components, or just the essentials the template's pages use. Either way they come fresh from the registry, so a new app never starts with stale copies. This repository keeps only the essentials.

pnpm create vichudo-app --help lists the options, such as --components and --ref to pin a template version. The CLI's source is in cli/.

Quick start

You need Node.js 24+ (nvm use or fnm use reads .nvmrc) and pnpm (corepack enable provides the exact version this project pins).

pnpm install
pnpm env:init    # creates .env with a fresh secret
pnpm db:dev      # starts a local Postgres (no Docker); leave it running

In a second terminal:

pnpm db:push     # creates the tables
pnpm dev         # starts the app

Open http://localhost:3000, create an account, and write a post.

Working on several apps? Each one has its own local database, but only one pnpm db:dev can run at a time: stop the other with Ctrl+C first.

Everyday commands

Command What it does
pnpm dev Starts the app with hot reload
pnpm db:dev Starts the local database
pnpm check Format, lint, typecheck and test: run it before you push
pnpm format Formats every file (your editor and the commit hook do it too)
pnpm lint:fix Applies lint autofixes
pnpm db:push Syncs the database with prisma/schema.prisma, no migration
pnpm db:migrate Creates and applies a migration, once you have data to keep
pnpm db:studio Browses the database in Prisma Studio
pnpm test Runs the tests (pnpm test:watch keeps them running)
pnpm build Builds for production
All other scripts
Command What it does
pnpm start Serves the production build
pnpm preview Builds, then serves
pnpm typecheck Generates route types, then runs tsc
pnpm format:check Checks formatting without writing
pnpm lint oxlint, type-aware
pnpm db:deploy Applies pending migrations (production)
pnpm db:generate Regenerates the Prisma client (runs on install)
pnpm auth:generate Adds the tables Better Auth's config and plugins need
pnpm auth:secret Prints a new BETTER_AUTH_SECRET
pnpm env:init Creates .env from .env.example if it is missing

How do I…

Add an API endpoint

Add a procedure to a router in src/server/api/routers/:

// src/server/api/routers/post.ts
export const postRouter = createTRPCRouter({
  // …
  byId: publicProcedure
    .input(z.object({ id: z.number() }))
    .query(({ ctx, input }) =>
      ctx.db.post.findUnique({ where: { id: input.id } }),
    ),
});

Register a new router in src/server/api/root.ts. Use protectedProcedure instead of publicProcedure to require a signed-in user; it gives you ctx.session.user.

Show data on a page

Prefetch in a Server Component, read the data in a Client Component:

// src/app/posts/[id]/page.tsx
import { Suspense } from 'react';

import { PostTitle } from '@/app/posts/[id]/_components/post-title';
import { Skeleton } from '@/components/ui/skeleton';
import { HydrateClient, prefetch, trpc } from '@/trpc/server';

export default function PostPage({ params }: PageProps<'/posts/[id]'>) {
  return (
    <Suspense fallback={<Skeleton className="h-6 w-48" />}>
      <Post params={params} />
    </Suspense>
  );
}

async function Post({ params }: Pick<PageProps<'/posts/[id]'>, 'params'>) {
  const id = Number((await params).id);
  prefetch(trpc.post.byId.queryOptions({ id }));
  return (
    <HydrateClient>
      <PostTitle id={id} />
    </HydrateClient>
  );
}
// src/app/posts/[id]/_components/post-title.tsx
'use client';

import { useSuspenseQuery } from '@tanstack/react-query';

import { useTRPC } from '@/trpc/client';

export function PostTitle({ id }: { id: number }) {
  const trpc = useTRPC();
  const { data: post } = useSuspenseQuery(trpc.post.byId.queryOptions({ id }));
  return <h1>{post?.name ?? 'Post not found'}</h1>;
}

Anything that reads request data (params, cookies, the session) renders inside <Suspense>: the rest of the page is prerendered and served instantly. For a mutation, see src/app/_components/latest-post.tsx.

Change the database

Edit prisma/schema.prisma, then run pnpm db:push while prototyping, or pnpm db:migrate to record a migration once there is data to keep. Either regenerates the Prisma client; restart pnpm dev to load it.

Add a UI component

Components live in src/components/ui. If yours isn't there yet, add it from the shadcn/ui registry, then import it:

pnpm exec shadcn add dialog
import { Dialog, DialogContent, DialogTrigger } from '@/components/ui/dialog';

The same command with --overwrite updates a component to its latest version (review the diff: the code is yours). pnpm format puts new files in the house style.

Check who is signed in

  • Server Component (inside <Suspense>): await getSession() from @/server/auth, which returns null when signed out.
  • tRPC procedure: use protectedProcedure; ctx.session.user is set.
  • Client Component: authClient.useSession() from @/lib/auth-client.

Add an environment variable

Declare it in src/env.js (server-only, or NEXT_PUBLIC_ for the browser), add it to .env.example so teammates know about it, then read env.MY_VAR from @/env. Reading process.env directly is a lint error, so a missing variable fails the build instead of a request.

Turn on Google sign-in

  1. In the Google Cloud console, create an OAuth client ID of type "Web application", with the redirect URI http://localhost:3000/api/auth/callback/google (add your production URL's equivalent too).
  2. Set GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in .env, and restart pnpm dev.

"Continue with Google" then appears on the sign-in and sign-up pages.

Add more auth features

Better Auth adds features as plugins: two-factor, passkeys, magic links, organizations, admin (list). Add the plugin in src/server/auth.ts and its client plugin in src/lib/auth-client.ts, then run pnpm auth:generate and pnpm db:push for any tables it needs. Password reset and email verification need an email sender; the spot is marked in src/server/auth.ts.

Deploy

On Vercel, import the repository, add a PostgreSQL database (for example Neon, from the Vercel Marketplace), and set:

  • DATABASE_URL: the production database.
  • BETTER_AUTH_SECRET: a new one from pnpm auth:secret (the build needs it too).
  • BETTER_AUTH_URL: the production URL.

The production database needs the tables. pnpm db:push records no migrations, so before the first deploy record the schema as one with pnpm db:migrate --name init (it may offer to reset a local database made by db:push) and commit prisma/migrations. Then apply migrations to production with pnpm db:deploy, with DATABASE_URL set to the production database, or run it in the build command: pnpm db:deploy && pnpm build.

Anywhere else, deploy it as any Next.js app. Server-only variables, Google's included, are read when the app runs, so one build can serve any environment.

Project structure

prisma/schema.prisma          Database schema
src/
  app/                        Routes: pages, layouts, API endpoints
    (auth)/                   Sign-in and sign-up pages
    _components/              Components used only by the home page
  components/                 Shared components
    ui/                       shadcn/ui (vendored)
  lib/auth-client.ts          Auth for Client Components
  server/
    api/routers/              tRPC routers
    api/trpc.ts               Context, middleware, procedure builders
    auth.ts                   Better Auth config and getSession()
    db.ts                     Prisma client
  trpc/                       tRPC for Client (client.tsx) and Server (server.tsx) Components
  env.js                      Validated environment variables

Editor setup

Open the folder in VS Code and accept the recommended extensions: oxc (format on save, lint fixes), TypeScript Native Preview (TypeScript 7), Tailwind CSS and Prisma. Coding agents read AGENTS.md, and Claude Code formats every file it edits.

What's inside

Layer Choice
Framework Next.js 16: App Router, Turbopack, Cache Components, typed routes
UI React 19.3, Tailwind CSS 4, shadcn/ui on Base UI
API tRPC 11 with TanStack Query and superjson
Database Prisma 7 and PostgreSQL; prisma dev locally
Auth Better Auth: email and password, Google ready
Validation Zod 4; environment variables through @t3-oss/env-nextjs
Language TypeScript 7, strict
Lint and format oxlint and oxfmt
Tests Vitest 5
Tooling pnpm 12, Husky + lint-staged, GitHub Actions CI
Why these choices
  • Rendering. Cache Components and Partial Prefetching are on, the defaults for new Next.js 16.4 apps. Everything outside a <Suspense> boundary is prerendered into a static shell; request-specific parts stream in at request time. Opt into caching with 'use cache'.
  • Better Auth, not Auth.js. Auth.js is maintained by the Better Auth team in security-patch mode, and they recommend Better Auth for new projects.
  • oxlint and oxfmt, not ESLint and Prettier. Much faster, one config each. The lint config also guards boundaries: environment access goes through @/env, UI never imports @/server, and imports use @/ rather than ../.
  • TypeScript 7. The native compiler type-checks this project in about half a second. It has no JavaScript API yet, hence the editor extension.
  • Prisma 7. The connection URL lives in prisma.config.ts; the client connects through @prisma/adapter-pg and is generated into src/generated/prisma. pnpm db:dev runs Prisma Postgres locally.
  • pnpm supply-chain defaults. New releases install only once they are a day old, and only the packages listed in pnpm-workspace.yaml may run install scripts.

Description taken from the repository README.

Ratings

–
0 ratings
5 0
4 0
3 0
2 0
1 0

Rate this app

Sign in to rate

Comments 0

Sign in to comment

No comments yet.

Related apps

#template · #nextjs · #trpc