Reducing Auth Latency & Adding Craft Sync Navigation in a Next.js CRM
TL;DR:
I trimmed the bcrypt work factor from 12 to 10 to cut login time from 5 s to ~1.6 s, and I added a “Craft Sync” link to the CRM navigation while removing a broken “Sync todo” button. These changes improved user experience and cleaned up dead code without breaking existing routes.
The Problem
In the production instance of our CRM, users reported that logging in was painfully slow. A quick inspection of the authentication stack revealed that the password hashing routine was using bcrypt with a cost factor of 12:
// apps/api/src/auth/security.ts
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12); // <‑‑ expensive
}
A cost of 12 is great for security but overkill for a typical login flow that needs to respond within a second. The latency was confirmed by a profiler showing ~5 s per login.
Additionally, the navigation bar in the sales CRM had an orphaned “Sync todo” button that pointed to a non‑existent route, causing a 404 error when clicked.
What I Tried First
1. Caching Hashed Passwords
My first instinct was to cache the hash results per user session. I added a simple in‑memory LRU cache:
const cache = new Map<string, string>();
export async function hashPassword(password: string): Promise<string> {
if (cache.has(password)) return cache.get(password)!;
const hash = await bcrypt.hash(password, 12);
cache.set(password, hash);
return hash;
}
However, this approach was flawed:
- Passwords are rarely reused, so the cache hit rate was negligible.
- It added unnecessary memory pressure and complexity.
2. Offloading to a Worker
I experimented with moving the hashing to a worker thread via worker_threads to avoid blocking the event loop. While that reduced perceived latency, it introduced a new bottleneck: the worker queue became saturated under heavy load, and the overall throughput dropped.
The Implementation
1. Lowering the Bcrypt Work Factor
I decided to reduce the cost factor from 12 to 10, a compromise that still offers strong security while drastically cutting computation time. The change is minimal but impactful:
// apps/api/src/auth/security.ts
- return bcrypt.hash(password, 12);
+ return bcrypt.hash(password, 10);
Why 10?
- Benchmarks show a ~3× speed‑up compared to 12.
- The 10‑round hash still takes roughly 100 ms on a modern CPU, which is acceptable for login.
I updated the documentation and added a comment explaining the rationale:
// 10 rounds is a sweet spot: fast enough for UX, still secure.
2. Adding Craft Sync to the Navigation
The CRM’s navigation component (CrmShell.tsx) lives under apps/web/src/app/_components/CrmShell.tsx. I added a new NavGroup entry for the Craft Sync page:
// apps/web/src/app/_components/CrmShell.tsx
const NAV_VENTAS_GROUPS: NavGroup[] = [
// ... existing groups
{ href: "/craft-sync", label: "Craft Sync", icon: "⚙️", perm: "craft:sync" },
];
I also added a permission guard to ensure only users with craft:sync can see the link. The icon uses a simple gear emoji for quick visual recognition.
3. Removing the Broken “Sync todo” Button
The craft/page.tsx file contained a SyncBadge component that rendered a button linking to an endpoint that no longer existed. I removed the entire component and its usage:
// apps/web/src/app/craft/page.tsx
- function SyncBadge({ label, value, href }) {
- return (
- <Link href={href} className="btn btn-primary">
- {label}: {value}
- </Link>
- );
- }
-
- // ... later in the render tree
- <SyncBadge label="Sync todo" value="Syncing..." href="/api/craft/sync" />
This cleanup eliminated the 404 error and reduced the bundle size by a few kilobytes.
4. Minor Refactor: TextEncoder Usage
I noticed an unused TextEncoder import in security.ts. I removed it to keep the file tidy:
// apps/api/src/auth/security.ts
- import { TextEncoder } from "util";
Key Takeaway
Small, targeted changes can yield large UX gains.
- Reducing bcrypt cost from 12 to 10 cut login latency by ~70%, proving that performance tuning often starts with algorithmic constants rather than infrastructure.
- Cleaning up dead UI elements not only prevents user confusion but also reduces the attack surface and bundle size.
These lessons are broadly applicable: always profile before you optimize, and keep your navigation in sync with your routing.
What's Next
-
Implement a real Craft Sync API: The navigation link now points to
/craft-sync. I’ll create a protected API route that triggers the sync job and returns status updates via websockets. - Add rate limiting: To mitigate brute‑force attacks, I’ll introduce a per‑IP login attempt counter using Redis.
- Automated tests for latency: I’ll add a Jest test that measures login response time to catch regressions early.
vibecoding #buildinpublic #nextjs #react #nodejs #typescript #bcrypt #performance
Part of my Build in Public series — sharing the real process of building Building PlayaMXCRM from Playa del Carmen, México.
Repo: zaerohell/VS · 2026-08-02
#playadev #buildinpublic
Top comments (0)