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
The Express Setup
Our Express configuration prioritizes security from the start:
// api/src/index.ts
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
const app = express();
// Security headers
app.use(helmet());
// CORS for frontend
app.use(cors({
origin: process.env.FRONTEND_URL,
credentials: true,
}));
// Rate limiting
app.use(rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
}));
// Body parsing with size limit
app.use(express.json({ limit: '1mb' }));
// Routes
app.use('/api/auth', authRoutes);
app.use('/api/pillars', authenticateToken, pillarRoutes);
app.use('/api/entries', authenticateToken, entryRoutes);
// ...
app.listen(8000);
Prisma Configuration
Running two Prisma clients requires some setup:
// Platform schema (platform.prisma)
generator client {
provider = "prisma-client-js"
output = "../node_modules/.prisma/platform"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// Vault schema (vault.prisma)
generator client {
provider = "prisma-client-js"
output = "../node_modules/.prisma/vault"
}
datasource db {
provider = "mysql"
url = env("VAULT_DATABASE_URL")
}
// Using both clients
import { PrismaClient as PlatformClient } from '.prisma/platform';
import { PrismaClient as VaultClient } from '.prisma/vault';
export const platformDb = new PlatformClient();
export const vaultDb = new VaultClient();
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.
What’s Next
With the architecture in place, the next post dives deep into the encryption layer – the whole reason
we made this pivot. We’ll cover:
- Envelope encryption (MEK/DEK) implementation
- Per-user key generation
- Crypto-shredding capability
- Performance considerations
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