The application stack from Part 1 only became useful once I could deploy it repeatedly without losing database history. I used Fly.io for the application, Cloudflare at the edge, Neon for PostgreSQL, and packaged the application as a Docker image.
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 a multi-stage Docker build:
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
Checks I kept in the loop
The project uses ESLint with React and TypeScript rules, Prettier, strict TypeScript with noUncheckedIndexedAccess, Husky hooks, Vitest, and Playwright. I wanted the same checks available locally and on every push.
Database Development with Drizzle Kit
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
What worked in production
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 a useful component base. Fly.io kept the deployment configuration small enough to understand.
Keep migration history in production Use
db:pushfor disposable development state. Production changes needdb:generateanddb:migrateso the schema has reviewable history and a recovery path.
The deployment pipeline runs tests and type checks on every push, then runs migrations during deployment and rolls back on failure. I would pick this stack again, with one rule I would not relax: development can use db:push, but production changes need migration files.
One quick signal
Did this earn your time?
Thanks. That gives me something concrete to check.



