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');
});
})
);
});
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();
}
}
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)
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
}
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
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
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.
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
}
};
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"]
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;
}
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>
);
}
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>
);
}
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)