â„šī¸ Authentication
Admin endpoints require a valid JWT in the Authorization: Bearer <token> header, or in the token cookie set by a successful login. Public endpoints require no auth.

Public Pages (HTML)

Server-rendered HTML pages returned to browsers.

GET /
Home page. Lists active waffles with live spot counts.
GET /waffles
Full public waffle list.
GET /waffle/:slug
Waffle detail page with live spot grid. This is the URL you share with buyers.
GET /buyer/:handle
Public buyer stats page showing win/loss history for an Instagram handle.
GET /about
Public about page. Authenticated admins see additional system information.

Public API — Waffles

GET /api/waffles
List all active (non-archived) waffles. Returns an array of waffle summary objects.
GET /api/waffles/:slug
Get full waffle details by slug, including title, price, total spots, claimed/paid counts, and media links.
GET /api/waffles/:slug/export
Download waffle spot list as CSV. Columns: spot number, status, claimed_by_handle. No auth required.

Public API — Spots & Claims

GET /api/waffles/:slug/spots
Get the full spot grid for a waffle. Returns all spots with number, status, and (if claimed) the Instagram handle.
POST /api/claims
Claim one or more spots. Rate-limited.
{
  "waffle_slug": "blue-wig-3",
  "spot_numbers": [4, 7, 12],
  "instagram_handle": "dani_boo_glass"
}
Returns the updated spot objects or an error if any spot is already taken. Transactionally safe — no double-claims possible.

Public API — Buyers

GET /api/buyers/:handle/stats
Win/loss statistics for an Instagram handle. Returns total claims, wins, and losses.
GET /api/buyers/:handle/history
Full claim history for an Instagram handle across all waffles.

Admin API — Auth

POST /api/admin/login
Authenticate and receive a JWT.
{ "username": "admin", "password": "syrup" }
Returns { "token": "..." } on success. Sets a secure token cookie. Returns 401 on bad credentials; 429 after lockout threshold is exceeded.
POST /api/admin/logout
Clears the session cookie. No body required.
POST /api/admin/forgot-password
Request a password reset token by username.
{ "username": "admin" }
Always returns 200 (does not reveal whether the username exists). A super_admin must retrieve the token and share it with the user out-of-band.
POST /api/admin/reset-password
Reset password using a token.
{ "token": "...", "new_password": "..." }
Token is single-use and expires. Password must meet the password policy.

Admin API — Current Admin

GET /api/admin/me
Returns the authenticated admin's profile: id, username, role, timezone, profile fields.
PATCH /api/admin/me/timezone
Update timezone preference.
{ "timezone": "America/New_York" }
POST /api/admin/change-password
Change your own password. Requires current password.
{ "current_password": "...", "new_password": "..." }

Admin API — Waffles

GET /api/admin/waffles
List waffles. Query param: ?archived=true for archived waffles, ?archived=false (default) for active.
POST /api/admin/waffles
Create a new waffle.
{
  "title": "Blue Wig #3",
  "price_per_spot": "25.00",
  "total_spots": 50,
  "media_links": ["https://www.instagram.com/p/..."]
}
PATCH /api/admin/waffles/:id
Update waffle title, price, or media links. Spot count cannot be changed after creation.
POST /api/admin/waffles/:id/archive
Archive a waffle. Requires admin or higher.
POST /api/admin/waffles/:id/unarchive
Restore an archived waffle to active status.
DELETE /api/admin/waffles/:id
Permanently delete a waffle and all its spots. Requires typing DELETE and providing current password as confirmation.
{ "confirmation": "DELETE", "password": "..." }
Requires admin or higher. Irreversible.
POST /api/admin/waffles/:id/winner
Enter the winning spot number.
{ "spot_number": 17 }
Marks the spot as winner, updates buyer stats for all paid spots.
POST /api/admin/waffles/:id/clear-winner
Clear the current winner. Resets the winning spot to active; recalculates buyer stats.
POST /api/admin/waffles/:id/change-winner
Reassign the winner to a different spot.
{ "spot_number": 23 }
Recalculates buyer stats for all affected spots.

Admin API — Spots

POST /api/admin/spots/:id/pay
Mark a pending spot as paid. Broadcasts a WebSocket update to all connected clients.
POST /api/admin/spots/:id/release
Release a pending or paid spot back to available. Broadcasts a WebSocket update.

Admin API — Admin Management

All endpoints in this section require super_admin role.

GET /api/admin/admins
List all admin accounts with username, role, and active status.
POST /api/admin/admins
Create a new admin account.
{ "username": "jane", "password": "...", "role": "admin" }
Valid roles: super_admin, admin, waffle_manager.
PATCH /api/admin/admins/:id
Update role. Demotions require current password confirmation.
{ "role": "waffle_manager", "current_password": "..." }
PATCH /api/admin/admins/:id/password
Reset another admin's password. The admin should change it on next login.
{ "new_password": "..." }
DELETE /api/admin/admins/:id
Deactivate an admin account. Requires current password.
{ "current_password": "..." }
Deactivated admins cannot log in. Their audit history is preserved. They can be reactivated.

Admin API — Reports

Available to all roles.

GET /api/admin/reports/drought
Buyers with the most losses and no recent win, sorted by loss streak.
GET /api/admin/reports/power-buyers
Buyers ranked by total paid spots across all waffles.
GET /api/admin/reports/monthly-activity
Waffles run, spots claimed, and payments received grouped by calendar month.
GET /api/admin/reports/spot-velocity
Time from waffle creation to last spot claimed, per waffle. Shows how quickly your drops sell out.

Admin API — Audit Log

Requires admin or super_admin.

GET /api/admin/audit
List audit log entries with pagination. Query params: ?page=1&per_page=50&admin_id=&action=&from=&to=
GET /api/admin/audit/:id
Get a single audit log entry by ID.
GET /api/admin/audit/export
Export filtered audit log entries as CSV. Same filter params as the list endpoint.

Admin API — Login History

GET /api/admin/login-history
List login history. Visibility is role-scoped:
  • waffle_manager — own entries only
  • admin — own + waffle_manager entries
  • super_admin — all entries
Optional query param: ?admin_id= (super_admin only).

Admin API — Users

GET /api/admin/users
List all registered buyer handles (the users registry). Returns handle and first-seen timestamp.

Admin API — Settings

Requires super_admin.

GET /api/admin/settings
Retrieve current system settings (WHOIS server, JWT expiry, retention periods, lockout config).
PATCH /api/admin/settings
Update one or more system settings.
{
  "whois_server": "whois.pwhois.org",
  "jwt_expiry_hours": 24,
  "audit_log_retention_days": 90,
  "login_history_retention_days": 90
}

WebSocket

WS /ws/:slug
Connect to the real-time hub for a waffle. Replace :slug with the waffle slug. The client receives JSON messages whenever a spot changes state.

Message format

{
  "type": "spot_update",
  "spot": {
    "id": 42,
    "number": 7,
    "status": "paid",
    "claimed_by_handle": "dani_boo_glass"
  }
}

Message types

typeTrigger
spot_updateAny spot status change (claim, pay, release, winner)
winnerWinner entered — includes winning spot and handle
pingServer heartbeat (every ~30s) — client should reply pong

Reconnection

The client-side WebSocket implementation (websocket-client.js) uses exponential backoff with jitter and a maximum retry cap. Stale connections are detected via the server-side ping/pong heartbeat.

Health & Readiness

GET /health
Returns 200 with {"status":"ok","db":"connected"} when healthy. Returns 503 if the database is unreachable.
GET /ready
Readiness probe for container orchestrators. Same response format as /health.