Table of contents
- Troubleshooting
- Startup and configuration
- JWT_SECRET is required and must contain at least 32 characters
- Configuration error: PUBLIC_BASE_URL must be a valid http(s) URL
- TRUSTED_PROXIES entry "..." must be an IP address, CIDR range, or ... preset
- The server exits immediately
- Database
- Invalid URL
- password authentication failed for user "..." / code 28P01
- ECONNREFUSED to db:5432
- Migrations fail
- Accounts or scores disappeared
- SQLite better-sqlite3 build fails
- E-mail and SMTP
- tls_validate_record_header:wrong version number
- 502 on resend verification
- 503 This server has no SMTP configuration
- Password reset mail never arrives
- HTTP and proxying
- CORS errors in the web app
- Rate limits trigger for everyone at once
- Logs show ip=10.x.x.x for every request
- 413 Payload Too Large
- 429 Too many attempts
- Accounts and sessions
- 401 Session expired. Sign in again.
- 401 Invalid or expired session
- 409 That username or e-mail address is already registered
- Account deletion fails
- Leaderboard
- 403 The leaderboard is disabled on this server
- 400 Unknown leaderboard game
- A new score did not appear
- Chess
- 403 Online chess is disabled on this server
- 409 It is not your turn
- 409 The match changed while your move was being saved
- 400 That move is not legal in this position
- Releases and updates
- Management panel
- Containers and disk
Troubleshooting
Startup and configuration
JWT_SECRET is required and must contain at least 32 characters
Set a random secret in .env:
openssl rand -base64 48
Configuration error: PUBLIC_BASE_URL must be a valid http(s) URL
Use an absolute URL with http:// or https:// and no path:
PUBLIC_BASE_URL=https://account.example.com
APP_PUBLIC_URL=https://games.example.com
TRUSTED_PROXIES entry "..." must be an IP address, CIDR range, or ... preset
Use a valid IP, CIDR or one of loopback, linklocal, uniquelocal. Remove hostnames, schemes and spaces inside entries.
The server exits immediately
Check the logs:
docker compose logs accounts
Common causes are a missing JWT_SECRET, invalid URL, invalid proxy entry or database connection failure.
Database
Invalid URL
Older releases built DATABASE_URL by string concatenation. Passwords containing @, :, /, ?, # or % produced an invalid URL.
Release 1.2.1+ builds the URL with encodeURIComponent. Fixes:
- Upgrade to the current release.
- Prefer
POSTGRES_*and leaveDATABASE_URLempty. - If you must set
DATABASE_URL, percent-encode the password.
# Preferred
POSTGRES_HOST=db
POSTGRES_DB=universal_gaming
POSTGRES_USER=universal_gaming
POSTGRES_PASSWORD=p@ss:word/with?special#chars
password authentication failed for user "..." / code 28P01
POSTGRES_PASSWORD only initializes a new data directory. Changing it in .env does not alter an existing role.
- Confirm the container's effective environment:
docker compose exec accounts env | grep -E 'POSTGRES|DATABASE_URL'
- Change the role password inside PostgreSQL:
docker compose exec db \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
-c "ALTER ROLE \"$POSTGRES_USER\" WITH PASSWORD '<new-password>';"
- Update
.envand restart.
Do not delete the PostgreSQL volume or data directory. It contains all accounts, scores and matches.
ECONNREFUSED to db:5432
docker compose psshould showdbhealthy.- The
accountsservice depends on the database health check. - If running Node on the host, use
POSTGRES_HOST=127.0.0.1, notdb.
Migrations fail
- Read the exact SQL error in the logs.
- Take a backup before retrying.
- Do not edit a released migration; add the next version.
- If the schema is partially changed, restore the pre-update dump and retry.
- Verify
schema_migrations:
docker compose exec db \
psql -U "$POSTGRES_USER" -d "$POSTGRES_DB" \
-c 'SELECT * FROM schema_migrations ORDER BY version;'
Accounts or scores disappeared
Check whether the Compose project name or volume changed. Compose uses the project name universal-gaming and the volume postgres-data.
docker volume ls
docker compose ps
Look for an older volume before doing anything destructive. Restore from a dump if needed. Never initialize a fresh database over existing data.
SQLite better-sqlite3 build fails
Install build tools and retry:
npm rebuild better-sqlite3
The Docker image already includes the required build dependencies.
E-mail and SMTP
tls_validate_record_header:wrong version number
Cause: implicit TLS was requested on a STARTTLS port (commonly SMTP_SECURE=true with port 587). OpenSSL tries to parse the plaintext SMTP greeting as a TLS record.
Fix:
SMTP_PORT=587
SMTP_SECURE=false
SMTP_REQUIRE_TLS=true
For port 465:
SMTP_PORT=465
SMTP_SECURE=true
Restart and check the startup log:
[mail] SMTP host=... port=587 secure=false requireTLS=true
Release 1.4.0+ auto-corrects SMTP_SECURE=true on a STARTTLS port and logs a warning, but the environment should still be fixed.
502 on resend verification
The SMTP server rejected or timed out the message. Check:
docker compose logs --tail=100 accounts
Then verify credentials and connectivity from inside the container:
docker compose exec accounts node -e "
import('./src/config.js').then(async ({loadConfig}) => {
const {createMailer} = await import('./src/mailer.js');
const m = createMailer(loadConfig());
await m.verify();
console.log('SMTP verify ok');
});
"
503 This server has no SMTP configuration
Set both SMTP_HOST and SMTP_FROM. Without them the mailer is disabled and verification/reset mail cannot be sent. Accounts still work.
Password reset mail never arrives
- The account must have an assigned and confirmed e-mail.
EMAIL_CONFIRMATION_ENABLEDmust allow confirmation.- SMTP must be configured.
- The endpoint always returns 202, even for unknown addresses, to prevent enumeration.
- Check spam and the SMTP provider's logs.
- Reset links expire after 30 minutes by default.
HTTP and proxying
CORS errors in the web app
Set the exact origin:
CORS_ORIGINS=https://games.example.com
- Include the scheme.
- No trailing slash.
- Multiple origins are comma-separated.
*is for development only.
Rate limits trigger for everyone at once
The backend sees the proxy IP instead of the client IP. Set TRUSTED_PROXIES to the proxy address or CIDR:
TRUSTED_PROXIES=127.0.0.1,::1
Ensure the proxy sends X-Forwarded-For. Do not use TRUST_PROXY=true publicly.
Logs show ip=10.x.x.x for every request
Same cause: trusted proxies are not configured, or the proxy is not forwarding the client IP.
413 Payload Too Large
The JSON body limit is 16 KB. Send smaller payloads; this limit is intentional.
429 Too many attempts
Wait for the window to pass. Limits are listed in API Reference. If legitimate traffic is affected, check proxy/IP configuration before raising limits.
Accounts and sessions
401 Session expired. Sign in again.
The token's auth_version no longer matches the account. This happens after a password reset. Sign in with the new password.
401 Invalid or expired session
- The token expired.
JWT_SECRETchanged.- The token is malformed or from another server.
409 That username or e-mail address is already registered
Usernames and e-mails are compared case-insensitively. Choose another value or sign in.
Account deletion fails
The request requires the current password:
{ "password": "..." }
A wrong password returns 401.
Leaderboard
403 The leaderboard is disabled on this server
Set LEADERBOARD_ENABLED=true and restart.
400 Unknown leaderboard game
Use one of the ten ranked IDs:
sudoku, killer-sudoku, mahjong, minesweeper, klondike, classic-solitaire, spider, freecell, pyramid, tripeaks.
A new score did not appear
Only the best score per user, game and mode is stored. A worse time or move count does not replace the existing row. Check the improved field in the response.
Chess
403 Online chess is disabled on this server
Set CHESS_ENABLED=true and restart.
409 It is not your turn
Refresh the match. The client may be showing stale state.
409 The match changed while your move was being saved
Two writes raced. Fetch the match again and retry.
400 That move is not legal in this position
The server replays the full history with chess.js. The move is illegal in the stored position, or the client is out of sync.
Releases and updates
404 Release proxy is disabled
Set UPDATE_ENABLED=true and restart.
502 Release source returned ...
The upstream Gitea release feed is unreachable or returned an error. Check UPDATE_SOURCE_URL and outbound network access.
App shows an old version
- Check
https://games.example.com/version.jsonandlatest.json. - The release proxy caches for
UPDATE_CACHE_SECONDS(default 300). - The browser may cache the service worker; hard-refresh or clear site data.
Management panel
/management returns 404
MANAGEMENT_PASSWORD is empty. Set it and restart. The panel is intentionally disabled without a password.
/management keeps asking for a password
Use MANAGEMENT_USER (default admin) and the exact MANAGEMENT_PASSWORD. Basic auth has no logout; close the browser or use a private window.
Containers and disk
no space left on device
df -h /
docker system df
docker image prune -f
Prune old images, not volumes. Database dumps and images are the usual consumers.
Container restarts repeatedly
docker compose ps
docker compose logs --tail=200 accounts
docker compose logs --tail=200 db
Check configuration, database health and migrations before restarting in a loop.
Universal Gaming Accounts
- Home
- Deployment
- Configuration
- API Reference
- Database and Migrations
- Security
- Operations
- Development and Testing
- Troubleshooting
Repositories
The accounts backend is optional. Universal Gaming remains fully playable offline without it.