Skip to main content
Architecture8 min readDecember 10, 2025

Routiine.io Architecture: Sales CRM at Scale

The architecture behind Routiine.io — a sales intelligence CRM built with Nuxt 3, Drizzle ORM, and Neon PostgreSQL. Design decisions for real-time dashboards and integrations.

James Ross Jr.

James Ross Jr.

Strategic Systems Architect & Enterprise Software Developer

Why Build Another CRM

The CRM market is one of the most saturated categories in software. Salesforce alone has over 150,000 customers. HubSpot, Pipedrive, Close, and dozens of others serve every market segment from solopreneurs to enterprises. Building a new CRM requires a specific thesis about what the existing options get wrong.

Routiine.io's thesis is about intelligence, not data entry. Traditional CRMs are record-keeping systems — they store contacts, deals, activities, and notes. The salesperson is responsible for interpreting that data, identifying which deals need attention, and deciding where to focus their limited time. Routiine.io inverts this: the system analyzes activity patterns and tells the salesperson where to focus, using momentum scoring to surface deals that are accelerating, stalling, or at risk.

This thesis shaped every architectural decision. The system needed to ingest activity data from multiple sources, process it in near-real-time, and surface actionable insights through a fast, responsive dashboard. The architecture had to support these requirements from the beginning rather than retrofitting them onto a traditional CRUD application.

The Stack

Routiine.io is built on Nuxt 3 with server routes handling the API layer through Nitro. The database is Neon PostgreSQL, accessed through Drizzle ORM. Authentication uses a custom implementation built on secure session management. The frontend uses Nuxt UI components styled with Tailwind CSS.

Each of these choices was made for specific reasons. Nuxt 3 was selected for the same reasons I chose it for BastionGlass — full-stack TypeScript, server-side rendering for the marketing pages, and the ability to deploy API routes and frontend as a single application. The consistency across projects also meant shared knowledge and patterns.

Drizzle ORM was chosen over Prisma for Routiine.io specifically because of its SQL-first approach. Prisma's query builder is excellent for straightforward CRUD operations, but Routiine.io's analytics queries — aggregations, window functions, complex joins across activity data — are easier to express and optimize when the ORM stays close to SQL. Drizzle lets you write queries that look like SQL with TypeScript type safety, which was the right trade-off for a data-intensive application.

Neon PostgreSQL was selected for its serverless scaling model. Routiine.io's usage pattern has significant peaks and valleys — heavy dashboard usage during business hours, minimal activity at night and on weekends. Neon's autoscaling means we do not pay for compute during idle periods, which keeps infrastructure costs proportional to actual usage during the growth phase.

Data Ingestion Architecture

The intelligence layer depends on comprehensive activity data. Routiine.io ingests data from three primary sources: direct user activity within the platform, Salesforce integration syncs, and email tracking.

Each source has a different data format, delivery mechanism, and reliability profile. Direct platform activity is synchronous — when a user logs a call or schedules a meeting, the event is recorded immediately. Salesforce syncs are periodic — a background job polls for changes on a configurable interval. Email tracking events arrive asynchronously via webhooks.

All ingested data is normalized into a common event schema before processing. An event has a type, a timestamp, an actor (who performed the action), a subject (what the action was performed on), and metadata specific to the event type. This normalization layer means the momentum scoring engine and the analytics dashboards work identically regardless of the data source. A meeting logged directly in Routiine.io and a meeting synced from Salesforce produce the same event type with the same schema.

The ingestion pipeline writes events to an append-only events table. This table grows large over time, so we partition it by month and maintain indexes optimized for the two primary query patterns: all events for a specific deal (used by momentum scoring) and all events by a specific user within a time range (used by activity reporting).

Dashboard Performance

The Routiine.io dashboard is the primary interface. It shows the deal pipeline, momentum scores, activity timeline, forecasts, and alerts on a single screen. This screen needs to load fast and update frequently — a dashboard that takes five seconds to load will not get used daily.

Performance optimization started with the data model. Precomputed aggregates are stored in materialized views that refresh on a schedule. The pipeline summary — total deals by stage, total value, weighted forecast — is precomputed rather than calculated on every page load. Momentum scores are precomputed hourly. Activity counts are precomputed daily.

The API layer returns only the data needed for the current view. The pipeline endpoint returns deal summaries — name, value, stage, momentum score — not full deal records with all activities and notes. Detail views load additional data on demand when the user clicks into a specific deal.

Client-side rendering uses Vue's reactivity system efficiently. The dashboard components are structured so that updating a single deal's momentum score triggers a re-render only of that deal's card, not the entire pipeline view. This keeps the interface responsive even with hundreds of deals visible.

Billing Integration

Routiine.io uses multi-tier Stripe billing with plans that scale based on the number of users and the feature set. The billing architecture is integrated into the application at the middleware level — every API request checks the tenant's subscription status and feature entitlements before processing.

This integration needed to be reliable without being brittle. Stripe webhook processing handles subscription lifecycle events — creation, upgrade, downgrade, cancellation, payment failure. Each event updates the tenant's entitlements in the local database, and all authorization checks read from the local database rather than querying Stripe on every request. This means a temporary Stripe outage does not break the application — entitlements continue to be enforced from the last known state.

The architecture supports a free tier with limited deals and users, a professional tier with full features, and an enterprise tier with Salesforce integration and priority support. Feature gating is implemented through a capabilities system rather than hard-coded tier checks, so adding new tiers or adjusting feature bundles is a configuration change rather than a code change.