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 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.
| 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: 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_idandredirect_uri. -
Authenticate with your Foursquare account and authorise the app.
-
Foursquare redirects back to
https://swarm.jeeves5454.ddns.net/callbackwith 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:
| 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:
- Visit
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:
| 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
-
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/statusand checkconnectedUsers > 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)
- Ensure
FOURSQUARE_REDIRECT_URIin 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