Base URL: https://sport-link-production.up.railway.app
All /api/* routes require Authorization: Bearer <JWT> unless marked public or auth-optional.
Create a new account.
Body: { username, email, password }
Returns: { token, user }
Email + password login.
Body: { email, password }
Returns: { token, user }
Google OAuth login/register via PKCE.
Body: { access_token } (Google OAuth access token from exchangeCodeAsync)
Returns: { token, user }. New users have onboarding_complete: false.
All upcoming active games. Params:
lat,lng— Haversine centerradius_km— filter (optional)q— full-text search ontitle+location_desc(≥2 chars, FULLTEXT for ≥3)
Each game includes is_joined: boolean (false for unauthenticated).
Games where the caller is host or participant (includes past games).
Create a game.
Body: { sport_type, level, latitude, longitude, location_desc, scheduled_time, equipment_notes?, photo?, max_players?, title?, recurrence?, invited_friends? }
recurrence must be 'none' | 'weekly' | 'biweekly'.
Edit a game (host only). Same fields as POST except no lat/lng.
Cancel a game (host only). Sets status='cancelled', notifies participants.
Join a game (transaction + SELECT FOR UPDATE).
Leave a game (participant only).
List all participants. Returns [{ id, username, avatar, is_host }].
User's game chats with last message preview.
Paginated messages. Returns up to limit (max 50) messages before before ID, ordered DESC (newest first). Default limit: 30.
Post a message. Body: { content }.
Current user's full profile: { id, username, bio, avatar, games_hosted, games_joined, karma, top_sport, sport_preferences }.
Update profile. Body (all optional): { username, bio, avatar, onboarding_complete }. Dynamic SET.
Username prefix search (min 2 chars), excludes self. Returns [{ id, username, avatar }].
Batch avatar fetch. Returns [{ id, avatar }].
Top 20 users by karma.
Save or clear Expo push token. Body: { push_token }.
Current user's sport preferences array.
Upsert preferences. Body: { preferences: [{ sport_type, skill_level, is_favorite }] }. Delete-all + re-insert in transaction.
Player matching. Returns suggested non-friends ranked by shared_count DESC, shared_game_count DESC, karma DESC. shared_game_count = completed games together.
Public profile. Returns { ..., friendship_status, friendship_id }.
Accepted friends list with karma.
Incoming pending requests.
Send friend request. Body: { addressee_id }. Sends push notification.
Accept request by Friends row id. Sends push notification to requester.
Remove friend or reject/cancel request.
Nearby courts via Google Places (6 parallel queries — Hebrew/English court keywords, gym, yoga/studio, padel, swimming pools) or mock fallback. detectSportType() classifies each result into one of the 12 sports by name pattern; note the footvolley check must run before the football check since Hebrew footvolley names ("כדורגל חוף") contain the football substring.
Proxy for Google Places photos. Streams image with 24h cache header.
Court detail: Google Places info + SportLink aggregate + reviews.
Returns: { places, review_count, avg_rating, reviews, claimed_by, is_manager }.
claimed_by: { id, username, avatar } | null. is_manager: boolean.
Full review list (includes owner_response, owner_response_at).
Submit or update own review. Body: { rating (1–5), comment? }. One per user per court.
Delete own review.
Manager adds/updates response. Body: { response }. Guards: caller must be court manager.
Manager removes their response.
Claim court as manager (first-come, one per court). Returns 409 if already claimed.
Release claim (manager only).
Returns { can_view, results }. can_view: false if caller has unrated players. Results: per-player { attended, peer_count, sportsmanship_pct, punctuality_pct, communication_pct, skill_avg }. Anonymous.
Returns { is_host, players } — still-unrated players for the caller.
Host submits attendance. Body: { game_id, ratings: [{ ratee_id, attended }] }.
Non-host submits peer ratings. Body: { game_id, ratings: [{ ratee_id, sportsmanship, punctuality, communication, skill }] }.
Returns { notifications, unread_count }. Last 50.
Mark single notification read.
Mark all notifications read.
DM conversation list: [{ id, username, avatar, last_content, last_type, last_event_id, last_sender_id, last_time, unread_count }].
Fetch up to 100 messages with that user (ASC). Auto-marks received messages as read.
Send message. Body: { content, type?, event_id? }. type='event' requires event_id. Emits new_dm socket event to receiver's user_${id} room.
Mark all messages from that user as read.
Friend activity feed. Returns last 50 events from friends: [{ type: 'joined'|'created', actor_id, actor_username, actor_avatar, game_id, game_title, game_sport_type, game_location_desc, happened_at }].
| Route | Description |
|---|---|
GET /game/:id |
Game share landing page with OG meta + sportlink://game/:id deep link |
GET /invite/:userId |
User invite landing page with sportlink://invite/:userId deep link |
GET /privacy |
Privacy policy |
GET /terms |
Terms of service |
GET /health |
{ status: 'OK' } |
Connect with { auth: { token } }.
| Event | Payload | Description |
|---|---|---|
join_game |
gameId |
Join room game_${id} (validates participation) |
send_message |
{ gameId, content } |
Send chat message; broadcasts new_message to room |
| Event | Payload | Sent to |
|---|---|---|
new_message |
{ id, user_id, username, content, created_at, avatar } |
Room game_${gameId} |
new_dm |
{ id, sender_id, receiver_id, content, type, event_id, is_read, created_at, ... } |
Room user_${receiverId} |
On connection, each user is automatically joined to their personal room user_${id}.
-- From utils/karmaSQL.js (KARMA_SQL constant)
(
SELECT COALESCE(SUM(CASE WHEN attended = 1 THEN 1 ELSE -1 END), 0)
FROM Ratings WHERE ratee_id = u.id
)
+ (
SELECT COALESCE(SUM(sportsmanship + punctuality + communication + (skill >= 3)), 0)
FROM PeerRatings WHERE ratee_id = u.id
)Host rates attendance (+1 / -1). Peers rate sportsmanship, punctuality, communication (thumb up = +1) and skill ≥ 3 = +1.
| Scope | Limit |
|---|---|
/api/auth/* |
10 requests / 60s |
All other /api/* |
300 requests / 60s |