Skip to main content

13-swarm-reitti-bridge.md

kstack: book: Centerpoint Home Lab chapter: Documents & Organization page: Swarm-Reitti Bridge tags: [swarm-reitti-bridge, foursquare, reitti, location, custom-built, node]

Overview

Swarm-Reitti Bridge is a custom-built Node.js service that automatically syncs Foursquare/Swarm check-ins into Reitti. It was built specifically for this homelab as a bridge between the Foursquare API and Reitti's OwnTracks-compatible ingestion endpoint — keeping all location history self-hosted.

The service runs at https://swarm.jeeves5454.ddns.net and is externally reachable so the OAuth callback from Foursquare can complete.

This service was built in this homelab environment. Its source code lives at /home/jeeves/docker/swarmreitti/swarm-reitti-FINAL-POLLING/ and the production build at /home/jeeves/docker/swarmreitti/swarm-reitti-bridge/. The container image (swarm-reitti-bridge-swarm-bridge) is built locally via docker compose build and is not published to any registry.


Background: Why Polling Mode?

This bridge was originally designed to use Foursquare push notifications — Foursquare would POST to the bridge's /push endpoint on every check-in, enabling near-instant syncing. That architecture worked correctly.

In late 2024 / early 2025, Foursquare changed their API pricing and moved push notifications behind a paid credit system. Attempting to subscribe to push notifications returns:

{ "errorType": "credits_exhausted", "code": 402 }

The bridge was updated to polling mode as the solution. Instead of waiting for Foursquare to push, it actively polls the Foursquare API every 15 minutes for new check-ins.

Aspect Push (legacy, broken) Polling (current)
Trigger Foursquare → Bridge (instant) Bridge → Foursquare (timer)
Latency < 1 second Up to 15 minutes
API credits required Yes (paid) No (free tier)
API calls/day ~0 idle 96 (10% of free quota)
Cost Requires payment $0.00

The 15-minute maximum latency is entirely acceptable for personal location history tracking.


How It Works

High-Level Flow

User checks in on Swarm
         ↓
Foursquare API stores check-in
         ↓
Bridge polls every 15 minutes
GET /v2/users/self/checkins?oauth_token=...
         ↓
Bridge identifies new check-ins (deduplication via lastCheckinIds)
         ↓
Bridge converts to OwnTracks format
         ↓
POST to Reitti OwnTracks ingest API
Authorization: Bearer <REITTI_API_TOKEN>
         ↓
Check-in appears on Reitti map

Deduplication

The bridge tracks lastCheckinIds per user — a map of userId → most recently processed check-in ID. On each poll, only check-ins newer than the last known ID are forwarded to Reitti. This state is persisted to disk (/app/data/state.json) so it survives container restarts.

State Persistence

Three state objects are saved to /app/data/state.json:

Key Contents
userTokens OAuth access tokens per userId (from Foursquare)
lastCheckinTimestamps Last seen check-in timestamp per userId
lastCheckinIds Last processed check-in ID per userId

Writes are atomic (write to .tmp, then rename) to avoid corruption on crash.


Data Mapping

The bridge converts Foursquare check-in JSON to Reitti's OwnTracks format:

OwnTracks Field Source Notes
_type Hardcoded: "location" OwnTracks type identifier
lat checkin.venue.location.lat Venue latitude
lon checkin.venue.location.lng Venue longitude (note: lnglon)
tst checkin.createdAt Unix timestamp of check-in
tid First 2 characters of username, uppercased OwnTracks tracker ID
acc Hardcoded: 10 Accuracy in metres (Foursquare doesn't provide GPS accuracy)
alt Hardcoded: 0 Altitude (not available from Foursquare)
batt Hardcoded: 100 Battery (not applicable)
vel Hardcoded: 0 Velocity (not applicable)
t Hardcoded: "c" OwnTracks trigger type: check-in
desc checkin.venue.name (max 200 chars) Venue name
addr Address + City + State + Country (max 300 chars) Formatted address string

The payload is sent as:

POST <REITTI_API_URL>
Authorization: Bearer <REITTI_API_TOKEN>
Content-Type: application/json

OAuth Setup

Foursquare OAuth 2.0 is used to grant the bridge permission to read check-in data on behalf of the user. This is a one-time setup that persists in the state file.

Initial Setup Steps

  1. Visit the auth URL:

    https://swarm.jeeves5454.ddns.net/auth
    
  2. The bridge redirects to Foursquare with the configured client_id and redirect_uri.

  3. Authenticate with your Foursquare account and authorise the app.

  4. Foursquare redirects back to https://swarm.jeeves5454.ddns.net/callback with an authorisation code.

  5. The bridge exchanges the code for an access token and stores it in the state file. Polling begins automatically.

Foursquare App Configuration

The Foursquare developer app must have these settings:

Field Value
Redirect URI https://swarm.jeeves5454.ddns.net/callback
Push API URL (not required in polling mode — was /push)

If the OAuth flow breaks, re-authenticate by visiting /auth. The new token replaces the stored one.


Access and Endpoints

External URL: https://swarm.jeeves5454.ddns.net
Certificate: Let's Encrypt (letsencrypt resolver)
Port: 3000 (internal)
Auth middleware: None — OAuth is handled internally

API Endpoints

Method Path Purpose
GET / Status dashboard web UI
GET /auth Start Foursquare OAuth flow
GET /callback OAuth callback (Foursquare redirects here)
POST /push Legacy webhook endpoint (inactive in polling mode)
GET /health Health check — returns 200 OK with JSON status
GET /status JSON status: polling interval, user count, last poll

The /auth endpoint is rate-limited (10 requests per 15-minute window) to prevent OAuth abuse.


Configuration

Environment Variables

Variable Value / Notes
FOURSQUARE_CLIENT_ID REDACTED (from Foursquare developer console)
FOURSQUARE_CLIENT_SECRET REDACTED
FOURSQUARE_REDIRECT_URI https://swarm.jeeves5454.ddns.net/callback
REITTI_API_URL https://reitti.jeeves5454.ddns.net/api/v1/ingest/owntracks
REITTI_API_TOKEN REDACTED (Reitti API token from Reitti Settings)
PUSH_SECRET REDACTED (validates legacy incoming webhooks)
POLLING_INTERVAL_MINUTES 15 (default)
PORT 3000
NODE_ENV production

Traefik Labels

traefik.http.routers.swarm-bridge.rule: Host(`swarm.jeeves5454.ddns.net`)
traefik.http.routers.swarm-bridge.entrypoints: websecure
traefik.http.routers.swarm-bridge.tls.certresolver: letsencrypt
traefik.http.services.swarm-bridge.loadbalancer.server.port: 3000

Volumes / Bind Mounts

Host Path Container Path Purpose
/home/jeeves/docker/swarmreitti/swarm-reitti-bridge/data /app/data State file (state.json) persisted across restarts

Logging

The bridge uses structured JSON logging to stdout (Dozzle-compatible):

{"ts":"2026-06-17T12:00:00.000Z","level":"INFO","cat":"POLL","msg":"Scheduled poll","userCount":1}
{"ts":"2026-06-17T12:00:01.000Z","level":"INFO","cat":"CHECKIN","msg":"Processing","venue":"Tim Hortons","lat":43.65,"lon":-79.38}
{"ts":"2026-06-17T12:00:02.000Z","level":"INFO","cat":"CHECKIN","msg":"Sent to Reitti","venue":"Tim Hortons"}
{"ts":"2026-06-17T12:00:02.000Z","level":"INFO","cat":"STATE","msg":"State saved","usersCount":1}

Log categories: POLL, CHECKIN, STATE, AUTH, ERROR.

View live logs via Dozzle at https://logs.home.local or:

docker logs -f swarm-reitti-bridge

Operations and Management

Verify the Bridge is Running

# Container status
docker ps | grep swarm-reitti-bridge

# Health check
curl -s https://swarm.jeeves5454.ddns.net/health | jq .

# JSON status
curl -s https://swarm.jeeves5454.ddns.net/status | jq .

Re-authenticate with Foursquare

If the OAuth token expires or becomes invalid:

  1. Visit https://swarm.jeeves5454.ddns.net/auth
  2. Log in with your Foursquare credentials
  3. Authorise the app
  4. The bridge saves the new token automatically
  5. Polling resumes within the next 15-minute cycle

Adjust Polling Interval

Edit the container environment via Portainer (or the compose file) and change POLLING_INTERVAL_MINUTES. Rebuild/restart the container for the change to take effect. Guidance:

Interval API calls/day Use case
5 288 More responsive (30% quota)
15 96 Recommended (10% quota)
30 48 Most conservative (5% quota)

Rebuild the Container

The image is built locally — there is no upstream registry to pull from:

cd /home/jeeves/docker/swarmreitti/swarm-reitti-bridge
docker compose down
docker compose build --no-cache
docker compose up -d
docker compose logs -f

Check State File

cat /home/jeeves/docker/swarmreitti/swarm-reitti-bridge/data/state.json | jq .

The state file contains OAuth tokens. Do not expose or commit this file.


Troubleshooting

Check-ins not appearing in Reitti

  1. Check bridge logs for errors:

    docker logs swarm-reitti-bridge | grep -E "ERROR|CHECKIN"
    
  2. Verify Reitti API connectivity:

    curl -s -o /dev/null -w "%{http_code}" \
      https://reitti.jeeves5454.ddns.net/api/v1/ingest/owntracks
    
  3. Verify OAuth token is stored — visit https://swarm.jeeves5454.ddns.net/status and check connectedUsers > 0. If 0, re-authenticate via /auth.

  4. Check Foursquare API access manually (run on host — substitute real token):

    curl "https://api.foursquare.com/v2/users/self/checkins?oauth_token=TOKEN&limit=5&v=20240101"
    

OAuth callback fails (redirect URI mismatch)

  • Ensure FOURSQUARE_REDIRECT_URI in env exactly matches the redirect URI registered in the Foursquare developer console.
  • The value must be: https://swarm.jeeves5454.ddns.net/callback

Bridge container exits immediately

  • Check logs: docker logs swarm-reitti-bridge
  • Most common cause: missing required environment variable. The app logs all missing vars and exits with code 1.

State file corruption

If state.json is corrupted (e.g., write interrupted on crash):

# The bridge will start fresh (no users connected)
rm /home/jeeves/docker/swarmreitti/swarm-reitti-bridge/data/state.json
docker restart swarm-reitti-bridge
# Then re-authenticate via /auth

Source Code

The production build directory:

/home/jeeves/docker/swarmreitti/swarm-reitti-bridge/
├── index.js           ← Main application (polling mode, production version)
├── Dockerfile
├── docker-compose.yml
├── package.json
├── node_modules/
├── tests/
└── data/
    └── state.json     ← Runtime state (OAuth tokens, last check-in IDs)

The development/reference archive (with all documentation files):

/home/jeeves/docker/swarmreitti/swarm-reitti-FINAL-POLLING/
├── index.js           ← Same polling implementation
├── index-polling.js   ← Polling version (legacy filename)
├── index-secure.js    ← Original push-mode version (for reference only)
├── SOLUTION-SUMMARY.md
├── POLLING-MODE-EXPLAINED.md
├── TROUBLESHOOTING.md
└── ...

Last Updated: 2026-06-17