DEV Community

Cover image for Remote Development Team Building Software for Remote Locations
Richard Sandown
Richard Sandown

Posted on

Remote Development Team Building Software for Remote Locations

The irony, challenges, and unexpected advantages of distributed developers building software for off-grid campgrounds

Our senior frontend developer was pair programming from a cabin in rural Vermont with spotty internet. Our backend lead was debugging API calls from a co-working space in Barcelona. Our designer was reviewing mockups from a coffee shop in Austin. And I was on a video call with all of them, sitting in my home office in Seattle, discussing how to optimize Camprs for campground managers in locations with even worse connectivity than our developer in Vermont.

The irony wasn't lost on us: we're a fully remote team building software specifically designed to work in remote locations where internet is unreliable, power is intermittent, and "going to the office" means walking across a campground.

This unusual situation—remote workers building for remote workers—has taught us valuable lessons about distributed development, offline-first architecture, and understanding users by living similar constraints. This post shares what we've learned about building Camprs as a remote team serving remote locations.

The Meta-Remote Challenge

Most remote development teams build software for users in cities with reliable infrastructure. We're building for campgrounds in places like:

  • National forests where cellular coverage is measured in bars, not megabits
  • Rural areas where "high-speed internet" means DSL if you're lucky
  • Seasonal operations that might lose power during storms
  • Properties spread across hundreds of acres where WiFi doesn't reach everywhere

This creates an unusual development dynamic: our team experiences connectivity challenges that pale in comparison to what our users face, but are still significant enough to inform our technical decisions.

Understanding Through Experience

When our Vermont-based developer's internet drops during a critical deployment, we're reminded viscerally why Camprs needs robust offline capabilities.

When our designer tries to upload mockups over a slow connection, we understand why image optimization isn't optional for campground software.

When pair programming over video becomes impossible due to bandwidth constraints, we appreciate why Camprs can't require constant connectivity for core functionality.

This shared experience of connectivity challenges—even if less severe than our users face—creates empathy that shapes our technical architecture.

Our Distributed Team Structure

Camprs is built by a team of seven developers spread across four time zones and three countries:

Frontend team:

  • Sarah (Vermont, EST) - React/TypeScript, mobile optimization
  • Marcus (Austin, CST) - UI/UX implementation, component library
  • Elena (Barcelona, CET) - Progressive Web App functionality, offline features

Backend team:

  • James (Seattle, PST) - API design, database architecture
  • Priya (Toronto, EST) - Integrations, third-party services
  • Alex (Portland, PST) - DevOps, infrastructure, performance

Design:

  • Jordan (Remote, various locations) - Product design, user research

This distribution isn't theoretical remote work where everyone's actually in the same city but works from home. We're genuinely distributed, which creates both challenges and advantages for building campground software.

Architectural Decisions Driven by Remote Reality

Our distributed team structure directly influences Camprs' technical architecture:

Offline-First by Necessity and Empathy

Building as a remote team with variable connectivity made offline-first architecture not just a user requirement but a personal necessity.

Service Worker implementation:

// Our service worker handles offline scenarios we've personally experienced
const CACHE_VERSION = 'camprs-v1.2.3';
const CORE_CACHE = [
  '/',
  '/schedule',
  '/bookings',
  '/staff',
  '/manifest.json',
  '/offline.html'
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_VERSION).then((cache) => {
      // Aggressively cache core functionality
      // We know from experience what users need when offline
      return cache.addAll(CORE_CACHE);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    // Network first for API calls (fresher data when available)
    fetch(event.request)
      .then((response) => {
        // Clone response for cache storage
        const responseClone = response.clone();
        caches.open(CACHE_VERSION).then((cache) => {
          cache.put(event.request, responseClone);
        });
        return response;
      })
      .catch(() => {
        // Fall back to cache when network fails
        // This is the scenario we personally hit multiple times weekly
        return caches.match(event.request).then((cachedResponse) => {
          if (cachedResponse) {
            return cachedResponse;
          }
          // Return offline page as last resort
          return caches.match('/offline.html');
        });
      })
  );
});
Enter fullscreen mode Exit fullscreen mode

This isn't theoretical offline support—it's based on real experiences of developers trying to work with unreliable connections.

Async Communication by Design

Remote teams communicate asynchronously by necessity. This directly influenced how we built Camprs:

Event-driven architecture:

// Changes propagate through events rather than requiring real-time sync
class BookingEventEmitter {
  constructor() {
    this.events = {};
    this.eventQueue = []; // Queue events when offline
  }

  emit(eventType, data) {
    // Try to send immediately if online
    if (navigator.onLine) {
      this.sendToServer(eventType, data);
    } else {
      // Queue for later if offline
      this.eventQueue.push({ eventType, data, timestamp: Date.now() });
      this.persistQueue(); // Save to IndexedDB
    }

    // Trigger local event handlers immediately
    if (this.events[eventType]) {
      this.events[eventType].forEach(callback => callback(data));
    }
  }

  async syncQueuedEvents() {
    // When connection returns, sync queued events
    while (this.eventQueue.length > 0 && navigator.onLine) {
      const event = this.eventQueue[0];
      try {
        await this.sendToServer(event.eventType, event.data);
        this.eventQueue.shift();
      } catch (error) {
        console.error('Failed to sync event', error);
        break; // Stop syncing if we hit an error
      }
    }
    this.persistQueue();
  }
}
Enter fullscreen mode Exit fullscreen mode

This event-driven approach emerged from our team's experience working asynchronously across time zones. Messages don't require immediate responses; actions queue and process when possible.

Documentation as First-Class Concern

Remote teams live or die by documentation. This influenced how we document Camprs:

Comprehensive API documentation:

# OpenAPI spec for every endpoint
/api/bookings:
  post:
    summary: Create new booking
    description: |
      Creates a new campground booking. Can be called offline - booking
      will queue and process when connection returns.

      Offline behavior:
      - Returns 202 Accepted immediately
      - Queues booking for sync when online
      - Updates local state optimistically
      - Syncs with server when connection available

      Common issues:
      - Site availability checked against cached data when offline
      - Conflicts resolved server-side (first booking wins)
      - Failed bookings trigger notifications to campground manager
    parameters:
      - name: site_id
        required: true
        description: Campsite identifier
      - name: check_in
        required: true
        description: Check-in date (YYYY-MM-DD)
      - name: check_out
        required: true
        description: Check-out date (YYYY-MM-DD)
Enter fullscreen mode Exit fullscreen mode

Every endpoint is documented with offline behavior explicitly described—because we've personally struggled with undocumented offline behavior in tools we use.

Code documentation standards:

/**
 * Handles booking creation with offline support
 * 
 * @param {Object} bookingData - Booking details
 * @param {string} bookingData.siteId - Campsite ID
 * @param {Date} bookingData.checkIn - Check-in date
 * @param {Date} bookingData.checkOut - Check-out date
 * 
 * @returns {Promise<Booking>} Created booking
 * 
 * @offline Queues booking for later sync
 * @throws {ValidationError} If booking data invalid
 * @throws {ConflictError} If site unavailable (online only)
 * 
 * @example
 * // Basic booking creation
 * const booking = await createBooking({
 *   siteId: 'site-123',
 *   checkIn: new Date('2024-06-15'),
 *   checkOut: new Date('2024-06-17')
 * });
 * 
 * @example
 * // Handling offline scenario
 * try {
 *   const booking = await createBooking(bookingData);
 *   if (booking.status === 'queued') {
 *     showMessage('Booking saved. Will confirm when online.');
 *   }
 * } catch (error) {
 *   handleBookingError(error);
 * }
 */
async function createBooking(bookingData) {
  // Implementation
}
Enter fullscreen mode Exit fullscreen mode

Detailed documentation isn't optional when you can't just tap someone on the shoulder to ask how something works.

Development Practices Shaped by Distribution

Our remote nature influenced specific development practices:

Async Code Reviews

Pull requests can't rely on real-time discussion. We've developed patterns for effective async reviews:

## Pull Request Template

### What This Changes
Clear description of what's changing and why

### Technical Approach
Explain key technical decisions, especially around offline handling

### Testing
- [ ] Tested offline functionality
- [ ] Tested on slow connection (throttled)
- [ ] Tested cross-browser (Chrome, Safari, Firefox)
- [ ] Tested on mobile devices

### Offline Behavior
Explicit description of how this works offline:
- What's cached?
- What queues for later sync?
- How are conflicts handled?

### Questions/Concerns
Things you're uncertain about or want specific feedback on

### Screenshots/Video
Visual context helps async reviewers
Enter fullscreen mode Exit fullscreen mode

Reviewers can understand context without synchronous communication.

Timezone-Aware Development Flow

With team members in EST, CST, PST, and CET, we structure work to minimize timezone friction:

Handoff-oriented workflow:

Morning (PST):
- Seattle team reviews overnight work from Barcelona
- Portland deploys changes
- Code reviews for EST team members

Afternoon (PST):
- Overlap time with EST team (meetings if needed)
- Pair programming when beneficial
- Deploy features ready for testing

Evening (PST):
- Prepare handoff notes for Barcelona team
- Document any blockers
- Queue reviews for next morning
Enter fullscreen mode Exit fullscreen mode

This creates a "follow-the-sun" development pattern where work progresses around the clock—similar to how Camprs needs to work for campgrounds operating across time zones.

Remote Pairing with Poor Connectivity

Traditional pair programming assumes good connectivity. We've adapted:

Async pairing through PRs:

## Pairing Session: Offline Booking Queue

@sarah starting the implementation. Approach:

1. IndexedDB for queue storage
2. Background sync API for processing
3. Conflict resolution on server side

Pushed initial implementation in commit abc123.

@marcus can you review the conflict resolution logic?
Specifically concerned about race conditions when
multiple devices sync simultaneously.

---

@marcus reviewed. Thoughts:

The race condition is real. Suggestion: add timestamp
and device_id to queued items. Server picks first by
timestamp, rejects duplicates.

See comments in code for specific suggestions.

---

@sarah implemented your suggestions in commit def456.
Added integration tests for race condition scenarios.

Ready for your final review before merge.
Enter fullscreen mode Exit fullscreen mode

This async pairing lacks the immediacy of real-time collaboration but creates valuable documentation of decision-making.

Testing in Realistic Conditions

Our distributed team naturally tests in varied network conditions:

Chrome DevTools network throttling:

// Testing scenarios we personally encounter
const networkProfiles = {
  'Vermont DSL': {
    downloadThroughput: 1.5 * 1024 * 1024 / 8, // 1.5 Mbps
    uploadThroughput: 384 * 1024 / 8, // 384 Kbps
    latency: 150
  },
  'Rural Campground': {
    downloadThroughput: 512 * 1024 / 8, // 512 Kbps
    uploadThroughput: 128 * 1024 / 8, // 128 Kbps
    latency: 300
  },
  'Spotty Cellular': {
    downloadThroughput: 750 * 1024 / 8,
    uploadThroughput: 250 * 1024 / 8,
    latency: 200,
    // Simulate intermittent disconnections
    disconnectProbability: 0.15
  }
};
Enter fullscreen mode Exit fullscreen mode

Team members regularly test Camprs under conditions similar to what they personally experience—and what campground managers face.

Advantages of Remote Team for Remote Software

Building remotely has unexpected advantages for our specific product:

We Experience Similar Constraints

When our designer works from cafes with variable WiFi, they understand why Camprs needs to handle connection drops gracefully.

When our developers work from home offices during power outages on battery, they appreciate why Camprs minimizes battery drain.

When our team does video calls on limited bandwidth, we're reminded why Camprs can't require constant streaming data.

Geographic Distribution Matches User Distribution

Our team spread across North America and Europe mirrors campground distribution. We're not building from a Silicon Valley bubble—we're distributed like our users.

Variable Schedules Match Seasonal Patterns

Remote work enables flexible schedules. Team members working at odd hours understand why campground managers need access to Camprs at 2 AM during emergencies or 6 AM before busy weekends.

Understanding Infrastructure Limitations

Living with infrastructure constraints that don't match urban tech worker experience helps us build with empathy for campground operators facing similar (or worse) constraints.

Challenges We Still Face

Remote development for remote locations isn't without difficulties:

Testing Truly Remote Scenarios

Even with throttling tools, we can't perfectly simulate:

  • Completely offline for days (like seasonal campgrounds closing for winter)
  • Intermittent power affecting device charging
  • Multiple devices syncing after long offline periods
  • Edge cases in truly remote locations

Our solution: Regular field visits to actual campgrounds to use Camprs in real conditions and gather feedback from users experiencing genuine remote scenarios.

Coordination Across Time Zones

With team members spanning 9 hours of time zones, finding overlap time for synchronous collaboration requires careful planning.

Our solution: Core overlap hours (1-3 PM PST / 4-6 PM EST / 10 PM-12 AM CET) for necessary meetings, with heavy reliance on async communication otherwise.

Cultural and Communication Differences

Remote teams amplify communication challenges. What's direct in one culture is rude in another. Text lacks tone and body language cues.

Our solution: Over-communicate context. Use video for sensitive discussions. Document communication norms explicitly. Build relationships through virtual social time.

Onboarding Remote Developers

Getting new team members up to speed without in-person shadowing requires extra effort.

Our solution: Comprehensive onboarding documentation, pair programming via screen sharing, regular check-ins during first month, buddy system for questions.

Tools That Enable Our Remote Team

Specific tools make our distributed development possible:

Communication

Slack: Async communication for most discussions

  • Channels organized by feature area
  • Expectations about response times
  • Important decisions documented in threads

Zoom: Synchronous communication when needed

  • Weekly all-hands
  • Pair programming sessions
  • Design reviews requiring discussion

Loom: Async video for complex explanations

  • Code walkthroughs
  • Bug reproductions
  • Feature demonstrations

Code Collaboration

GitHub: Central code repository

  • Comprehensive PR templates
  • Automated checks before merging
  • Integration with project management

Linear: Issue tracking and project management

  • Clear issue descriptions
  • Async updates on progress
  • Roadmap visibility

Notion: Documentation and knowledge sharing

  • Architecture decisions recorded
  • Onboarding documentation
  • Meeting notes archived

Development Environment

Docker: Consistent environments across machines

# Everyone runs the same development environment
FROM node:18-alpine

WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm ci

# Copy application
COPY . .

# Expose port
EXPOSE 3000

CMD ["npm", "run", "dev"]
Enter fullscreen mode Exit fullscreen mode

VS Code Live Share: Real-time collaboration when pair programming

Postman: Shared API collections for consistent testing

Building Features for Remote Users, Remotely

Specific Camprs features emerged from our remote development experience:

Offline-First Booking Management

Because we've all tried to book things with poor connections:

// Optimistic booking creation
async function createBookingOfflineFirst(bookingData) {
  // Create local booking immediately
  const localBooking = {
    ...bookingData,
    id: generateLocalId(),
    status: 'pending_sync',
    created_offline: !navigator.onLine,
    created_at: new Date().toISOString()
  };

  // Save to IndexedDB
  await saveToLocalDB('bookings', localBooking);

  // Update UI immediately
  updateUIOptimistically(localBooking);

  // Try to sync with server
  if (navigator.onLine) {
    try {
      const serverBooking = await syncBookingToServer(localBooking);
      await updateLocalDB('bookings', serverBooking);
      return serverBooking;
    } catch (error) {
      // Server sync failed, keep local version
      console.warn('Booking saved locally, will sync later');
      return localBooking;
    }
  }

  return localBooking;
}
Enter fullscreen mode Exit fullscreen mode

Conflict Resolution UI

Because we've experienced sync conflicts in our own tools:

// Show users when conflicts occur and let them resolve
function showConflictResolution(localVersion, serverVersion) {
  return (
    <ConflictModal>
      <h2>This booking was modified while offline</h2>

      <ComparisonView>
        <Version label="Your Changes">
          <BookingDetails booking={localVersion} />
        </Version>

        <Version label="Current Version">
          <BookingDetails booking={serverVersion} />
        </Version>
      </ComparisonView>

      <Actions>
        <Button onClick={() => resolveConflict('keep_local')}>
          Keep My Changes
        </Button>
        <Button onClick={() => resolveConflict('keep_server')}>
          Use Current Version
        </Button>
        <Button onClick={() => resolveConflict('manual')}>
          Merge Manually
        </Button>
      </Actions>
    </ConflictModal>
  );
}
Enter fullscreen mode Exit fullscreen mode

Sync Status Transparency

Because we hate when tools don't tell us what's happening:

// Always show sync status
function SyncIndicator({ syncStatus }) {
  const indicators = {
    synced: {
      icon: '',
      color: 'green',
      message: 'All changes synced'
    },
    syncing: {
      icon: '',
      color: 'blue',
      message: 'Syncing changes...'
    },
    offline: {
      icon: '',
      color: 'orange',
      message: 'Working offline - will sync when online'
    },
    error: {
      icon: '!',
      color: 'red',
      message: 'Sync error - tap to retry'
    }
  };

  const current = indicators[syncStatus];

  return (
    <StatusBadge color={current.color}>
      <Icon>{current.icon}</Icon>
      <Message>{current.message}</Message>
    </StatusBadge>
  );
}
Enter fullscreen mode Exit fullscreen mode

Lessons for Other Remote Teams

If you're building software remotely—especially for users in remote locations—here's what we've learned:

Embrace Your Constraints as Features

The limitations your team faces (connectivity, timezone distribution, async communication) can inform better product decisions for users facing similar constraints.

Document Everything

Remote teams need documentation. So do remote users. The documentation you create for your team often translates directly into user documentation.

Test in Realistic Conditions

Simulate the conditions your users face. If they have poor connectivity, test with poor connectivity. If they go offline for days, test those scenarios.

Build Async-First

Whether for your team's workflow or your users' experience, async-first architecture creates resilience and flexibility.

Use Your Pain Points

When your tools frustrate you, that's a signal. If your video calls drop, your users' requests will drop. If your connection is slow, theirs is slower. Build solutions that would make your own experience better.

The Bottom Line

Being a remote team building software for remote locations creates interesting dynamics—we're distant from each other but close to our users' experience in unexpected ways.

The connectivity challenges we face as a distributed team inform how we build Camprs. The async communication patterns we've developed for our workflow translate into event-driven architecture. The documentation we create for ourselves becomes user documentation.

Most importantly, experiencing infrastructure limitations firsthand—even if less severe than our users face—creates empathy that shapes technical decisions. We're not building from theory about what remote locations need. We're building from experience with some of the same challenges.

The irony of remote developers building for remote locations isn't just amusing, it's a genuine advantage. Our distributed team is better positioned to serve distributed campgrounds than a co-located team in a well-connected urban office could ever be.

Sometimes the best qualification for solving a problem is experiencing a version of it yourself.


Building software remotely? Serving users in remote locations? We'd love to hear about your experiences, challenges, and solutions. Drop a comment or reach out, remote workers should stick together.

Top comments (0)