尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證製作者的介紹僅有上面的一句話。 更多詳情請閱讀儲存庫 README。 儲存庫 README ↗
正在載入完整 README。
檢視完整 README ↗Tournament management with live scoring for tonament.com. Badminton, pickleball, table tennis, squash, tennis, padel, volleyball, beach volleyball, throwball, sepak takraw, football, futsal, hockey, kabaddi, basketball, snooker, pool and carrom today; new sports are plug-in modules.
public/sports/timed.js):
public/sports/tie.js: 2 singles + 1 doubles, 3 singles + 2 doubles (Thomas/Uber), Sudirman (MS, WS, MD, WD, XD), 4 singles + 1 doubles (Davis/Corbillon), 5 singles (Swaythling), 3 doubles, 1 singles + 2 doubles, or a custom odd list; optionally every match is played. Before each tie, each captain (whoever entered the team, or the roster's captain) or an organiser picks who plays which match from the roster — a player plays at most one singles and one doubles — and the line-ups stay sealed until both are in. Each match is scored with the sport's rules (live or the final score); its winner goes into the tie's score (matches.rallies holds the matches' winners), so groups, standings (ties won, then matches won), the knockout and the order of play work as usual. Logic in server/ties.js.public/regions.js, so filters group cleanly; there's also "Other" for any country. Players get an "Open in Google Maps" link. Venue and dates stay editable after the start./new is one screen: name, date, sport, events and format (or start from a template or a tournament you ran, with its events, format, scoring, fees, prizes and venue — never its players, dates or results). Any event's settings can be copied to other events still being set up ("Copy to other events…": settings that don't fit — partners for singles, ties for non-team events — are left and listed), and new events can take their settings from any event. Setup › Events has Run it again (a new edition, the year moved on) and Save as a template (/templates lists them). A new edition offers to tell last time's players it's back (a notification and email, once each). Server: setupOf, createFrom, copyEventSettings, invitePrevious in server/service.js.server/ai.js, with Claude): on /new, "Start from your poster or Google Form" — a photo or PDF of the poster, a Google Form link (read from the public form page; only Google Forms hosts) or the announcement's text becomes a draft to check (guesses marked "Check this", event problems listed); nothing is saved until Create. On the Players tab, Import from Google Form takes the responses' CSV (or a published sheet's CSV link): Claude maps the columns from the headers and a few rows with emails and phone numbers masked (saved on the tournament, so the next import skips the call), then server/importer.js turns every row into entries without AI — each ticked category an entry, partners paired or marked "find me a partner", Indian mobiles as +91, duplicates merged (paid if either response says so), problems flagged — with a preview before anything is added. Imported players aren't messaged. Off without ANTHROPIC_API_KEY; each call is logged in ai_calls with its cost (on the admin Funnel); 10 drafts per person and 200 site-wide a day.public/schedule.js, shared by the server (auto-call) and the browser./t/<id>/courts, /t/<id>/m/<match>, /new, /?sport=…) with the History API; old #/… links are converted on load. Share-key and password-reset links keep their secret after the # (/#/t/<id>?key=…, /#/reset?token=…) so it never reaches the server or its logs. GET /t/<id> returns the app page with the tournament rendered in (server/pages.js): title, description, canonical, link-preview tags (the cover, or a 1200×630 copy made in the browser on upload) and the details as HTML, which the app takes over once loaded. Public tournaments with a date and place also get schema.org SportsEvent data for Google's event results; link-only tournaments and match pages are noindex; private ones show nothing. /sitemap.xml lists the main pages and every public tournament; robots.txt keeps account pages out; unknown URLs return a real 404. On Vercel, /t/* and /sitemap.xml are rewritten to the API function (which bundles public/index.html), and the app's other pages to index.html./how-it-works (steps for organisers and players, with screenshots) and /faq (with FAQPage data for search). The screenshots in public/guide/ are real app screens of a seeded demo tournament, in the dark and light themes (each page loads only the visitor's theme). Regenerate them after UI changes with NODE_PATH=$(npm root -g) node scripts/guide-shots.js (needs Playwright with Chromium).upi:// link with the payee, amount and their TN<entry> reference filled in, the organiser's QR code, or the UPI ID to copy) or in cash — then tap "I've paid" (optionally with the 12-digit UPI reference). The organiser checks their UPI app and taps Received (or Not received, Paid in cash, Waive) in "Payments to check" on the Players tab; the player is emailed either way. An unpaid entry holds its place for 24 or 48 hours or until entries close (the organiser's choice), then falls to the waitlist; a player who has said they paid keeps the place until the organiser decides. Tonament never handles this money. Tournaments organised by Tonament still take fees online through Razorpay./t/<id>/display is a read-only big-screen view for a TV or projector (organisers get an "Open venue display" button on the Courts tab and a "Venue display" link in the Next step card on Live). It always uses the dark theme and fits one landscape screen: every court with its live score (or the matches on court now when no courts are set), "Up next" from the order of play with estimated times, and a panel rotating every 15 s through latest results, group standings, the current knockout round, champions, prizes and the link to follow on a phone. It updates itself, shows "Reconnecting…" when offline, keeps the screen awake (Wake Lock), hides the mouse pointer when idle, has a full-screen button, and shows the organiser's logo and sponsors.media table), then served from /api/media/:id with long caching./clubs/new): a club, team, academy, venue, company, school or coaching squad. People join with the invite link (or ask, when it isn't open), admins set roles (captain, coach, admin), and the club has a feed, members and a leaderboard at /club/<slug>. A paid page adds the public, searchable /@slug; without one, /@slug opens the club in the app. Players and clubs can be followed; the home page shows a feed of podiums, confirmed group games and club news from people and clubs you follow, with kudos, comments, reporting and blocking. Under-18s (by birth year) stay out of strangers' feeds. Logic in server/clubs.js and server/social.js./img/<kind>/<id>.png draws a branded image (Outfit on Deep Navy with the lime slashes, server/cards.js, rasterised with @resvg/resvg-js from the static Outfit files in server/fonts/) for a feed item (a), a club, a player (p) or a tournament (t), 1200×630 for link previews or ?size=story for 1080×1920. They're the link previews for /club/<slug>, /a/<id> (one shared feed item), profiles, club pages and tournaments without an uploaded image. Only public things get a card: private tournaments and profiles, followers-only items and under-18s are a 404. The app's Share sheet offers WhatsApp, the story image, Copy link and the phone's share menu./notifications for comments, kudos, new followers, club join requests, a club letting you in, and group results to confirm (server/notify.js). Never sent to the person who did it, or across a block. They also go to phones as web push once the person taps Turn on (needs VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY and optionally VAPID_SUBJECT — make a pair with npx web-push generate-vapid-keys; without them there's no push), and join requests by email, with a link that turns those emails off. People choose phone and email per kind on the Notifications page./challenges/new) to a game, one v one or in pairs, in any sport played that way. They accept or decline (unanswered ones expire after 14 days); the game is scored at /c/<id> live or as a final score, and the losing side confirms it (it confirms itself after 24 hours; a disputed one doesn't count). A confirmed game moves each player's Tonament rating for that sport (Elo, ratings table) — so do confirmed group games — and goes to the feed with a share card and Challenge back. At most 10 unanswered challenges at a time; no challenges across a block. Logic in server/challenges.js./img/ladder/<id>.png is a top-five share card. Logic in server/ladders.js./goals — "30 games this month". Personal goals (everyone who joins chases the target; friends join by link), a club's total, or club v club (whose members play more), set by a club's admins and shown above the club's feed. Progress counts confirmed group games, challenges and tournament matches between the goal's start and end, as games played or won, in one sport or any. Reaching a target (or winning club v club) goes to the feed and the inbox once; /goals/<id> has a link preview and /img/goal/<id>.png a share card. Up to 92 days. Logic in server/goals.js./cc/<id> (link preview) and /img/cc/<id>.png. Unanswered challenges expire after 14 days. Logic in server/club-challenges.js./season/<id> (link preview) and /img/season/<id>.png (the table). Logic in server/seasons.js./runs — log a run (distance, time, day, name, place; everyone, followers or only me). It goes to the feed with kudos, comments and a share card, unless private. The page shows this week, month and year, best efforts (fastest pace over a run at least 5K, 10K, half or full marathon long; beating one is flagged on the post), and this week's km against the people you follow. Goals can count km run or runs. Logic in server/runs.js./club-run/<id> takes one tap: the run gets the club's name and place and goes on the club's feed. The page shows the regulars (most runs in 90 days). Logic in server/club-runs.js./races — anyone (or a club's admins, for the club) sets up a race with distances (5K, 10K, half, marathon, or any km). Runners enter and get the next bib; organisers add walk-ins, close entries, and type finish times by bib on the day (or mark a DNF). Results rank each distance overall, by gender and by age group; publishing them turns each linked finisher's time into a run (bests, totals, km goals), posts their finish to the feed with a share card, and tells everyone in it — corrections afterwards keep runs and posts up to date. /race/<id> has a link preview and /img/race/<id>.png a card. Logic in server/races.js./meets — athletics, swimming and track cycling. An organiser (or a club's admins) picks events from the sport's list for men, women or open, and the number of lanes. Athletes enter themselves (for one of their clubs) or the organiser pastes a list (Name, Team, email). Timed events with more entries than lanes draw heats; the best heat marks make the final, seeded into the middle lanes; field events go straight to a start list. Organisers type marks (10.85, 1:52.30 or metres; DNS/DNF/DQ); finishing an event places everyone (ties share), marks personal bests, scores team points (8-6-5-4-3-2-1) and posts each linked athlete's result; when every event's done the top team's post goes out. Events can be reopened to fix a mark. /meet/<id> has a link preview and /img/meet/<id>.png the team table. Logic in server/meets.js./champs — motorsport championships (karting and club racing). An organiser (or a club's admins) sets up rounds; drivers join with a number and team, or the organiser pastes a list. Each round: qualifying laps (to the thousandth) set the grid; the race is entered as position, laps, best lap and DNF/DNS/DSQ; finishing it classifies the field (DNFs by laps), gives 25-18-15-12-10-8-6-4-2-1 plus 1 for the fastest lap in the points, and posts the podium. Drivers' and teams' standings add up; rounds can be reopened; closing the championship crowns the champion. /champ/<id> has a link preview and /img/champ/<id>.png the standings. Logic in server/champs.js./clubs/rankings ranks clubs, academies and venues by those points (by city and kind). Players choose the club they represent on their profile: it goes on their race entries and is the first club offered when they enter a meet. Logic in server/honours.js./play: groups, challenges, goals, runs), Compete (/compete: tournaments, races, meets and racing — find, enter, or run your own), Clubs, and You (/you). On desktops (1024px and up) a sidebar shows every hub with its parts (and your clubs), the top bar has a Find events box, and Home and the hubs get a side column (getting started, up next, goals, what's on near you). On phones the hubs are a bar at the bottom (pages with their own bar, a tournament or a club, keep theirs). The avatar opens the account menu everywhere. + New opens one sheet with everything you can start. Someone new gets a "what would you like to do?" home with four ways in, a getting-started checklist, events near them and clubs to join; signed out, the home page explains what Tonament does for organisers, players, clubs and runners.rally_times, milliseconds, beside the rallies string) for video, highlights and stats later. The phone sends the time of each tap, so queued offline taps keep theirs; the server clamps times in the future or before the last rally, undo drops the last, and a score entered at the end has none. Logic in server/rally-times.js.ADMIN_EMAILS can manage every tournament and see all users at /#/admin.Free for tournaments up to 16 entries, counted across all events. Bigger tournaments buy a one-time plan for that tournament: Club (up to 64 entries, ₹499), Open (150, ₹999) or Championship (400, ₹1,999). Upgrading pays only the difference; tournaments that bought the original unlimited Event Pass keep it. Tiers live in server/billing.js. Payment uses Razorpay Standard Checkout (UPI, cards, netbanking, wallets):
The limit applies only once Razorpay keys are set.
Entry fees (tournaments organised by Tonament only). A site admin can mark a tournament "Organised by Tonament" (Team tab, or POST /api/admin/tournaments/:id/official). Its events can then set an entry fee. Players pay through the same Razorpay checkout when they enter; the money goes to Tonament's account.
Players are never shown the free limit. Past it, self-entries are still accepted but saved as unconfirmed: the player sees "Entry received — the organiser will confirm your place", and the owner is emailed once. Unconfirmed entries aren't drawn. Moving up a plan confirms them automatically, or the organiser can confirm or remove them one by one. Organisers can also set their own maximum entries: players then see "N of M places left", and once it's reached new entries join a waitlist (the same unconfirmed state). A maximum above the plan's limit needs a bigger plan.
Environment variables (Vercel → Settings → Environment Variables):
| Name | Example | Notes |
|---|---|---|
RAZORPAY_KEY_ID | rzp_test_… | Test keys first, live keys after activation |
RAZORPAY_KEY_SECRET | Never exposed to the browser | |
RAZORPAY_WEBHOOK_SECRET | The secret you choose when adding the webhook | |
FREE_ENTRY_LIMIT | 16 | |
BUSINESS_NAME, BUSINESS_ADDRESS, BUSINESS_EMAIL, BUSINESS_PHONE, BUSINESS_GSTIN | Shown on /contact, the footer and receipts |
Razorpay activation checklist:
/pricing, /contact, /terms, /privacy, /refunds and /delivery, and have the policy text reviewed (it's a starting draft).https://tonament.com/api/billing/razorpay/webhook with events payment.captured, order.paid and payment.failed, and the same secret as RAZORPAY_WEBHOOK_SECRET.Site admins see all payments and revenue on the admin page, and can switch a tournament's Pass on or off by hand (refunds, promotions).
The look follows the Tonament brand: Deep Navy, Electric Lime, Slate and Mist, the Outfit typeface, and the T mark with its lime dot. Dark theme is the default, with a light theme and an Auto/Dark/Light switch. The full rules are in docs/BRAND.md. Logos and icons are in public/brand/; to rebuild them, run python3 scripts/brand/generate_logos.py && node scripts/brand/render.js.
You need Node.js 20 or later.
npm install
npm start # http://localhost:3000
npm run dev # restarts on file changes
npm test
To test emails locally without sending anything, run EMAIL_LOG=1 npm start; emails are printed in the terminal instead (MESSAGING_LOG=1 does the same for WhatsApp and SMS). You don't need to install a database to run it locally. Without DATABASE_URL, the app uses an embedded Postgres (PGlite) and stores its files in data/pglite; delete that folder to start fresh. To use a real Postgres server instead, set DATABASE_URL=postgres://…. To run the tests against a real server, set TEST_DATABASE_URL.
Import the project. In Vercel, choose Add New → Project, import tharunjd-VS/tonament, and keep the defaults. No build step is needed; vercel.json sets everything up.
Add a database. In the project, open Storage → Create Database → Neon (Postgres) and connect it to the project. This sets DATABASE_URL for you. The tables are created automatically on the first request.
Make yourself site admin. Under Settings → Environment Variables, add ADMIN_EMAILS with your email address. For several admins, separate the addresses with commas.
Redeploy so the new environment variables are picked up.
Optional: turn on email (password reset and team invites) with Resend. Verify tonament.com in Resend by adding its DNS records at your domain registrar, then set:
RESEND_API_KEY: a key with sending accessEMAIL_FROM: e.g. Tonament <[email protected]>APP_URL: https://tonament.com (the base for links in emails)Without these the app works normally; it just doesn't send email.
Turn on the scheduled jobs. Add CRON_SECRET: any long random string you make yourself (for example openssl rand -hex 32). Vercel Cron (set in vercel.json) sends it to /api/cron/hourly every hour (it settles challenges, ladder forfeits, club challenges and goal badges) /api/cron/weekly on Mondays at 09:01 IST (the admins' summary email), and /api/cron/reminders daily at 18:01 IST (a reminder the day before to everyone in a tournament, race, meet, group session or club run tomorrow — in the app, by push and by email, once each; people can turn them off under Notifications). Each run is recorded and shown under Admin → Overview → Scheduled jobs, where a site admin can also run one by hand.
Later: WhatsApp and SMS. Messages go through server/messaging.js, which already handles email. WhatsApp and SMS stay off (and nothing about them shows in the app) until their settings are added:
WHATSAPP_PROVIDER=meta, , (or with , , ). Get the templates named in approved in Meta's WhatsApp Manager first ( as an Authentication template).How it works on Vercel:
public/ is served as static files from Vercel's CDN./api/* request is rewritten to a single function, api/index.js, which shares its request handler with the local server.s-maxage=1, so Vercel's CDN answers most of those requests. Hundreds of spectators cost roughly one database read per second for each tournament. Organiser requests use a separate URL (?admin=1) and are never cached.SELECT … FOR UPDATE), so two taps arriving at once on different function instances are applied one after the other.HttpOnly, SameSite=Lax cookie (plus Secure over HTTPS), and only a SHA-256 hash of each token is stored. Sessions last 30 days.Origin is another site are rejected.?me=1 and are never cached.APP_URL (or the project's production domain on Vercel), never the request's Host header.public/sports/<sport>.js), then saves it in the background. Each request says which rally count it applies to (at), so retries can't double-count and two devices can't overwrite each other. If the connection drops, points are kept and sent automatically when it returns."regions": ["sin1"] (for example) to vercel.json. For players in India, Singapore is a good region for both (sin1 on Vercel, aws-ap-southeast-1 on Neon).Server-Timing: app;dur=… header. In DevTools (Network → Timing), a large gap between that number and the total request time means the time is going on the network, not on the server.api/index.js Vercel function entry point
server/
index.js local / self-hosted HTTP server
app.js routing, caching headers, static files
service.js tournament lifecycle and role checks: entries, draw, scoring, team
auth.js sign up / sign in, password hashing, sessions, password reset
email.js Resend sender and email templates (invites, resets, receipts)
messaging.js messages by channel: email, WhatsApp, SMS (off until configured), logged and capped
message-templates.js every message by name, in each channel's form
sources.js where sign-ups come from (first touch: utm, shared links, referrer)
cron.js scheduled jobs (/api/cron/*): hourly settling, the weekly admin email
ai.js AI setup with Claude: poster/form/text → tournament draft; form columns → a mapping
importer.js Google Form responses → entries (masking, categories, partners, phones, duplicates)
csv.js a small CSV parser
billing.js Razorpay orders, signature checks, billing settings
formats.js re-exports public/formats.js: round-robin, snake groups, bracket seeding, standings
pages.js server-rendered pages: tournaments, sports, cities, clubs, players, rankings, blog
tools-pages.js the free fixture tools' pages (/tools/*)
blog.js blog posts from content/blog/*.md (front matter + a small Markdown renderer)
db.js Postgres (pg) or embedded PGlite, plus the schema
sports/<sport>.js re-exports public/sports/<sport>.js (the scoring rules, shared with the browser)
sports/index.js sport registry
public/ single-page front end (plain JS, no build step)
brand/ logos, app icons, email logo, link-preview image
fonts/ Outfit (self-hosted)
formats.js draw logic shared by the server and the free fixture tools (tools.js)
content/blog/ blog posts, one Markdown file each (see docs/BLOG.md)
docs/BRAND.md brand guidelines
docs/BLOG.md how to write a blog post
scripts/brand/ logo generator and PNG renderer
test/ unit and integration tests
vercel.json Vercel config: static output, API rewrite and crons
Badminton, pickleball, table tennis, squash, tennis, padel and the volleyball family are playable today. server/sports/index.js also lists the sports on the way, shown on the home page as "Soon"/"Later" chips that signed-in users can tap to request. Site admins see the totals on the admin page. Next: football, futsal, hockey, kabaddi and basketball (goal/point scoring with draws). Cricket comes later.
Create public/sports/<sport>.js with the same exports as badminton.js (id, name, defaults, fields, normalizeConfig, compute(rallies, config, firstServer) and summary), wrapped the same way so both the server and the browser can load it. Rally-scored sports can use public/sports/rally.js, which runs games, win-by-2 and best-of: the sport supplies only whether a rally scores and how the serve moves (see squash.js); set sports are a few lines with setSport (see volleyball.js), and tennis-style sports with racketSport in tennis-core.js. A sport's event types (singles/doubles/team) are set in SPORT_LISTS in public/sports/events.js. Add a one-line server/sports/<sport>.js re-export, add the id to MODULES in server/sports/index.js, set its catalogue status to live, and add its <script> tag to public/index.html (after rally.js).
All endpoints are under /api. Permission comes from the signed-in session cookie, or from a share link sent in the X-Admin-Key header. Add ?me=1 to GET /tournaments/:id to get your role, permissions and share links.
| Method | Path | Purpose |
|---|---|---|
| POST | /auth/signup, /auth/login, /auth/logout | Accounts (session cookie) |
| POST | /auth/forgot { email }, /auth/reset { token, password } | Password reset by email |
| GET | /auth/me, /me/tournaments | Current user; tournaments you're on the team of |
| GET | /admin/users | All users (site admins only) |
| GET | /billing/config | Public payment settings (price, free limit, Razorpay key id, business details) |
| POST | /tournaments/:id/checkout { tier } | Create a Razorpay order for a plan or upgrade (organisers; defaults to the next plan up) |
| POST | /billing/razorpay/verify | Verify a completed checkout and activate the Pass |
| POST | /billing/razorpay/webhook | Razorpay webhook (signature-checked against the raw body) |
| GET | /admin/payments; POST /admin/tournaments/:id/plan { plan } | Payments and revenue; manual Pass on/off (site admins) |
| GET | /sports | Sport catalogue: playable sports (with scoring options) and upcoming ones (status: soon / later) |
| GET | /sports/interest | Request counts per upcoming sport, plus the sports you requested |
| POST | /sports/:id/interest { want } | Request (or withdraw a request for) an upcoming sport; signed-in users, one vote each |
| GET/POST | /tournaments | Find public tournaments (?q&sport&country®ion&city&when=upcoming|week|month|past|any&open=1&today=YYYY-MM-DD) / create one |
| GET | /tournaments/places | Countries, states and cities that have public tournaments (for the filters) |
| GET/PATCH/DELETE |
WHATSAPP_TOKENWHATSAPP_PHONE_IDWHATSAPP_PROVIDER=gupshupWHATSAPP_TOKENWHATSAPP_SENDERWHATSAPP_APPserver/message-templates.jstonament_otpSMS_PROVIDER=msg91, SMS_KEY, SMS_SENDER and SMS_TEMPLATES (JSON mapping each template name, like otp.login, to its DLT template id); or Twilio: SMS_PROVIDER=twilio, SMS_KEY (SID:token), SMS_SENDER.MESSAGING_DAILY_CAP (default 500) limits WhatsApp and SMS messages per channel per day.Once either is on, people can confirm their mobile on the Notifications page, sign in with a code, and choose WhatsApp per kind of notification (WhatsApp only after they opt in; SMS only for codes and reminders). Locally, MESSAGING_LOG=1 npm start turns both on in log-only mode: messages are printed in the terminal.
Optional: AI setup. Add ANTHROPIC_API_KEY (from the Claude Console) to turn on "Start from your poster or Google Form" and the Google Form import's column matching. It never reaches the browser. In the Console, set a monthly spend limit and leave auto-reload off. It uses Claude Opus 5.5 (AI_MODEL picks another, e.g. claude-sonnet-5-5 or claude-haiku-5-5); a poster costs a few rupees, a column mapping much less. Without the key the app works as before, and imports match columns by their names.
Add the domain. Under Settings → Domains, add tonament.com and www.tonament.com, then follow the DNS instructions Vercel shows.
/tournaments/:id |
| View / change settings (during setup) / delete |
| GET/POST | /tournaments/:id/members | Team list / add or change a member { email, role } |
| DELETE | /tournaments/:id/members/:memberId | Remove a team member |
| POST | /tournaments/:id/claim | Add an ownerless tournament to your account (needs the organiser link) |
| POST | /tournaments/:id/entries | { names: [...], walkIn } |
| POST | /tournaments/:id/visibility { visibility } | private, link or public (owner only) |
| POST/PATCH/DELETE | /tournaments/:id/events, /tournaments/:id/events/:eventId | Add events ({ events: [...] }), change one (type, age, level, name, format, scoring, places, fee), delete one (before it starts) |
| PUT | /tournaments/:id/events/order { ids } | Reorder events |
| POST | /tournaments/:id/register | Self-entry by the signed-in player { name?, partner?, phone?, solo?, players? } while entries are open; returns confirmed: false past the tournament's capacity |
| POST | /tournaments/:id/entries/:entryId/leave | The signed-in player withdraws their own entry before the draw |
| GET | /me/entries | The signed-in player's entries across tournaments, with their status |
| POST | /tournaments/:id/entries/:entryId/fee { status } | Record an entry fee as cash (paid in person), waived, or null (official tournaments) |
| POST/DELETE | /tournaments/:id/pairs { a, b }, /tournaments/:id/pairs/draw { event, balance }, /tournaments/:id/pairs/:entryId | Pair two solo players, draw pairs for everyone waiting, or split a drawn pair |
| POST | /tournaments/:id/entries/:entryId/confirm | Confirm a waiting self-entry (within the plan's limit) |
| POST | /tournaments/:id/entries/:entryId/check-in { here }, /tournaments/:id/check-in-all | Event-day check-in |
| POST | /tournaments/:id/redraw | Undo the draw (only before the first point) |
| POST | /tournaments/:id/late-entry { name } | Add a player after the start (when late entries are allowed) |
| POST | /tournaments/:id/entries/:entryId/withdraw | Withdraw a player; remaining matches become walkovers |
| PUT | /tournaments/:id/entries/order | { ids: [...] } sets the seeding |
| DELETE | /tournaments/:id/entries/:entryId | Remove an entry |
| POST | /tournaments/:id/start { skipUnconfirmed } | Make the draw and fixtures (unconfirmed entries must be confirmed, removed, or skipped explicitly) |
| POST | /tournaments/:id/knockout | Draw the bracket from the group standings |
| POST | /tournaments/:id/matches/:mid/rally | { side: "a" | "b" } |
| POST | /tournaments/:id/matches/:mid/undo | Remove the last rally |
| PATCH | /tournaments/:id/matches/:mid | { court, firstServer, status: "live" } |