BREAKING CHANGE
From: Laravel 11 + PHP + SQLite
To: Node.js + Express + TypeScript + MySQL + Prisma
Why Node.js?
The decision to pivot wasn't about Node.js being "better" than Laravel. It was about choosing the right tool for our specific requirements. Here's why Node.js won:
Encryption First
Node.js crypto module gives us low-level control over encryption. No framework magic to fight against.
TypeScript
Type safety across the entire stack. Catches encryption bugs at compile time. Same language as our Next.js frontend.
Prisma ORM
Multi-database support out of the box. Separate clients for platform vs vault. Type-safe queries.
Async Native
Encryption/decryption on every request benefits from non-blocking I/O. Promise-based APIs everywhere.
The New Architecture
Dual Database Pattern
The most important architectural decision was separating data into two databases:
┌─────────────────────┐ ┌─────────────────────┐
│ timos_platform │ │ timos_vault │
├─────────────────────┤ ├─────────────────────┤
│ • users │ │ • daily_entries │
│ • auth_sessions │ │ • feels_entries │
│ • oauth_accounts │ │ • pillars │
│ • subscriptions │ │ • weekly_recaps │
│ • user_deks │ │ • morning_habits │
│ • audit_logs │ │ ALL ENCRYPTED │
└─────────────────────┘ └─────────────────────┘
Why Two Databases?
The platform database contains authentication and billing data that needs to be queryable.
The vault database contains user content that's always encrypted. This separation means:
- We can backup/restore them independently
- Different retention policies
- Vault can be hosted in a different region for compliance
- Clear security boundary
Project Structure
api/
├── src/
│ ├── index.ts # Express entry point
│ ├── middleware/
│ │ ├── auth.ts # JWT verification
│ │ ├── rateLimit.ts # Rate limiting
│ │ └── csrf.ts # CSRF protection
│ ├── services/
│ │ ├── auth.ts # Signup, login, MFA
│ │ ├── oauth.ts # Microsoft, Google, Apple
│ │ ├── encryption.ts # MEK/DEK management
│ │ ├── pillars.ts # Pillar CRUD
│ │ ├── dailyEntries.ts
│ │ ├── feelsEntries.ts
│ │ └── ...
│ ├── routes/
│ │ ├── auth.ts
│ │ ├── pillars.ts
│ │ └── ...
│ └── prisma/
│ ├── platform.prisma
│ └── vault.prisma
└── package.json
Building in 5.5 Hours
Here's how we structured the sprint:
Hour 1: Foundation
- Express + TypeScript setup
- Prisma installation and configuration
- Dual database connections
- Basic middleware (CORS, helmet, body-parser)
Hour 2: Database Schema
- Platform schema: 11 tables (users, sessions, tokens, DEKs, etc.)
- Vault schema: 9 tables (entries, pillars, recaps, etc.)
- Prisma migrations
- Type generation
Hours 3-4: Core Services
- Encryption service (MEK/DEK)
- Auth service (signup, login, JWT)
- MFA service (TOTP with encrypted secrets)
- OAuth service (Microsoft, Google, Apple stubs)
Hour 5: Routes & API
- Auth routes (/signup, /login, /verify-mfa)
- OAuth callback routes
- Protected route middleware
- Error handling
Hour 5.5: Testing & Fixes
- Manual API testing
- Bug fixes
- Frontend API client updates
- Documentation
Session 3 Stats
Time spent: 5.5 hours
Manual estimate: 25.0 hours
ROI: 4.5x
Files created: ~35
Lines of code: ~4,000
What Made This Possible
Rebuilding an entire backend in 5.5 hours sounds impossible. Here's what made it work:
- Clear Requirements: Sessions 1-2 taught us exactly what we needed. No exploratory design work - we knew the data model, the auth flow, the API shape.
- AI Pair Programming: Claude Code generated boilerplate, caught type errors, and suggested patterns. We focused on architecture decisions.
- TypeScript + Prisma: Type safety meant fewer bugs. Prisma's schema language made database design declarative.
- No Perfectionism: We built the minimum viable backend. Polish would come later. Working code beats perfect code.
Key Takeaways
- Choose architecture based on your hardest requirements
- Dual databases provide clear security boundaries
- TypeScript + Prisma is a powerful combination
- 5.5 hours of focused work can accomplish a lot with clear requirements
- Previous "failed" work provides valuable learning