Commit 81460cca authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add complete API documentation

Covers all endpoints, request/response schemas, enums, FIDE pairing
engine, tiebreaks, realtime, lifecycle, RBAC, cURL examples, and
TypeScript SDK example.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent 76ec5dc3
Pipeline #40 failed with stages
# Swiss System Tournament API — Complete Documentation
**Base URL:** `https://swissapi.caprover.al-arcade.com`
**API Prefix:** `/api/v1`
**Version:** 1.0.0
---
## Table of Contents
1. [Overview](#overview)
2. [Authentication](#authentication)
3. [Error Handling](#error-handling)
4. [Rate Limiting](#rate-limiting)
5. [Endpoints](#endpoints)
- [Health & Root](#health--root)
- [Auth](#auth)
- [Organizations](#organizations)
- [Events](#events)
- [Tournaments](#tournaments)
- [Players](#players)
- [Rounds](#rounds)
- [Pairings](#pairings)
- [Standings](#standings)
- [Categories](#categories)
- [Export](#export)
6. [Enums & Types](#enums--types)
7. [FIDE Dutch Pairing Engine](#fide-dutch-pairing-engine)
8. [Tiebreak Systems](#tiebreak-systems)
9. [Realtime WebSocket](#realtime-websocket)
10. [Tournament Lifecycle](#tournament-lifecycle)
11. [Multi-Tenancy & RBAC](#multi-tenancy--rbac)
---
## Overview
A full SaaS multi-tenant REST API for managing FIDE-compliant Swiss System chess tournaments. Features:
- Full FIDE Dutch System (C.04) pairing engine
- 11 tiebreak calculators
- Elo rating calculations with K-factor
- Multi-organization support with role-based access
- Realtime updates via Supabase WebSocket
- FIDE TRF export format
- Crosstable generation
- Bulk player import
- Automatic standings recalculation
---
## Authentication
All authenticated endpoints require a JWT Bearer token in the `Authorization` header:
```
Authorization: Bearer <access_token>
```
Tokens are obtained via `/api/v1/auth/login` or `/api/v1/auth/signup`. Refresh expired tokens via `/api/v1/auth/refresh`.
The JWT contains:
- `sub` — User UUID
- `role` — Supabase role
- `exp` — Expiration timestamp
---
## Error Handling
All errors follow this format:
```json
{
"statusCode": 400,
"message": "Human-readable error description",
"code": "MACHINE_READABLE_CODE"
}
```
### Error Codes
| Code | HTTP Status | Description |
|------|-------------|-------------|
| `VALIDATION_ERROR` | 400 | Request body/params failed validation |
| `UNAUTHORIZED` | 401 | Missing or invalid JWT token |
| `FORBIDDEN` | 403 | Insufficient role/permissions |
| `NOT_FOUND` | 404 | Resource does not exist |
| `CONFLICT` | 409 | Duplicate resource or state conflict |
| `BUSINESS_RULE_VIOLATION` | 422 | Operation violates tournament rules |
| `INTERNAL_ERROR` | 500 | Unexpected server error |
---
## Rate Limiting
- **Default:** 100 requests per 60 seconds per IP
- Headers returned: `X-RateLimit-Limit`, `X-RateLimit-Remaining`, `X-RateLimit-Reset`
- Exceeding returns `429 Too Many Requests`
---
## Endpoints
---
### Health & Root
#### `GET /`
Returns API metadata. No auth required.
**Response `200`:**
```json
{
"name": "Swiss System Tournament API",
"version": "1.0.0",
"docs": "/api/v1"
}
```
#### `GET /health`
Health check endpoint. No auth required.
**Response `200`:**
```json
{
"status": "ok",
"timestamp": "2026-05-24T18:04:39.000Z"
}
```
---
### Auth
Base path: `/api/v1/auth`
---
#### `POST /api/v1/auth/signup`
Create a new user account. Optionally creates an organization.
**Auth required:** No
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `email` | string | Yes | Valid email address |
| `password` | string | Yes | 8-128 characters |
| `fullName` | string | Yes | 1-200 characters |
| `organizationName` | string | No | 1-200 characters. If provided, creates an org and assigns user as `org_admin` |
**Example Request:**
```json
{
"email": "arbiter@chess-club.org",
"password": "securePassword123",
"fullName": "Magnus Carlsen",
"organizationName": "Oslo Chess Club"
}
```
**Response `201`:**
```json
{
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "arbiter@chess-club.org",
"fullName": "Magnus Carlsen"
},
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "v1.MjAyNi0wNS0yNFQx..."
}
```
---
#### `POST /api/v1/auth/login`
Authenticate an existing user.
**Auth required:** No
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `email` | string | Yes | Valid email |
| `password` | string | Yes | Min 1 character |
**Example Request:**
```json
{
"email": "arbiter@chess-club.org",
"password": "securePassword123"
}
```
**Response `200`:**
```json
{
"user": {
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"email": "arbiter@chess-club.org",
"fullName": "Magnus Carlsen"
},
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "v1.MjAyNi0wNS0yNFQx...",
"expiresAt": 1716580800
}
```
---
#### `POST /api/v1/auth/refresh`
Exchange a refresh token for new access/refresh tokens.
**Auth required:** No
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `refreshToken` | string | Yes | Valid refresh token |
**Response `200`:**
```json
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "v1.MjAyNi0wNS0yNFQx...",
"expiresAt": 1716580800
}
```
---
#### `GET /api/v1/auth/me`
Get the authenticated user's profile.
**Auth required:** Yes
**Response `200`:**
```json
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"fullName": "Magnus Carlsen",
"displayName": "Magnus",
"avatarUrl": null,
"fideId": "1503014",
"fideRatingStandard": 2830,
"fideRatingRapid": 2830,
"fideRatingBlitz": 2886,
"nationalId": null,
"nationalRating": null,
"birthDate": "1990-11-30",
"countryCode": "NOR",
"title": "GM",
"createdAt": "2026-05-24T18:00:00.000Z",
"updatedAt": "2026-05-24T18:00:00.000Z"
}
```
---
#### `PATCH /api/v1/auth/me`
Update the authenticated user's profile.
**Auth required:** Yes
**Request Body (all fields optional):**
| Field | Type | Constraints |
|-------|------|-------------|
| `fullName` | string | 1-200 characters |
| `displayName` | string | Max 100 characters |
| `fideId` | string | FIDE ID number |
| `nationalId` | string | National federation ID |
| `birthDate` | string | Date format |
| `countryCode` | string | Exactly 3 characters (ISO 3166-1 alpha-3) |
| `title` | string | e.g. "GM", "IM", "FM", "CM", "WGM", "WIM" |
**Example Request:**
```json
{
"fideId": "1503014",
"fideRatingStandard": 2830,
"title": "GM",
"countryCode": "NOR"
}
```
**Response `200`:** Updated profile (same shape as `GET /me`).
---
### Organizations
Base path: `/api/v1/organizations`
All organization endpoints require authentication.
---
#### `POST /api/v1/organizations`
Create a new organization. The creating user becomes `org_admin`.
**Auth required:** Yes
**Role required:** None (any authenticated user)
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `name` | string | Yes | 1-200 characters |
| `contactEmail` | string | Yes | Valid email |
| `website` | string | No | Valid URL |
| `countryCode` | string | No | Exactly 3 characters |
| `fideFederationId` | string | No | FIDE federation code |
**Example Request:**
```json
{
"name": "Egyptian Chess Federation",
"contactEmail": "info@ecf-chess.org",
"website": "https://ecf-chess.org",
"countryCode": "EGY",
"fideFederationId": "EGY"
}
```
**Response `201`:**
```json
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "Egyptian Chess Federation",
"slug": "egyptian-chess-federation",
"logoUrl": null,
"website": "https://ecf-chess.org",
"countryCode": "EGY",
"fideFederationId": "EGY",
"contactEmail": "info@ecf-chess.org",
"settings": {},
"subscriptionTier": "free",
"maxTournaments": 5,
"maxPlayersPerTournament": 200,
"createdAt": "2026-05-24T18:00:00.000Z",
"updatedAt": "2026-05-24T18:00:00.000Z"
}
```
---
#### `GET /api/v1/organizations`
List all organizations the authenticated user is a member of.
**Auth required:** Yes
**Response `200`:**
```json
{
"data": [
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "Egyptian Chess Federation",
"slug": "egyptian-chess-federation",
"contactEmail": "info@ecf-chess.org",
"role": "org_admin",
"...": "..."
}
]
}
```
---
#### `GET /api/v1/organizations/:orgId`
Get a single organization's details.
**Auth required:** Yes
**Role required:** Any org member
**Path Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `orgId` | UUID | Organization ID |
**Response `200`:** Full organization object.
---
#### `PATCH /api/v1/organizations/:orgId`
Update an organization.
**Auth required:** Yes
**Role required:** `org_admin` or higher
**Request Body:** Same fields as create, all optional.
**Response `200`:** Updated organization object.
---
#### `DELETE /api/v1/organizations/:orgId`
Delete an organization and all its data (events, tournaments, etc).
**Auth required:** Yes
**Role required:** `org_admin`
**Response:** `204 No Content`
---
#### `GET /api/v1/organizations/:orgId/members`
List all members of an organization with their profiles.
**Auth required:** Yes
**Role required:** Any org member
**Response `200`:**
```json
{
"data": [
{
"id": "membership-uuid",
"userId": "user-uuid",
"organizationId": "org-uuid",
"role": "org_admin",
"status": "active",
"invitedBy": null,
"joinedAt": "2026-05-24T18:00:00.000Z",
"createdAt": "2026-05-24T18:00:00.000Z",
"updatedAt": "2026-05-24T18:00:00.000Z",
"profile": {
"fullName": "Magnus Carlsen",
"displayName": "Magnus",
"fideId": "1503014"
}
}
]
}
```
---
#### `POST /api/v1/organizations/:orgId/members`
Invite a user to the organization by email.
**Auth required:** Yes
**Role required:** `org_admin`
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `email` | string | Yes | Valid email of existing user |
| `role` | string | Yes | `org_admin`, `arbiter`, `player`, or `spectator` |
**Example Request:**
```json
{
"email": "arbiter@chess-club.org",
"role": "arbiter"
}
```
**Response `201`:** Membership record.
---
#### `PATCH /api/v1/organizations/:orgId/members/:memberId`
Change a member's role.
**Auth required:** Yes
**Role required:** `org_admin`
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `role` | string | Yes | `org_admin`, `arbiter`, `player`, or `spectator` |
**Response `200`:** Updated membership record.
---
#### `DELETE /api/v1/organizations/:orgId/members/:memberId`
Remove a member from the organization.
**Auth required:** Yes
**Role required:** `org_admin`
**Response:** `204 No Content`
---
### Events
Events are multi-tournament containers (e.g., "Cairo Open 2026" with sections A, B, C).
---
#### `POST /api/v1/organizations/:orgId/events`
Create a new event within an organization.
**Auth required:** Yes
**Role required:** `org_admin` or `arbiter`
**Org membership:** Required
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `name` | string | Yes | 1-200 characters |
| `description` | string | No | Free text |
| `venue` | string | No | Venue name |
| `city` | string | No | City name |
| `countryCode` | string | No | 3 characters (ISO) |
| `dateStart` | string | Yes | Date (YYYY-MM-DD) |
| `dateEnd` | string | Yes | Date (YYYY-MM-DD) |
| `timeControlDescription` | string | No | e.g., "90min + 30sec/move" |
| `timeControlType` | string | No | `standard`, `rapid`, `blitz`, `bullet`. Default: `standard` |
| `chiefArbiterId` | UUID | No | User ID of chief arbiter |
| `isFideRated` | boolean | No | Default: `false` |
**Example Request:**
```json
{
"name": "Cairo International Open 2026",
"description": "9-round Swiss system tournament",
"venue": "Cairo Convention Center",
"city": "Cairo",
"countryCode": "EGY",
"dateStart": "2026-07-01",
"dateEnd": "2026-07-09",
"timeControlDescription": "90 minutes for 40 moves + 30 minutes + 30 seconds increment from move 1",
"timeControlType": "standard",
"isFideRated": true
}
```
**Response `201`:**
```json
{
"id": "event-uuid",
"organizationId": "org-uuid",
"name": "Cairo International Open 2026",
"description": "9-round Swiss system tournament",
"venue": "Cairo Convention Center",
"city": "Cairo",
"countryCode": "EGY",
"dateStart": "2026-07-01",
"dateEnd": "2026-07-09",
"timeControlDescription": "90 minutes for 40 moves + 30 minutes + 30 seconds increment from move 1",
"timeControlType": "standard",
"chiefArbiterId": null,
"deputyArbiterIds": [],
"status": "draft",
"isFideRated": true,
"fideEventId": null,
"metadata": "{}",
"createdAt": "2026-05-24T18:00:00.000Z",
"updatedAt": "2026-05-24T18:00:00.000Z"
}
```
---
#### `GET /api/v1/organizations/:orgId/events`
List all events in an organization, ordered by `dateStart` descending.
**Auth required:** Yes
**Role required:** Any org member
**Response `200`:**
```json
{
"data": [ ...array of event objects... ]
}
```
---
#### `GET /api/v1/events/:eventId`
Get a single event by ID.
**Auth required:** Yes
**Response `200`:** Full event object.
---
#### `PATCH /api/v1/events/:eventId`
Update an event.
**Auth required:** Yes
**Request Body:** Same fields as create, all optional.
**Response `200`:** Updated event object.
---
#### `DELETE /api/v1/events/:eventId`
Delete an event and all its tournaments.
**Auth required:** Yes
**Response:** `204 No Content`
---
### Tournaments
Tournaments are individual sections within an event (e.g., "Open A", "Under 1800", "Women's Section").
---
#### `POST /api/v1/events/:eventId/tournaments`
Create a tournament within an event.
**Auth required:** Yes
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `name` | string | Yes | 1-200 characters |
| `tournamentType` | string | No | `swiss` (default), `round_robin`, `double_round_robin` |
| `roundsNumber` | integer | Yes | 1-30 |
| `tableStartNumber` | integer | No | Default: 1 |
| `pairingSystem` | string | No | Default: `dutch` |
| `accelerationMethod` | string/null | No | e.g., "baku" |
| `accelerationRounds` | integer | No | Default: 0 |
| `initialOrdering` | string[] | No | Default: `["fide_rating","name"]` |
| `tiebreakRules` | string[] | No | Default: `["buchholz_cut_1","buchholz","sonneborn_berger"]`. See [Tiebreak Systems](#tiebreak-systems) for valid values |
| `maxPlayers` | integer | No | Default: 200 |
| `byeValue` | string | No | Default: `"1.00"`. Points awarded for a bye |
| `maxByesPerPlayer` | integer | No | Default: 1 |
| `registrationOpensAt` | datetime | No | ISO 8601 |
| `registrationClosesAt` | datetime | No | ISO 8601 |
**Example Request:**
```json
{
"name": "Open Section A",
"tournamentType": "swiss",
"roundsNumber": 9,
"tiebreakRules": ["buchholz_cut_1", "buchholz", "sonneborn_berger", "progressive_score"],
"maxPlayers": 150,
"byeValue": "1.00",
"maxByesPerPlayer": 1,
"accelerationMethod": "baku",
"accelerationRounds": 3
}
```
**Response `201`:**
```json
{
"id": "tournament-uuid",
"eventId": "event-uuid",
"organizationId": "org-uuid",
"name": "Open Section A",
"tournamentType": "swiss",
"status": "draft",
"roundsNumber": 9,
"currentRound": 0,
"tableStartNumber": 1,
"pairingSystem": "dutch",
"accelerationMethod": "baku",
"accelerationRounds": 3,
"colorAllocationRule": "equalise_alternate",
"initialOrdering": ["fide_rating", "name"],
"tiebreakRules": ["buchholz_cut_1", "buchholz", "sonneborn_berger", "progressive_score"],
"ratedMinimum": null,
"ratedMaximum": null,
"kFactorOverride": null,
"maxPlayers": 150,
"byeValue": "1.00",
"maxByesPerPlayer": 1,
"registrationOpensAt": null,
"registrationClosesAt": null,
"startedAt": null,
"completedAt": null,
"createdAt": "2026-05-24T18:00:00.000Z",
"updatedAt": "2026-05-24T18:00:00.000Z"
}
```
---
#### `GET /api/v1/events/:eventId/tournaments`
List all tournaments in an event.
**Auth required:** Yes
**Response `200`:**
```json
{
"data": [ ...array of tournament objects... ]
}
```
---
#### `GET /api/v1/tournaments/:tournamentId`
Get a single tournament.
**Auth required:** Yes
**Response `200`:** Full tournament object.
---
#### `PATCH /api/v1/tournaments/:tournamentId`
Update a tournament's settings.
**Auth required:** Yes
**Request Body:** Same fields as create, all optional.
**Response `200`:** Updated tournament object.
---
#### `POST /api/v1/tournaments/:tournamentId/start`
Transition tournament from `draft`/`registration` to `in_progress`.
**Auth required:** Yes
**Business Rules:**
- Status must be `draft` or `registration`
- At least 2 registered players required
**Response `200`:** Tournament with `status: "in_progress"` and `startedAt` set.
---
#### `POST /api/v1/tournaments/:tournamentId/complete`
Mark a tournament as completed.
**Auth required:** Yes
**Business Rules:**
- Status must be `in_progress`
**Response `200`:** Tournament with `status: "completed"` and `completedAt` set.
---
#### `DELETE /api/v1/tournaments/:tournamentId`
Delete a tournament and all associated data.
**Auth required:** Yes
**Response:** `204 No Content`
---
### Players
Tournament player registration and management.
---
#### `POST /api/v1/tournaments/:tournamentId/players`
Register a single player in a tournament.
**Auth required:** Yes
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `name` | string | Yes | 1-200 characters |
| `startNumber` | integer | No | Auto-assigned if omitted |
| `birthDate` | string | No | Date (YYYY-MM-DD) |
| `countryCode` | string | No | 3 characters |
| `city` | string | No | Player's city |
| `club` | string | No | Club name |
| `title` | string | No | FIDE title (GM, IM, FM, etc.) |
| `fideId` | string | No | FIDE player ID |
| `fideRatingStandard` | integer | No | FIDE standard rating |
| `fideRatingRapid` | integer | No | FIDE rapid rating |
| `fideRatingBlitz` | integer | No | FIDE blitz rating |
| `nationalId` | string | No | National federation ID |
| `nationalRating` | integer | No | National rating |
| `categoryId` | UUID | No | Category/section UUID |
**Example Request:**
```json
{
"name": "Anand, Viswanathan",
"fideId": "5000017",
"fideRatingStandard": 2751,
"countryCode": "IND",
"title": "GM",
"club": "Petroleum Sports Promotion Board"
}
```
**Response `201`:**
```json
{
"id": "player-uuid",
"tournamentId": "tournament-uuid",
"organizationId": "org-uuid",
"categoryId": null,
"userId": null,
"startNumber": 1,
"name": "Anand, Viswanathan",
"birthDate": null,
"countryCode": "IND",
"city": null,
"club": "Petroleum Sports Promotion Board",
"title": "GM",
"fideId": "5000017",
"fideRatingStandard": 2751,
"fideRatingRapid": null,
"fideRatingBlitz": null,
"nationalId": null,
"nationalRating": null,
"isActive": true,
"withdrawnAfterRound": null,
"roundsExcluded": [],
"receivedByeInRounds": [],
"totalPoints": "0",
"floatHistory": [],
"colorHistory": [],
"createdAt": "2026-05-24T18:00:00.000Z",
"updatedAt": "2026-05-24T18:00:00.000Z"
}
```
---
#### `GET /api/v1/tournaments/:tournamentId/players`
List all players in a tournament, ordered by `startNumber` ascending.
**Auth required:** Yes
**Response `200`:**
```json
{
"data": [ ...array of player objects... ]
}
```
---
#### `GET /api/v1/tournaments/:tournamentId/players/:playerId`
Get a single player's details.
**Auth required:** Yes
**Response `200`:** Full player object.
---
#### `PATCH /api/v1/tournaments/:tournamentId/players/:playerId`
Update a player's information.
**Auth required:** Yes
**Request Body:** Same fields as create, all optional.
**Response `200`:** Updated player object.
---
#### `DELETE /api/v1/tournaments/:tournamentId/players/:playerId`
Remove or withdraw a player.
**Auth required:** Yes
**Behavior:**
- If tournament is `in_progress`: Player is **withdrawn** (`isActive=false`, `withdrawnAfterRound` set). Returns `200` with updated player.
- If tournament is `draft`/`registration`: Player is **hard-deleted**. Returns `204 No Content`.
---
#### `POST /api/v1/tournaments/:tournamentId/players/import`
Bulk import multiple players at once.
**Auth required:** Yes
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `players` | array | Yes | Array of player objects (same fields as single create) |
**Example Request:**
```json
{
"players": [
{ "name": "Carlsen, Magnus", "fideId": "1503014", "fideRatingStandard": 2830, "countryCode": "NOR", "title": "GM" },
{ "name": "Nakamura, Hikaru", "fideId": "2016192", "fideRatingStandard": 2794, "countryCode": "USA", "title": "GM" },
{ "name": "Ding, Liren", "fideId": "8603677", "fideRatingStandard": 2780, "countryCode": "CHN", "title": "GM" }
]
}
```
**Response `201`:**
```json
{
"data": [ ...array of created player objects... ],
"count": 3
}
```
---
### Rounds
Round generation and management. The pairing engine runs here.
---
#### `GET /api/v1/tournaments/:tournamentId/rounds`
List all rounds in a tournament, ordered by `roundNumber` ascending.
**Auth required:** Yes
**Response `200`:**
```json
{
"data": [
{
"id": "round-uuid",
"tournamentId": "tournament-uuid",
"organizationId": "org-uuid",
"roundNumber": 1,
"status": "completed",
"scheduledAt": null,
"startedAt": "2026-07-01T09:00:00.000Z",
"completedAt": "2026-07-01T14:00:00.000Z",
"pairedAt": "2026-07-01T08:45:00.000Z",
"pairedBy": "user-uuid",
"createdAt": "2026-05-24T18:00:00.000Z",
"updatedAt": "2026-05-24T18:00:00.000Z"
}
]
}
```
---
#### `GET /api/v1/tournaments/:tournamentId/rounds/:roundNum`
Get a specific round by number.
**Auth required:** Yes
**Path Parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `tournamentId` | UUID | Tournament ID |
| `roundNum` | integer | Round number (1-indexed) |
**Response `200`:** Single round object.
---
#### `POST /api/v1/tournaments/:tournamentId/rounds/generate`
**Run the FIDE Dutch Pairing Engine** to generate the next round's pairings.
**Auth required:** Yes
**Business Rules:**
- Tournament must have status `in_progress`
- If a previous round exists, it must have status `completed`
- `currentRound + 1` must not exceed `roundsNumber`
- At least 2 active players required
**Response `201`:**
```json
{
"round": {
"id": "round-uuid",
"tournamentId": "tournament-uuid",
"roundNumber": 1,
"status": "paired",
"pairedAt": "2026-07-01T08:45:00.000Z",
"pairedBy": "user-uuid"
},
"pairingsCount": 75,
"bye": 142,
"logs": [
"Round 1: 150 active players",
"Bye allocated to player #142 (lowest in lowest score group)",
"Score group 0.0: 149 players",
"Bracket pairing: S1=75, S2=74",
"All 75 pairings generated successfully"
]
}
```
The `bye` field contains the `startNumber` of the player who received the bye, or `null` if even number of players.
The `logs` array provides a human-readable audit trail of every pairing decision made by the engine.
---
#### `DELETE /api/v1/tournaments/:tournamentId/rounds/:roundNum`
Unpair the last round (delete all its pairings and the round itself).
**Auth required:** Yes
**Business Rules:**
- Can only unpair the **last** round (where `currentRound === roundNum`)
- Decrements `tournament.currentRound`
**Response:** `204 No Content`
---
### Pairings
Individual game pairings within rounds.
---
#### `GET /api/v1/rounds/:roundId/pairings`
List all pairings in a round, ordered by `boardNumber` ascending.
**Auth required:** Yes
**Response `200`:**
```json
{
"data": [
{
"id": "pairing-uuid",
"roundId": "round-uuid",
"tournamentId": "tournament-uuid",
"organizationId": "org-uuid",
"boardNumber": 1,
"whitePlayerId": "player-uuid-1",
"blackPlayerId": "player-uuid-2",
"result": "not_played",
"whitePoints": "0.00",
"blackPoints": "0.00",
"isForfeit": false,
"isBye": false,
"resultEnteredBy": null,
"resultEnteredAt": null,
"resultConfirmed": false,
"createdAt": "2026-05-24T18:00:00.000Z",
"updatedAt": "2026-05-24T18:00:00.000Z"
}
]
}
```
---
#### `GET /api/v1/pairings/:pairingId`
Get a single pairing.
**Auth required:** Yes
**Response `200`:** Full pairing object.
---
#### `PATCH /api/v1/pairings/:pairingId/result`
Enter or update a game result.
**Auth required:** Yes
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `result` | string | Yes | See [Game Results](#game-results) enum |
**Example Request:**
```json
{
"result": "white_wins"
}
```
**Response `200`:** Updated pairing with computed points:
```json
{
"id": "pairing-uuid",
"boardNumber": 1,
"whitePlayerId": "player-uuid-1",
"blackPlayerId": "player-uuid-2",
"result": "white_wins",
"whitePoints": "1.00",
"blackPoints": "0.00",
"isForfeit": false,
"resultEnteredBy": "user-uuid",
"resultEnteredAt": "2026-07-01T14:30:00.000Z",
"...": "..."
}
```
**Point Calculation:**
| Result | White Points | Black Points |
|--------|:-----------:|:------------:|
| `white_wins` | 1.00 | 0.00 |
| `black_wins` | 0.00 | 1.00 |
| `draw` | 0.50 | 0.50 |
| `white_forfeit` | 0.00 | 1.00 |
| `black_forfeit` | 1.00 | 0.00 |
| `double_forfeit` | 0.00 | 0.00 |
| `bye_full` | 1.00 | 0.00 |
| `bye_half` | 0.50 | 0.00 |
| `bye_zero` | 0.00 | 0.00 |
**Side Effects:** Recalculates `totalPoints` for all affected players.
---
#### `POST /api/v1/rounds/:roundId/pairings/results`
Enter multiple results at once (batch).
**Auth required:** Yes
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `results` | array | Yes | Array of `{ pairingId: UUID, result: GameResult }` |
**Example Request:**
```json
{
"results": [
{ "pairingId": "uuid-1", "result": "white_wins" },
{ "pairingId": "uuid-2", "result": "draw" },
{ "pairingId": "uuid-3", "result": "black_wins" }
]
}
```
**Response `200`:**
```json
{
"data": [ ...updated pairing objects... ],
"allComplete": true
}
```
**Side Effects:**
- If `allComplete` is `true`: Round status is automatically updated to `completed`.
- Recalculates all player points.
---
#### `POST /api/v1/rounds/:roundId/pairings/manual`
Manually create a pairing (arbiter override).
**Auth required:** Yes
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `boardNumber` | integer | Yes | Min 1 |
| `whitePlayerId` | UUID | Yes | Must be a player in the tournament |
| `blackPlayerId` | UUID/null | No | `null` for bye |
| `isBye` | boolean | No | Default: `false`. If `true`, auto-sets result to `bye_full` |
**Example Request:**
```json
{
"boardNumber": 76,
"whitePlayerId": "player-uuid",
"blackPlayerId": null,
"isBye": true
}
```
**Response `201`:** Created pairing object.
---
#### `DELETE /api/v1/pairings/:pairingId`
Delete a single pairing.
**Auth required:** Yes
**Response:** `204 No Content`
---
### Standings
Tournament standings with tiebreak calculations.
---
#### `GET /api/v1/tournaments/:tournamentId/standings`
Get current standings for a tournament.
**Auth required:** Yes
**Query Parameters:**
| Param | Type | Required | Description |
|-------|------|----------|-------------|
| `round` | integer | No | Round number. Defaults to current round |
| `category` | UUID | No | Filter by category ID |
**Example:** `GET /api/v1/tournaments/abc123/standings?round=5&category=cat-uuid`
**Response `200`:**
```json
{
"data": [
{
"rank": 1,
"rankCategory": 1,
"playerId": "player-uuid",
"name": "Carlsen, Magnus",
"startNumber": 1,
"points": 4.5,
"tiebreakValues": [18.5, 22.0, 15.75, 10.0],
"fideRating": 2830,
"club": "Offerspill Chess Club",
"gamesPlayed": 5,
"wins": 4,
"draws": 1,
"losses": 0
},
{
"rank": 2,
"rankCategory": 2,
"playerId": "player-uuid-2",
"name": "Nakamura, Hikaru",
"startNumber": 2,
"points": 4.0,
"tiebreakValues": [19.0, 23.5, 14.00, 10.0],
"fideRating": 2794,
"club": null,
"gamesPlayed": 5,
"wins": 3,
"draws": 2,
"losses": 0
}
],
"roundNumber": 5
}
```
The `tiebreakValues` array corresponds to the tournament's `tiebreakRules` array in order. For example, if `tiebreakRules: ["buchholz_cut_1", "buchholz", "sonneborn_berger", "progressive_score"]`, then `tiebreakValues[0]` is Buchholz Cut 1, `[1]` is full Buchholz, etc.
---
#### `POST /api/v1/tournaments/:tournamentId/standings/recalculate`
Force recalculation of all standings and tiebreaks.
**Auth required:** Yes
**Business Rules:** At least one round must have been played.
**Response `200`:**
```json
{
"data": [ ...recalculated standings... ],
"roundNumber": 5
}
```
---
### Categories
Sub-sections within a tournament (e.g., "Under 2000", "Women", "Seniors 65+").
---
#### `POST /api/v1/tournaments/:tournamentId/categories`
Create a category.
**Auth required:** Yes
**Request Body:**
| Field | Type | Required | Constraints |
|-------|------|----------|-------------|
| `name` | string | Yes | 1-100 characters |
| `abbreviation` | string | No | Max 10 characters |
| `description` | string | No | Free text |
| `minRating` | integer | No | Minimum rating for eligibility |
| `maxRating` | integer | No | Maximum rating for eligibility |
| `minAge` | integer | No | Minimum age |
| `maxAge` | integer | No | Maximum age |
| `gender` | string/null | No | `"M"`, `"F"`, or `null` |
| `sortOrder` | integer | No | Display order. Default: 0 |
**Example Request:**
```json
{
"name": "Under 2000",
"abbreviation": "U2000",
"maxRating": 1999,
"sortOrder": 1
}
```
**Response `201`:**
```json
{
"id": "category-uuid",
"tournamentId": "tournament-uuid",
"organizationId": "org-uuid",
"name": "Under 2000",
"abbreviation": "U2000",
"description": null,
"minRating": null,
"maxRating": 1999,
"minAge": null,
"maxAge": null,
"gender": null,
"sortOrder": 1,
"createdAt": "2026-05-24T18:00:00.000Z",
"updatedAt": "2026-05-24T18:00:00.000Z"
}
```
---
#### `GET /api/v1/tournaments/:tournamentId/categories`
List all categories for a tournament, ordered by `sortOrder`.
**Auth required:** Yes
**Response `200`:**
```json
{
"data": [ ...array of category objects... ]
}
```
---
#### `PATCH /api/v1/categories/:categoryId`
Update a category.
**Auth required:** Yes
**Request Body:** Same fields as create, all optional.
**Response `200`:** Updated category object.
---
#### `DELETE /api/v1/categories/:categoryId`
Delete a category. Players in this category will have their `categoryId` set to `null`.
**Auth required:** Yes
**Response:** `204 No Content`
---
### Export
Export tournament data in various formats.
---
#### `GET /api/v1/tournaments/:tournamentId/export/trf`
Export in **FIDE Tournament Report File** format. This is the official format required for FIDE-rated tournament submissions.
**Auth required:** Yes
**Response:** Plain text file download.
- `Content-Type: text/plain`
- `Content-Disposition: attachment; filename="Tournament_Name.trf"`
**TRF Format Example:**
```
012 Cairo International Open 2026
022 Cairo
032 EGY
042 2026-07-01
052 Chief Arbiter Name
062 150
072 9
082 1234567890
092 Standard
102 IA Chief Arbiter
132 90/40+30+30
001 1 Carlsen g NOR 1503014 2830 9.0 1 w 1 2 b 1 3 w = ...
001 2 Nakamura g USA 2016192 2794 8.5 1 b 0 2 w 1 3 b 1 ...
```
---
#### `GET /api/v1/tournaments/:tournamentId/export/json`
Export complete tournament data as JSON.
**Auth required:** Yes
**Response `200`:**
```json
{
"tournament": { ...full tournament object... },
"event": { ...full event object... },
"players": [ ...all player objects... ],
"rounds": [ ...all round objects... ],
"pairings": [ ...all pairing objects... ]
}
```
---
#### `GET /api/v1/tournaments/:tournamentId/export/crosstable`
Generate a crosstable showing all head-to-head results.
**Auth required:** Yes
**Response `200`:**
```json
{
"data": [
{
"startNumber": 1,
"name": "Carlsen, Magnus",
"rating": 2830,
"points": 7.5,
"results": {
"2": "1w",
"5": "0.5b",
"8": "1w",
"3": "0.5w",
"12": "1b",
"4": "1w",
"6": "0.5b",
"9": "1w",
"7": "1b"
}
}
]
}
```
The `results` object maps opponent `startNumber` to a result string:
- `"1w"` = Win with white
- `"1b"` = Win with black
- `"0.5w"` = Draw with white
- `"0.5b"` = Draw with black
- `"0w"` = Loss with white
- `"0b"` = Loss with black
---
## Enums & Types
### Tournament Type
| Value | Description |
|-------|-------------|
| `swiss` | Swiss system (paired by engine) |
| `round_robin` | Everyone plays everyone |
| `double_round_robin` | Everyone plays everyone twice |
### Tournament Status
| Value | Description |
|-------|-------------|
| `draft` | Initial state, setup phase |
| `registration` | Open for player registration |
| `in_progress` | Tournament has started, rounds being played |
| `completed` | All rounds finished |
| `cancelled` | Tournament was cancelled |
### Round Status
| Value | Description |
|-------|-------------|
| `pending` | Round exists but not yet paired |
| `paired` | Pairings generated, games not started |
| `in_progress` | Games being played |
| `completed` | All results entered |
### Game Results
| Value | White Points | Black Points | Notes |
|-------|:---:|:---:|-------|
| `white_wins` | 1 | 0 | Normal white victory |
| `black_wins` | 0 | 1 | Normal black victory |
| `draw` | 0.5 | 0.5 | Drawn game |
| `white_forfeit` | 0 | 1 | White forfeited |
| `black_forfeit` | 1 | 0 | Black forfeited |
| `double_forfeit` | 0 | 0 | Both forfeited |
| `bye_full` | 1 | - | Full-point bye |
| `bye_half` | 0.5 | - | Half-point bye |
| `bye_zero` | 0 | - | Zero-point bye |
| `not_played` | 0 | 0 | Default, no result yet |
### User Roles (hierarchy)
| Role | Level | Capabilities |
|------|:-----:|-------------|
| `super_admin` | 5 | Everything across all orgs |
| `org_admin` | 4 | Full control of own org |
| `arbiter` | 3 | Manage events/tournaments they're assigned to |
| `player` | 2 | View data, participate |
| `spectator` | 1 | Read-only access |
### Time Control Type
| Value | Description |
|-------|-------------|
| `standard` | Classical (≥60 min) |
| `rapid` | Rapid (15-60 min) |
| `blitz` | Blitz (3-15 min) |
| `bullet` | Bullet (<3 min) |
### FIDE Titles
| Value | Description |
|-------|-------------|
| `GM` | Grandmaster |
| `IM` | International Master |
| `FM` | FIDE Master |
| `CM` | Candidate Master |
| `WGM` | Woman Grandmaster |
| `WIM` | Woman International Master |
| `WFM` | Woman FIDE Master |
| `WCM` | Woman Candidate Master |
---
## FIDE Dutch Pairing Engine
The API implements the **FIDE Dutch System (C.04)** pairing algorithm. This is the official system used in FIDE-rated Swiss tournaments worldwide.
### How It Works
When you call `POST /tournaments/:id/rounds/generate`, the engine:
1. **Filters active players** — excludes withdrawn and excluded players
2. **Allocates bye** — lowest-ranked player in lowest score group who hasn't had a bye
3. **Applies acceleration** (if configured) — artificially inflates scores in early rounds to create more decisive games at the top
4. **Forms score groups** — players with the same score are grouped together
5. **For each bracket** (highest score group down):
- Splits into S1 (top half by rating) and S2 (bottom half)
- Checks compatibility constraints:
- No player pair that already played each other
- Color allocation constraints (absolute/strong/mild)
- Attempts pairing with systematic transposition and exchange
- Applies float rules for players moving between groups
6. **Assigns board numbers** — top-rated pair on board 1
7. **Allocates colors** according to FIDE C.04 rules
### Color Allocation Rules (FIDE C.04.A7-A9)
| Priority | Rule | Description |
|----------|------|-------------|
| **Absolute** | Must | Had same color last 2 games → MUST get other color |
| **Strong** | Should | More total games with one color → SHOULD get other color |
| **Mild** | Prefer | Alternate from last game → preference only |
### Acceleration
When `accelerationMethod` is set (e.g., "baku"), players' virtual scores are boosted in the first N rounds (`accelerationRounds`) to create stronger pairings at the top tables early in the tournament.
### Float Management
Players who cannot be paired within their score group "float" up or down:
- **Float up**: Paired against a higher-scoring opponent
- **Float down**: Paired against a lower-scoring opponent
- The engine tracks float history to avoid repeated floating
---
## Tiebreak Systems
The following tiebreak calculators are available. Set them in `tournament.tiebreakRules`:
| Value | Name | Description |
|-------|------|-------------|
| `buchholz` | Buchholz | Sum of all opponents' scores |
| `buchholz_cut_1` | Buchholz Cut 1 | Buchholz minus lowest opponent score |
| `buchholz_median` | Buchholz Median | Buchholz minus highest and lowest |
| `sonneborn_berger` | Sonneborn-Berger | Sum of defeated opponents' scores + half of drawn opponents' scores |
| `direct_encounter` | Direct Encounter | Result between tied players |
| `number_of_wins` | Number of Wins | Total games won (excludes forfeits) |
| `number_of_blacks` | Number of Blacks | Games played with black pieces |
| `koya` | Koya System | Points scored against opponents with ≥50% |
| `progressive_score` | Progressive Score | Cumulative sum of round-by-round scores |
| `average_rating_opponents` | Average Rating of Opponents | Mean rating of all opponents faced |
| `performance_rating` | Performance Rating | FIDE performance rating |
### FIDE Recommended Tiebreak Order
For Swiss tournaments, FIDE recommends:
```json
["buchholz_cut_1", "buchholz", "sonneborn_berger"]
```
For Round Robin:
```json
["direct_encounter", "number_of_wins", "sonneborn_berger"]
```
---
## Realtime WebSocket
The API broadcasts live updates via Supabase Realtime. Connect to receive instant notifications when:
- **Pairings are generated** — new round appears
- **Results are entered** — game results update live
- **Standings change** — rankings recalculated
### Connecting
```javascript
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://safe-supabase-kong.caprover.al-arcade.com',
'YOUR_ANON_KEY'
)
// Subscribe to pairings changes for a tournament
supabase
.channel('tournament-updates')
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'pairings',
filter: `tournament_id=eq.YOUR_TOURNAMENT_ID`
}, (payload) => {
console.log('Pairing update:', payload)
})
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'standings',
filter: `tournament_id=eq.YOUR_TOURNAMENT_ID`
}, (payload) => {
console.log('Standing update:', payload)
})
.on('postgres_changes', {
event: '*',
schema: 'public',
table: 'rounds',
filter: `tournament_id=eq.YOUR_TOURNAMENT_ID`
}, (payload) => {
console.log('Round update:', payload)
})
.subscribe()
```
### Published Tables
| Table | Events | Use Case |
|-------|--------|----------|
| `pairings` | INSERT, UPDATE | New pairings, result entry |
| `standings` | INSERT, UPDATE | Ranking changes |
| `rounds` | INSERT, UPDATE | New rounds, status changes |
| `tournament_players` | UPDATE | Player withdrawal, score updates |
---
## Tournament Lifecycle
```
┌──────────┐ start() ┌─────────────┐ complete() ┌───────────┐
│ draft │ ───────────────> │ in_progress │ ──────────────> │ completed │
└──────────┘ └─────────────┘ └───────────┘
│ │
│ (optional) │ generate() / result / generate() ...
v │
┌──────────────┐ │
│ registration │ ───────────────────┘
└──────────────┘ start()
┌───────────┐
│ cancelled │ (can set from any state)
└───────────┘
```
### Typical Tournament Flow
1. **Create event**`POST /organizations/:orgId/events`
2. **Create tournament**`POST /events/:eventId/tournaments`
3. **Register players**`POST /tournaments/:id/players` (or `/import` for bulk)
4. **Start tournament**`POST /tournaments/:id/start`
5. **Generate round 1**`POST /tournaments/:id/rounds/generate`
6. **Enter results**`PATCH /pairings/:id/result` (or batch via `POST /rounds/:id/pairings/results`)
7. **Repeat steps 5-6** for each round
8. **View standings**`GET /tournaments/:id/standings`
9. **Complete tournament**`POST /tournaments/:id/complete`
10. **Export**`GET /tournaments/:id/export/trf`
---
## Multi-Tenancy & RBAC
### Organization Isolation
Every resource belongs to an organization via `organization_id`. PostgreSQL Row Level Security (RLS) policies enforce that:
- Users can only see data from organizations they belong to
- Admin operations require `org_admin` role in that specific org
- The API layer enforces this at the application level too (defense-in-depth)
### Role-Based Access Control Matrix
| Action | Super Admin | Org Admin | Arbiter | Player | Spectator |
|--------|:-:|:-:|:-:|:-:|:-:|
| Create organizations | Yes | Yes | Yes | Yes | Yes |
| Manage own org settings | Yes | Yes | - | - | - |
| Delete org | Yes | Yes | - | - | - |
| Invite/remove members | Yes | Yes | - | - | - |
| Create events | Yes | Yes | Yes | - | - |
| Manage tournaments | Yes | Yes | Yes | - | - |
| Generate pairings | Yes | Yes | Yes | - | - |
| Enter results | Yes | Yes | Yes | - | - |
| View standings/pairings | Yes | Yes | Yes | Yes | Yes |
| Export TRF | Yes | Yes | Yes | - | - |
| View audit logs | Yes | Yes | - | - | - |
### Organization Header
For routes that require org context, include:
```
X-Organization-Id: <org-uuid>
```
Or use the `:orgId` path parameter where available.
---
## Full cURL Examples
### Complete Tournament Setup
```bash
# 1. Sign up
curl -X POST https://swissapi.caprover.al-arcade.com/api/v1/auth/signup \
-H "Content-Type: application/json" \
-d '{
"email": "admin@chess-club.org",
"password": "mySecurePass123",
"fullName": "Tournament Director",
"organizationName": "Local Chess Club"
}'
# Save the token
TOKEN="eyJhbGciOiJIUzI1NiIs..."
# 2. Create event
curl -X POST https://swissapi.caprover.al-arcade.com/api/v1/organizations/$ORG_ID/events \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Spring Open 2026",
"dateStart": "2026-06-01",
"dateEnd": "2026-06-03",
"venue": "Community Center",
"timeControlType": "rapid",
"timeControlDescription": "15+10"
}'
# 3. Create tournament
curl -X POST https://swissapi.caprover.al-arcade.com/api/v1/events/$EVENT_ID/tournaments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Open Section",
"roundsNumber": 7,
"tiebreakRules": ["buchholz_cut_1", "buchholz", "sonneborn_berger"]
}'
# 4. Bulk import players
curl -X POST https://swissapi.caprover.al-arcade.com/api/v1/tournaments/$TOURNAMENT_ID/players/import \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"players": [
{"name": "Smith, John", "fideRatingStandard": 2100, "countryCode": "USA"},
{"name": "Mueller, Hans", "fideRatingStandard": 1950, "countryCode": "GER"},
{"name": "Petrov, Ivan", "fideRatingStandard": 2200, "countryCode": "RUS"}
]
}'
# 5. Start tournament
curl -X POST https://swissapi.caprover.al-arcade.com/api/v1/tournaments/$TOURNAMENT_ID/start \
-H "Authorization: Bearer $TOKEN"
# 6. Generate round 1
curl -X POST https://swissapi.caprover.al-arcade.com/api/v1/tournaments/$TOURNAMENT_ID/rounds/generate \
-H "Authorization: Bearer $TOKEN"
# 7. Enter results (batch)
curl -X POST https://swissapi.caprover.al-arcade.com/api/v1/rounds/$ROUND_ID/pairings/results \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"results": [
{"pairingId": "uuid-1", "result": "white_wins"},
{"pairingId": "uuid-2", "result": "draw"},
{"pairingId": "uuid-3", "result": "black_wins"}
]
}'
# 8. Get standings
curl https://swissapi.caprover.al-arcade.com/api/v1/tournaments/$TOURNAMENT_ID/standings \
-H "Authorization: Bearer $TOKEN"
# 9. Export TRF
curl https://swissapi.caprover.al-arcade.com/api/v1/tournaments/$TOURNAMENT_ID/export/trf \
-H "Authorization: Bearer $TOKEN" \
-o tournament.trf
```
---
## SDK Integration Example (TypeScript)
```typescript
const API_BASE = 'https://swissapi.caprover.al-arcade.com/api/v1';
class SwissSystemClient {
private token: string;
constructor(token: string) {
this.token = token;
}
private async request(method: string, path: string, body?: unknown) {
const res = await fetch(`${API_BASE}${path}`, {
method,
headers: {
'Authorization': `Bearer ${this.token}`,
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`);
if (res.status === 204) return null;
return res.json();
}
// Auth
static async login(email: string, password: string) {
const res = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password }),
});
const data = await res.json();
return new SwissSystemClient(data.accessToken);
}
// Tournaments
createTournament(eventId: string, data: unknown) {
return this.request('POST', `/events/${eventId}/tournaments`, data);
}
startTournament(id: string) {
return this.request('POST', `/tournaments/${id}/start`);
}
// Players
importPlayers(tournamentId: string, players: unknown[]) {
return this.request('POST', `/tournaments/${tournamentId}/players/import`, { players });
}
// Rounds
generateRound(tournamentId: string) {
return this.request('POST', `/tournaments/${tournamentId}/rounds/generate`);
}
// Results
enterResults(roundId: string, results: Array<{pairingId: string, result: string}>) {
return this.request('POST', `/rounds/${roundId}/pairings/results`, { results });
}
// Standings
getStandings(tournamentId: string, round?: number) {
const query = round ? `?round=${round}` : '';
return this.request('GET', `/tournaments/${tournamentId}/standings${query}`);
}
// Export
async exportTRF(tournamentId: string): Promise<string> {
const res = await fetch(`${API_BASE}/tournaments/${tournamentId}/export/trf`, {
headers: { 'Authorization': `Bearer ${this.token}` },
});
return res.text();
}
}
```
---
## Database Schema (ERD Summary)
```
organizations
└── org_memberships ── user_profiles (auth.users)
└── events
└── tournaments
├── categories
├── tournament_players
├── rounds
│ └── pairings
└── standings
└── audit_logs
```
All tables have `organization_id` for multi-tenancy isolation via RLS.
---
## Deployment Info
| Property | Value |
|----------|-------|
| Public URL | `https://swissapi.caprover.al-arcade.com` |
| Internal URL | `http://srv-captain--swissapi` |
| Health Check | `GET /health` |
| Runtime | Node.js 20 (Alpine) |
| Database | PostgreSQL 15 (Supabase) |
| Auth Provider | Supabase GoTrue |
| Realtime | Supabase Realtime (WebSocket) |
================================================================================
AL-ARCADE SELF-HOSTED SUPABASE — CONNECTION & REFERENCE
================================================================================
SERVER ACCESS
=============
IP: 3.68.63.185
User: ubuntu
SSH Key: NewServer.pem
SSH Command: ssh -i NewServer.pem ubuntu@3.68.63.185
All docker commands require sudo.
SUPABASE API URL
================
https://safe-supabase-kong.caprover.al-arcade.com
SUPABASE STUDIO (Dashboard)
============================
URL: https://safe-supabase-studio.caprover.al-arcade.com
Auth: HTTP Basic Auth
Username: admin
Password: Alarcade123#
API KEYS
========
Anon Key (public, client-side safe):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84
Service Role Key (secret, server-side only, bypasses RLS):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4
JWT Secret:
902343981eb82f43ff7a3757f3fcf25f14a2b9c729454eae5029ee3d1f189eb7
DATABASE DIRECT CONNECTION
==========================
Host: safe-supabase-db (internal) or localhost from server
Port: 5432
Database: postgres
Admin User: supabase_admin
Password: 28ac17bf9d4f7a3d1bad045408102cf5
Connection String (from server):
postgresql://supabase_admin:28ac17bf9d4f7a3d1bad045408102cf5@localhost:5432/postgres
Connection Pooler (Supavisor):
Port 6543 (transaction mode)
API ENDPOINTS
=============
All endpoints are relative to the API URL above.
All require header: apikey: <anon_key or service_role_key>
REST API: /rest/v1/
Auth: /auth/v1/
Storage: /storage/v1/
Realtime: /realtime/v1/
Edge Functions: /functions/v1/<function_name>
GraphQL: /graphql/v1
Postgres Meta: /pg/
CLIENT SDK SETUP
================
JavaScript/TypeScript:
----------------------
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
'https://safe-supabase-kong.caprover.al-arcade.com',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84'
)
Unity C#:
---------
var url = "https://safe-supabase-kong.caprover.al-arcade.com";
var key = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84";
var client = new Supabase.Client(url, key);
Flutter/Dart:
-------------
final supabase = Supabase.initialize(
url: 'https://safe-supabase-kong.caprover.al-arcade.com',
anonKey: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84',
);
Python:
-------
from supabase import create_client
supabase = create_client(
"https://safe-supabase-kong.caprover.al-arcade.com",
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYW5vbiIsImlzcyI6InN1cGFiYXNlIiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjE4OTM0NTYwMDB9.31PF6PvP-pSrvRuQwLFptQoejR0W1A7o53lZhEbnz84"
)
AVAILABLE FEATURES
==================
1. DATABASE (PostgreSQL 15)
- Create tables, schemas, views, functions, triggers
- Row Level Security (RLS) policies
- Extensions: pgvector, pg_graphql, pgjwt, uuid-ossp, pgcrypto
- Full SQL access via psql or REST API
2. AUTHENTICATION (GoTrue v2.186.0)
- Email/password sign-up and login
- Anonymous users
- JWT-based sessions
- Admin user management API
- Auto-confirm enabled (no SMTP configured yet)
3. STORAGE (v1.22.12)
- Create buckets (public or private)
- Upload/download files up to 50MB
- Image transformations via ImgProxy
- RLS policies on buckets/objects
4. REALTIME (v2.34.47)
- Postgres Changes (subscribe to INSERT/UPDATE/DELETE)
- Broadcast (send messages between clients)
- Presence (track online users)
- Enable per table: ALTER PUBLICATION supabase_realtime ADD TABLE <table_name>;
5. EDGE FUNCTIONS (Deno runtime v1.71.2)
- Deploy at /captain/data/safe-supabase/functions/
- Each function is a folder with index.ts
- Accessible at /functions/v1/<function_name>
6. REST API (PostgREST v12.2.8)
- Auto-generated REST endpoints for all tables
- Filtering, pagination, ordering, embedding (joins)
- Respects RLS policies based on JWT role
7. GRAPHQL (pg_graphql)
- Auto-generated GraphQL schema from tables
- Endpoint: /graphql/v1
8. CONNECTION POOLING (Supavisor 2.7.4)
- Transaction mode on port 6543
- Max 100 client connections, pool size 20
9. IMAGE TRANSFORMATION (ImgProxy v3.30.1)
- Resize, crop, format conversion
- WebP auto-detection
10. ANALYTICS (Logflare 1.36.1)
- PostgreSQL backend (not BigQuery)
- Log collection via Vector
MANAGING VIA SSH (for AI agents)
================================
Run SQL:
sudo docker exec safe-supabase-db psql -U supabase_admin -d postgres -c "YOUR SQL HERE"
Create a table:
sudo docker exec safe-supabase-db psql -U supabase_admin -d postgres -c "
CREATE TABLE public.my_table (
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
created_at timestamptz DEFAULT now(),
name text NOT NULL
);
ALTER TABLE public.my_table ENABLE ROW LEVEL SECURITY;
"
Enable Realtime on a table:
sudo docker exec safe-supabase-db psql -U supabase_admin -d postgres -c "
ALTER PUBLICATION supabase_realtime ADD TABLE public.my_table;
"
Create a storage bucket:
curl -X POST http://localhost:8787/storage/v1/bucket \
-H 'apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4' \
-H 'Content-Type: application/json' \
-d '{"id":"my-bucket","name":"my-bucket","public":true}'
List auth users:
curl http://localhost:8787/auth/v1/admin/users \
-H 'apikey: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4' \
-H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoic2VydmljZV9yb2xlIiwiaXNzIjoic3VwYWJhc2UiLCJpYXQiOjE3MzU2ODk2MDAsImV4cCI6MTg5MzQ1NjAwMH0.wNfmuJNkX-bZwD7RbjxOChlRf_3Xm4I7bswEYTcDCg4'
Deploy an edge function:
# Create function directory on server
sudo mkdir -p /captain/data/safe-supabase/functions/my-function
# Write index.ts to the function directory
# Function becomes available at /functions/v1/my-function
Restart a service:
sudo docker restart safe-supabase-<service>
# Services: db, kong, auth, rest, realtime, storage, functions, meta, analytics, supavisor, imgproxy, vector, studio
View service logs:
sudo docker logs safe-supabase-<service> --tail 50
DOCKER COMPOSE LOCATION
========================
/captain/data/safe-supabase/compose/docker-compose.yml
PERSISTENT DATA
===============
/captain/data/safe-supabase/db/data — PostgreSQL data
/captain/data/safe-supabase/storage — File uploads
/captain/data/safe-supabase/functions — Edge functions code
/captain/data/safe-supabase/kong — Kong config
IMPORTANT NOTES
===============
- This is a SINGLE PROJECT deployment (not multi-tenant like supabase.com)
- All apps/games share the same database — use schemas or table prefixes to organize
- The anon key is safe to embed in client apps (RLS protects data)
- The service role key must NEVER be in client code (it bypasses all security)
- Always enable RLS on tables and write policies before exposing to clients
- Database is NOT exposed to the internet — only accessible via Kong API or SSH
- Expires: JWT keys expire in ~5 years (2030-01-01)
================================================================================
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment