Building a CRM That Works Without Internet: A 3-Week Journey

March 22, 2026

My manager messaged me: “Can we talk about making the CRM work offline?” I looked up from my laptop. “Offline? Like, no internet at all?” “Yeah. The sales guys keep complaining. Can you figure it out?” I’d been coding for less than two years. I’d never built anything like this. But I said yes anyway.


What followed was three weeks of intense research, creative problem-solving, and more Google searches than I can count. This is the story of how I added offline capability to a production CRM, and what I learned about software engineering along the way.

The Real Problem

The client's sales team had a legitimate frustration. They visited factories and warehouses in locations with spotty or zero internet connectivity. Every time they tried to access the CRM, they'd see the dreaded “no internet connection” message (you know, the Chrome dinosaur game that's fun the first time but infuriating when you're trying to work).

  • Drive to a remote location
  • Try to open CRM
  • Fail
  • Write orders on paper
  • Drive back to the office
  • Manually enter everything into the system
  • Hope they didn't make transcription errors

The CRM was built in the SAP Hybris ecosystem using JSP (Java Server Pages). The architecture was complex: multiple websites (the CRM and an e-commerce platform) lived in the same codebase, differentiated only by URL paths. And I had three weeks to figure it out.

Week 1: Down the Research Rabbit Hole

I started where every developer starts: Google.

  • “How to make a website work offline”
  • “Offline web application”
  • “Cache website without internet”

That's when I discovered Service Workers.

What Are Service Workers?

In simple terms, Service Workers are JavaScript files that run in the background of your browser, separate from your web page. They act like a middleman between your website and the network, intercepting requests and deciding whether to fetch from the internet or serve cached content.

Think of them as a smart assistant that:

  • Saves copies of your website's files (HTML, CSS, JavaScript)
  • Watches for when you go offline
  • Serves those saved copies when there's no internet
  • Syncs data back to the server once you're online again

It's the technology that powers Progressive Web Apps (PWAs), but you don't need a full PWA to use Service Workers.

The Challenge: Everything Was for React

Here's the problem I ran into: almost every Service Worker tutorial I found was for React or Next.js PWAs. People assumed you were building a modern single-page application.

But I was working with JSP. A server-side templating language from 2004.

I spent days reading documentation, trying to understand how Service Workers worked at a fundamental level. The MDN docs became my bible. I read about the Service Worker lifecycle (install, activate, fetch), caching strategies, and offline-first architecture.

The breakthrough came when I realised: Service Workers are just JavaScript. They don't care about your backend framework. If you're serving HTML, CSS, and JavaScript to the browser, you can use Service Workers.

Planning the Architecture

By the end of Week 1, I had a plan:

  • Register a Service Worker at the root level of our application
  • Cache CRM-specific assets (HTML, CSS, JS files) but NOT the e-commerce site
  • Detect offline mode using navigator.onLine
  • Store cart data locally in an encrypted format
  • Show a sync prompt when the user comes back online
  • Send data to the backend if the user confirms

Simple in theory. Painful in practice.

Week 2: Implementation (Where Things Got Tricky)

Problem #1: The Root-Level Conundrum

Service Workers need to be registered at the root level of your domain to control all pages. But in our SAP Hybris setup, multiple sites shared the same codebase. I couldn't just drop a service-worker.js file at the root without affecting both the CRM and the e-commerce site.

Then one of my colleagues came through with a Java-based solution. In SAP Hybris, there's a way to programmatically link files to the root level through the Java entry point, even if they don't physically exist there. It was hacky, but it worked. We got the Service Worker registered.

Problem #2: Selective Caching

The CRM and e-commerce site shared the same domain but had different URL paths:

  • CRM: /crm/...
  • E-commerce: /shop/...

I needed to cache ONLY the CRM files. If I cached everything, the e-commerce site would behave weirdly offline (imagine adding products to a shopping cart when you can't actually check out — confusing UX).

The solution was adding a filter in the Service Worker's fetch event:

self.addEventListener('fetch', (event) => {
  const url = new URL(event.request.url);
  if (!url.pathname.startsWith('/crm')) {
    return;
  }
  event.respondWith(
    caches.match(event.request).then((response) => {
      return response || fetch(event.request);
    })
  );
});

This ensured that only CRM pages were cached and served offline.

Problem #3: Offline Detection and UI Changes

Users needed to KNOW they were offline. I didn't want them to think the website was broken.

I used navigator.onLine to:

  • Show an “Offline Mode” banner at the top of the CRM
  • Disable features that require server communication (like real-time inventory checks)
  • Change button labels (e.g., “Add to Cart” became “Save Locally”)
window.addEventListener('offline', () => {
  document.getElementById('offline-banner').style.display = 'block';
  updateUIForOfflineMode();
});
window.addEventListener('online', () => {
  document.getElementById('offline-banner').style.display = 'none';
  checkForOfflineData();
});

This made the experience transparent. Users knew exactly what was happening.

Problem #4: Storing Cart Data Locally

When offline, users could browse the product catalogue (cached) and add items to their cart. But where do we store this data? Cart data contains sensitive information (product IDs, quantities, pricing). I couldn't just store it in plain text.

So I encrypted it. Basic encryption using Base64 encoding, but enough to prevent casual snooping:

function saveOfflineCart(cartData) {
  const encrypted = btoa(JSON.stringify(cartData));
  localStorage.setItem('offline-cart', encrypted);
}
function loadOfflineCart() {
  const encrypted = localStorage.getItem('offline-cart');
  return encrypted ? JSON.parse(atob(encrypted)) : null;
}

Problem #5: Syncing Data When Back Online

When the user's internet connection returned, I needed to:

  • Detect that they have offline data saved
  • Ask if they want to sync it
  • Send it to the backend if they confirm
  • Clear Local Storage after successful sync
window.addEventListener('online', () => {
  if (localStorage.getItem('offline-cart')) {
    showSyncModal();
  }
});
function syncOfflineData() {
  const cartData = loadOfflineCart();
  fetch('/api/orders', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(cartData)
  })
  .then(response => {
    if (response.ok) {
      localStorage.removeItem('offline-cart');
      alert('Orders synced successfully!');
    }
  })
  .catch(error => {
    alert('Sync failed. Data is still saved locally.');
  });
}

Week 3: Testing, Edge Cases, and Deployment

Implementation was done. Now came the hard part: making sure it actually worked in the real world.

Testing Strategy

  • Chrome DevTools (Network Tab → Offline): primary testing tool
  • Disabling WiFi: powered off WiFi and tested CRM for edge conditions
  • Field Testing: sales rep used CRM in a real no-internet location

Edge Cases

  • Refresh while offline: Service Worker served cached HTML and kept UX working
  • Two devices offline updates: “last write wins” (workaround for conflict handling)
  • Failed sync: keep data in localStorage and allow retry
  • Never online: data stays in localStorage indefinitely

Deployment and Impact

The offline CRM shipped in three weeks and let sales reps:

  • Open CRM in remote locations
  • Browse products from cache
  • Add items to the cart while offline
  • Process orders as soon as they got internet

No more paper notepads. No more transcription errors. No more frustration.

Lessons Learned

1. Constraints breed creativity.

2. User experience > technical perfection.

3. Real-world testing is irreplaceable.

When Should You Build Offline Functionality?

Build it if:

  • Users face connectivity issues
  • Real-time data entry is critical
  • Offline capability is a competitive advantage
  • Data loss would be costly

Don't build it if:

  • App depends on real-time server data
  • Users always have stable internet
  • Complexity outweighs the benefit

Final Thoughts

Three weeks. One impossible-sounding task. A technology I'd never used before. This project was a turning point in my career. If you're a developer facing a hard challenge: break it down, Google aggressively, understand fundamentals, test in real conditions, and accept that building something is still worth it.