← Back to Blog

Route Sharing: Building a URL Shortener for Spatial Navigation

One of EF-Map's most popular features is route sharing: calculate a multi-waypoint route, click "Share," and get a short URL you can send to corpmates. They click the link, and your entire route—waypoints, optimizations, gate preferences—loads instantly in their browser.

Building this required solving several interesting problems: how do we compress complex route data into URLs? How do we generate collision-free short IDs? How do we make sharing work without requiring user accounts? Here's how we built a serverless, privacy-focused route sharing system on Cloudflare.

The Problem: Routes are Too Big for URLs

A typical route in EVE Frontier might include:

{
    "waypoints": [
        "J100422-Komi",
        "J212103-Taru", 
        "J313204-Vela",
        // ... 20 more systems
    ],
    "optimized": true,
    "algorithm": "genetic",
    "avoid_lowsec": false,
    "max_jumps": 50,
    "created_at": "2025-10-15T14:30:00Z"
}

Encoding this as a query string produces URLs like:

https://ef-map.com/?waypoints=J100422,J212103,J313204,...&optimized=true&algorithm=genetic&avoid_lowsec=false&max_jumps=50

For a 20-waypoint route, this can exceed 2000 characters—too long for many messaging apps (Discord, Slack) which truncate or auto-shorten URLs. We needed a compact representation.

Solution: Compression + Short IDs

We built a two-step process:

Step 1: Compress Route Data

Before storing routes, we compress the JSON payload using Gzip compression:

const compressRoute = async (route: RouteData): Promise<string> => {
    const json = JSON.stringify(route);
    const encoder = new TextEncoder();
    const data = encoder.encode(json);
    
    // Gzip compress
    const compressed = await new Response(
        new Blob([data]).stream().pipeThrough(
            new CompressionStream('gzip')
        )
    ).arrayBuffer();
    
    // Base64 encode for safe transmission
    return btoa(String.fromCharCode(...new Uint8Array(compressed)));
};

For a typical 20-waypoint route (~800 bytes JSON), this produces ~200 bytes compressed—a 75% reduction. Base64 encoding adds 33% overhead, resulting in ~270 bytes total.

Step 2: Generate Short IDs

Instead of storing the compressed payload in the URL (still too long), we store it server-side and return a short ID:

const generateShortId = (): string => {
    // 8 characters from base62 (a-zA-Z0-9)
    const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
    let id = '';
    for (let i = 0; i < 8; i++) {
        id += chars[Math.floor(Math.random() * chars.length)];
    }
    return id;
};

Eight base62 characters give us 62^8 = 218 trillion possible IDs—enough to avoid collisions for billions of routes. We also check for duplicates before storing:

const createShare = async (route: RouteData): Promise<string> => {
    const compressed = await compressRoute(route);
    
    let id = generateShortId();
    let exists = await kv.get(`route:${id}`);
    
    // Retry if collision detected (extremely rare)
    while (exists !== null) {
        id = generateShortId();
        exists = await kv.get(`route:${id}`);
    }
    
    await kv.put(`route:${id}`, compressed, {
        expirationTtl: 60 * 60 * 24 * 90 // 90 days
    });
    
    return id;
};

Routes expire after 90 days to prevent unbounded storage growth. Users can re-share if needed.

Step 3: Short URL Generation

The final share URL looks like:

https://ef-map.com/s/aB3xY9Zq

Only 32 characters total—short enough for any messaging app, easy to copy-paste, and clean enough to share on social media.

Cloudflare Worker: Serverless Route Storage

We use Cloudflare KV (key-value storage) for route persistence. A lightweight Worker handles share creation and retrieval:

// _worker.js
export default {
    async fetch(request, env) {
        const url = new URL(request.url);
        
        // Create share: POST /api/create-share
        if (url.pathname === '/api/create-share' && request.method === 'POST') {
            const route = await request.json();
            const compressed = await compressRoute(route);
            const id = await createShareId(env.EF_SHARES);
            
            await env.EF_SHARES.put(`route:${id}`, compressed, {
                expirationTtl: 60 * 60 * 24 * 90
            });
            
            return new Response(JSON.stringify({ id }), {
                headers: { 'Content-Type': 'application/json' }
            });
        }
        
        // Retrieve share: GET /api/get-share/:id
        if (url.pathname.startsWith('/api/get-share/')) {
            const id = url.pathname.split('/').pop();
            const compressed = await env.EF_SHARES.get(`route:${id}`);
            
            if (!compressed) {
                return new Response('Not found', { status: 404 });
            }
            
            const decompressed = await decompressRoute(compressed);
            return new Response(decompressed, {
                headers: { 'Content-Type': 'application/json' }
            });
        }
        
        // Redirect short URLs: GET /s/:id
        if (url.pathname.startsWith('/s/')) {
            const id = url.pathname.split('/').pop();
            return Response.redirect(`/?share=${id}`, 302);
        }
        
        // Serve static site for all other routes
        return env.ASSETS.fetch(request);
    }
};

This Worker runs on Cloudflare's global edge network, so route creation and retrieval happen in <50ms globally. No server provisioning, no database management—just pure serverless architecture.

Frontend Integration: One-Click Sharing

In the React app, sharing is a single button click:

const ShareButton = ({ route }: { route: RouteData }) => {
    const [shareUrl, setShareUrl] = useState<string | null>(null);
    const [loading, setLoading] = useState(false);
    
    const handleShare = async () => {
        setLoading(true);
        
        const response = await fetch('/api/create-share', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(route)
        });
        
        const { id } = await response.json();
        const url = `${window.location.origin}/s/${id}`;
        
        setShareUrl(url);
        setLoading(false);
        
        // Copy to clipboard automatically
        navigator.clipboard.writeText(url);
    };
    
    return (
        <div>
            <button onClick={handleShare} disabled={loading}>
                {loading ? 'Creating share...' : 'Share Route'}
            </button>
            {shareUrl && (
                <div className="share-url">
                    <input value={shareUrl} readOnly />
                    <span className="copy-indicator">✓ Copied to clipboard</span>
                </div>
            )}
        </div>
    );
};

When someone visits /s/aB3xY9Zq, the Worker redirects to /?share=aB3xY9Zq, and the app fetches the route data and loads it:

useEffect(() => {
    const params = new URLSearchParams(window.location.search);
    const shareId = params.get('share');
    
    if (shareId) {
        fetch(`/api/get-share/${shareId}`)
            .then(res => res.json())
            .then(route => {
                loadRoute(route);
                showNotification('Route loaded from share link');
            })
            .catch(() => {
                showError('Share link expired or invalid');
            });
    }
}, []);

The entire flow—click share, copy URL, send to friend, friend clicks, route loads—takes <5 seconds and requires zero account creation or authentication.

Privacy: No Tracking, No Analytics

We designed route sharing to be privacy-first:

This means routes are ephemeral and anonymous. If you share a route publicly (e.g., Reddit post), anyone can view it, but there's no way to trace it back to you. If you want persistent routes, you save them locally in browser storage.

Performance: CDN-Powered Distribution

Cloudflare KV is eventually consistent across their global network. When you create a share in Tokyo, it might take 1-2 seconds to propagate to São Paulo. But once propagated, retrieval is instant from any edge location.

We measured share creation and retrieval latencies:

This is fast enough to feel instant, even for complex 50-waypoint routes.

Lessons for Building Serverless URL Shorteners

Building this feature taught us several principles:

1. Compression matters. Gzip reduced our storage costs by 75% and improved transmission speed.

2. Short IDs scale forever. Eight base62 characters support billions of routes without collisions.

3. Serverless is perfect for ephemeral data. Cloudflare Workers + KV eliminated database management entirely.

4. Privacy builds trust. Not tracking users made sharing feel safe and frictionless.

5. Auto-expiration prevents bloat. 90-day TTLs keep storage bounded without manual cleanup.

Future Enhancements

We're considering several improvements to route sharing: