Opinionated fullstack starter for new client engagements. Built so a new project
goes from git clone to a deployed app in about 30 minutes.
This is the standard template for fikst consulting engagements. Use it as the starting point for every new client project unless there's a specific reason not to.
| Layer | Choice |
|---|---|
| Framework | Next.js 15 (App Router) + React 19 |
| Language | TypeScript (strict) |
| Styling | Tailwind CSS v4 |
| Database | Postgres via Drizzle ORM |
| Auth | Auth.js v5 (NextAuth) + GitHub OAuth |
| Validation | Zod (shared types between client/server) |
| Testing | Vitest |
| Lint/Format | ESLint (next/core-web-vitals) + Prettier |
| CI | GitHub Actions (typecheck / lint / format / test / build) |
| Hosting | Vercel (default) |
| DB hosting | Neon (default; any Postgres works) |
Why these picks: every one is boring, well-supported, and something a client engineering team can pick up without a handoff document. If you're tempted to swap in something exotic, write down why in the engagement README.
You'll need: Node 20+, a GitHub account, a Neon account (free tier is fine), and a Vercel account.
Click Use this template in GitHub, or:
gh repo create my-client-app --template fikst/fullstack-starter --private --clone
cd my-client-appnpm installSign in to Neon, create a new project, copy the connection string. (Or use Supabase, Railway, RDS, or local Postgres — anything that speaks Postgres.)
cp .env.example .envFill in:
DATABASE_URL— the Neon connection string from step 3.AUTH_SECRET— generate withopenssl rand -base64 32.AUTH_URL— leave ashttp://localhost:3000for now.AUTH_GITHUB_ID/AUTH_GITHUB_SECRET— see step 5.
- Homepage URL:
http://localhost:3000 - Authorization callback URL:
http://localhost:3000/api/auth/callback/github
Copy the client ID and generate a client secret into .env.
npm run db:generate # generates SQL from src/db/schema.ts
npm run db:migrate # applies migrations to your DATABASE_URLnpm run devOpen http://localhost:3000, sign in, confirm you land
on /dashboard.
- Push this repo to GitHub.
- In Vercel, import the repo.
- Add the same env vars from
.envto the Vercel project. - Update
AUTH_URLto your production URL (e.g.https://my-client-app.vercel.app). - Add a second GitHub OAuth callback URL on your OAuth app:
https://my-client-app.vercel.app/api/auth/callback/github. - Trigger a deploy. CI will run on every PR thereafter.
That's the engagement on a real URL. Replace the home page and dashboard with client-specific UI and you're shipping product, not boilerplate.
src/
app/ # Next.js App Router
page.tsx # public home page
layout.tsx # root layout
globals.css # Tailwind v4 entry
(auth)/signin/ # sign-in screen
api/
auth/[...nextauth]/route.ts # Auth.js handlers
health/route.ts # health check (used by uptime monitors)
dashboard/ # authenticated area
auth/
index.ts # NextAuth() config + exports (auth, signIn, signOut)
db/
client.ts # Drizzle + postgres-js client (singleton)
schema.ts # Drizzle schema (users, accounts, sessions, todos)
migrate.ts # migration runner used by `npm run db:migrate`
lib/
env.ts # Zod-validated environment config
utils.ts # cn() + small shared helpers
middleware.ts # protects /dashboard/*
tests/ # Vitest tests
drizzle/ # generated SQL migrations (commit these)
.github/workflows/ # CI
| Command | What it does |
|---|---|
npm run dev |
Start the dev server on :3000 |
npm run build |
Production build |
npm start |
Start the production build |
npm run typecheck |
tsc --noEmit |
npm run lint |
ESLint |
npm run format |
Prettier write |
npm run format:check |
Prettier check (used in CI) |
npm test |
Vitest (one-shot) |
npm run test:watch |
Vitest watch mode |
npm run db:generate |
Generate SQL migrations from src/db/schema.ts |
npm run db:migrate |
Apply pending migrations |
npm run db:studio |
Drizzle Studio (UI for the DB) |
- Edit
src/db/schema.ts. npm run db:generate(creates SQL indrizzle/).npm run db:migrate(applies toDATABASE_URL).- Commit both the schema change and the generated SQL.
Auth.js providers are configured in src/auth/index.ts. To add Google:
import Google from 'next-auth/providers/google';
providers: [
GitHub({ ... }),
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
}),
],Then add AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET to .env.example, .env, and
your Vercel project. The Drizzle adapter handles the database side
automatically.
The src/middleware.ts file gates /dashboard/*. To protect another path,
add it to the matcher array. For per-page checks (server components),
call auth() at the top:
import { redirect } from 'next/navigation';
import { auth } from '@/auth';
export default async function Page() {
const session = await auth();
if (!session?.user) redirect('/signin');
// ...
}This template is the fikst default. Reasons to deviate:
- Client already runs on AWS / GCP / their own infra → use it. Vercel is the fallback, not a hard requirement.
- Workload requires long-running background jobs or websockets at scale → swap Next.js API routes for a dedicated Node service (Fastify or Hono on Fly/Render). Keep the Next.js frontend.
- Heavy reporting / analytics → add Postgres read replicas before reaching for a separate analytics database.
- Client already uses Prisma → swap Drizzle for Prisma. The rest of the template stays.
Document the deviation in the client repo's README so future you knows why.
MIT — see LICENSE.