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
emptySwarm-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 viadocker compose buildand 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.
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:
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:
_type
Hardcoded: "location"
OwnTracks type identifier
lat
checkin.venue.location.lat
Venue latitude
lon
checkin.venue.location.lng
Venue longitude (note: lng → lon)
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
Visit the auth URL:
https://swarm.jeeves5454.ddns.net/auth
The bridge redirects to Foursquare with the configured client_id and
redirect_uri.
Authenticate with your Foursquare account and authorise the app.
Foursquare redirects back to https://swarm.jeeves5454.ddns.net/callback
with an authorisation code.
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:
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
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
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
/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:
https://swarm.jeeves5454.ddns.net/auth
Log in with your Foursquare credentials
Authorise the app
The bridge saves the new token automatically
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:
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
Check bridge logs for errors:
docker logs swarm-reitti-bridge | grep -E "ERROR|CHECKIN"
Verify Reitti API connectivity:
curl -s -o /dev/null -w "%{http_code}" \
https://reitti.jeeves5454.ddns.net/api/v1/ingest/owntracks
Verify OAuth token is stored — visit
https://swarm.jeeves5454.ddns.net/status and check connectedUsers > 0.
If 0, re-authenticate via /auth.
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)
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
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