The Big Pivot: Node.js Architecture

January 6, 2026 4 min read

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:

  1. 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.
  2. AI Pair Programming: Claude Code generated boilerplate, caught type errors,
    and suggested patterns. We focused on architecture decisions.
  3. TypeScript + Prisma: Type safety meant fewer bugs. Prisma’s schema
    language made database design declarative.
  4. 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
Secret Link