Building a Modern Full-Stack Web Application - Part 2
This post covers deployment and production workflows. Part 1 covered the tech stack and architecture.
Hosting & Deployment
Infrastructure
Fly.io hosts the application. It scales to zero when idle and runs database migrations during deployment.
Cloudflare handles CDN, DDoS protection, and SSL.
Neon hosts PostgreSQL with serverless auto-scaling, database branching for dev/staging, point-in-time recovery, and connection pooling.
Docker Multi-Stage Builds
The application uses multi-stage builds for optimization:
FROM node:24-alpine AS base
WORKDIR /app
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci --only=production
FROM base AS build
COPY . .
RUN npm run build
FROM base AS runtime
COPY --from=build /app/build ./build
COPY --from=deps /app/node_modules ./node_modules
EXPOSE 3000
CMD ["npm", "start"]
Development Workflow
# Development
npm run dev # Start development server
npm run test # Run tests
npm run typecheck # Type checking
# Database
npm run db:studio # Visual database management
npm run db:seed # Populate with sample data
npm run db:migrate # Apply schema changes
Code Quality Standards
The project uses ESLint with React and TypeScript rules, Prettier for formatting, strict TypeScript with noUncheckedIndexedAccess, Husky for git hooks, Vitest for unit tests, and Playwright for E2E testing.
Database Development with Drizzle Kit
Tip
Use db:push for quick development iteration, but always use db:generate and db:migrate for production changes to maintain migration history.
Schema changes follow a structured approach:
# 1. Edit app/db/schema.ts
# 2. Generate migration
npm run db:generate
# 3. Apply migration
npm run db:migrate
# Development workflow
npm run db:push # Quick schema sync (dev only)
npm run db:studio # Visual database UI
npm run db:reset # Clean database reset
Performance
Vite delivers sub-second HMR, tree shaking, route-based code splitting, and image compression.
Drizzle ORM caches query plans with prepared statements and pools connections. Type-safe migrations prevent runtime errors.
Catalyst UI gave us high-quality components without overhead. Fly.io reduced operational complexity to near zero.
Caution
Run db:generate and db:migrate before deploying, never db:push. Using db:push in production skips migration history and makes rollbacks impossible.
The deployment pipeline runs tests on every push, checks types, runs migrations during deploy, and rolls back on failure. React Router v7, Drizzle ORM, better-auth, and Fly.io together make a stack I'd pick again.