From c1cbd5d9802a757b69e980fe7f7e65fa73d97a8b Mon Sep 17 00:00:00 2001 From: oscartobar Date: Fri, 24 Oct 2025 20:46:22 -0500 Subject: [PATCH 01/14] Prevents an index error from being generated when using a clean production version --- .../274385f2a757_add_on_delete_cascade_to_users_x_clubs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py b/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py index 5f447ec75..11d3bd813 100644 --- a/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py +++ b/backend/alembic/versions/274385f2a757_add_on_delete_cascade_to_users_x_clubs.py @@ -17,7 +17,7 @@ def upgrade() -> None: # ### commands auto generated by Alembic - please adjust! ### - op.drop_index("ix_users_email", table_name="users") + op.drop_index("ix_users_email", table_name="users", if_exists=True) op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True) op.drop_constraint("users_x_clubs_user_id_fkey", "users_x_clubs", type_="foreignkey") op.drop_constraint("users_x_clubs_club_id_fkey", "users_x_clubs", type_="foreignkey") @@ -36,6 +36,6 @@ def downgrade() -> None: op.create_foreign_key( "users_x_clubs_user_id_fkey", "users_x_clubs", "users", ["user_id"], ["id"] ) - op.drop_index(op.f("ix_users_email"), table_name="users") + op.drop_index(op.f("ix_users_email"), table_name="users", if_exists=True) op.create_index("ix_users_email", "users", ["email"], unique=False) # ### end Alembic commands ### From 7fce97b3aa9bccb98f37d34c4545c35f1d4dc9db Mon Sep 17 00:00:00 2001 From: oscartobar Date: Sat, 25 Oct 2025 10:55:56 -0500 Subject: [PATCH 02/14] Alembic is modified to use the correct database configuration depending on the development or production environment. --- backend/alembic.ini | 3 ++- backend/alembic/env.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/backend/alembic.ini b/backend/alembic.ini index 7ba4f8b9d..a2f4308d3 100644 --- a/backend/alembic.ini +++ b/backend/alembic.ini @@ -1,7 +1,8 @@ [alembic] # path to migration scripts script_location = alembic -sqlalchemy.url = postgresql://bracket:bracket@localhost/bracket +# URL placeholder - will be overridden by env.py using environm +sqlalchemy.url = driver://user:pass@localhost/dbname # Logging configuration [loggers] diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 02a219e60..f4f8277f8 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -18,7 +18,8 @@ def run_migrations_offline() -> None: - url = ALEMBIC_CONFIG.get_main_option("sqlalchemy.url") + # Use the environment configuration instead of alembic.ini + url = str(config.pg_dsn) context.configure(url=url, target_metadata=Base.metadata, compare_type=True) with context.begin_transaction(): From a37778859489505fff23fd06579320762b56ec07 Mon Sep 17 00:00:00 2001 From: oscartobar Date: Sat, 25 Oct 2025 11:39:22 -0500 Subject: [PATCH 03/14] This solution solves exactly the problem you were having: "relation 'users' does not exist" because now all tables are created first before attempting any migration. --- backend/bracket/app.py | 10 ++++++--- backend/bracket/utils/db_init.py | 37 +++++++++++++++++++++++++++----- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/backend/bracket/app.py b/backend/bracket/app.py index 3180a2451..0f0cc9618 100644 --- a/backend/bracket/app.py +++ b/backend/bracket/app.py @@ -40,9 +40,13 @@ @asynccontextmanager async def lifespan(_: FastAPI) -> AsyncIterator[None]: await database.connect() - await init_db_when_empty() - - if config.auto_run_migrations and environment is not Environment.CI: + + # Initialize database if empty (this will create tables and stamp Alembic) + admin_user_created = await init_db_when_empty() + + # Only run migrations if the database was not just initialized from scratch + if config.auto_run_migrations and environment is not Environment.CI and admin_user_created is None: + logger.info("Running Alembic migrations...") alembic_run_migrations() if environment is Environment.PRODUCTION: diff --git a/backend/bracket/utils/db_init.py b/backend/bracket/utils/db_init.py index 0c86fdc9f..85ed23e89 100644 --- a/backend/bracket/utils/db_init.py +++ b/backend/bracket/utils/db_init.py @@ -116,19 +116,46 @@ async def create_admin_user() -> UserId: return user.id +async def check_alembic_sync() -> bool: + """Check if the database is in sync with Alembic migrations.""" + try: + # Check if alembic_version table exists + alembic_version_exists = await database.fetch_val( + "SELECT EXISTS (SELECT FROM information_schema.tables " + "WHERE table_schema = 'public' AND table_name = 'alembic_version')" + ) + return bool(alembic_version_exists) + except Exception: + return False + + async def init_db_when_empty() -> UserId | None: table_count = await database.fetch_val( "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'" ) + + # Check if we have a completely empty database (no tables at all) + is_empty_db = table_count == 0 + # Check if Alembic tracking is in place + is_alembic_synced = await check_alembic_sync() + if config.admin_email and config.admin_password: if (table_count <= 1 and environment != Environment.CI) or ( environment is Environment.DEVELOPMENT and await get_user(config.admin_email) is None ): - logger.warning("Empty db detected, creating tables...") - metadata.create_all(engine) - alembic_stamp_head() - - logger.warning("Empty db detected, creating admin user...") + if is_empty_db or not is_alembic_synced: + logger.warning("Fresh database detected, creating all tables from schema...") + # Create all tables directly from the schema models + metadata.create_all(engine) + # Mark the database as up-to-date with Alembic to prevent migration conflicts + alembic_stamp_head() + logger.warning("Database initialized successfully with all tables and Alembic sync") + else: + logger.warning("Existing database detected, ensuring tables exist...") + metadata.create_all(engine) + alembic_stamp_head() + + logger.warning("Creating admin user...") return await create_admin_user() return None From 31d7ad7f70c4390c2b1cd17916badacdfa1f74dd Mon Sep 17 00:00:00 2001 From: oscartobar Date: Sat, 25 Oct 2025 11:54:03 -0500 Subject: [PATCH 04/14] solution is now production-safe for any database state you encounter. --- backend/bracket/app.py | 37 ++++++++++++++++++---- backend/bracket/utils/db_init.py | 53 +++++++++++++++++++++++--------- 2 files changed, 70 insertions(+), 20 deletions(-) diff --git a/backend/bracket/app.py b/backend/bracket/app.py index 0f0cc9618..66c0c6d02 100644 --- a/backend/bracket/app.py +++ b/backend/bracket/app.py @@ -41,13 +41,38 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: await database.connect() - # Initialize database if empty (this will create tables and stamp Alembic) - admin_user_created = await init_db_when_empty() + # Check database state before any operations + table_count = await database.fetch_val( + "SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public'" + ) + is_completely_empty = table_count == 0 - # Only run migrations if the database was not just initialized from scratch - if config.auto_run_migrations and environment is not Environment.CI and admin_user_created is None: - logger.info("Running Alembic migrations...") - alembic_run_migrations() + # Check if alembic_version table exists (indicates migrations were run before) + alembic_version_exists = await database.fetch_val( + "SELECT EXISTS (SELECT FROM information_schema.tables " + "WHERE table_schema = 'public' AND table_name = 'alembic_version')" + ) + + logger.info(f"Database state: table_count={table_count}, alembic_version_exists={alembic_version_exists}") + + # Initialize database if needed + await init_db_when_empty() + + # Decision logic for migrations: + if config.auto_run_migrations and environment is not Environment.CI: + if is_completely_empty: + # Fresh database: tables created from schema, no migrations needed + logger.info("Fresh database detected, skipping Alembic migrations (tables already created from schema)") + elif alembic_version_exists: + # Existing database with Alembic history: run migrations to update + logger.info("Existing database with migration history detected, running Alembic migrations...") + alembic_run_migrations() + else: + # Existing database without Alembic history: dangerous, log warning + logger.warning( + "Existing database without Alembic history detected. " + "Manual intervention may be required. Skipping migrations to prevent conflicts." + ) if environment is Environment.PRODUCTION: start_cronjobs() diff --git a/backend/bracket/utils/db_init.py b/backend/bracket/utils/db_init.py index 85ed23e89..af1785fd0 100644 --- a/backend/bracket/utils/db_init.py +++ b/backend/bracket/utils/db_init.py @@ -135,28 +135,53 @@ async def init_db_when_empty() -> UserId | None: ) # Check if we have a completely empty database (no tables at all) - is_empty_db = table_count == 0 + is_completely_empty = table_count == 0 # Check if Alembic tracking is in place is_alembic_synced = await check_alembic_sync() if config.admin_email and config.admin_password: - if (table_count <= 1 and environment != Environment.CI) or ( - environment is Environment.DEVELOPMENT and await get_user(config.admin_email) is None - ): - if is_empty_db or not is_alembic_synced: - logger.warning("Fresh database detected, creating all tables from schema...") + # Only initialize if database is truly empty OR in development + should_initialize = ( + is_completely_empty or + (environment is Environment.DEVELOPMENT and not is_alembic_synced) + ) + + if should_initialize: + if is_completely_empty: + logger.warning("Completely empty database detected, initializing from schema...") # Create all tables directly from the schema models metadata.create_all(engine) - # Mark the database as up-to-date with Alembic to prevent migration conflicts + # Mark the database as up-to-date with Alembic alembic_stamp_head() - logger.warning("Database initialized successfully with all tables and Alembic sync") - else: - logger.warning("Existing database detected, ensuring tables exist...") + logger.warning("Database initialized successfully from schema with Alembic sync") + elif environment is Environment.DEVELOPMENT: + logger.warning("Development environment: ensuring tables exist...") metadata.create_all(engine) - alembic_stamp_head() - - logger.warning("Creating admin user...") - return await create_admin_user() + if not is_alembic_synced: + alembic_stamp_head() + + # Try to create admin user if needed + try: + existing_user = await get_user(config.admin_email) + if existing_user is None: + logger.warning("Creating admin user...") + return await create_admin_user() + else: + logger.info("Admin user already exists") + except Exception as e: + logger.warning(f"Could not check/create admin user: {e}") + + elif table_count > 0: + logger.info(f"Existing database detected with {table_count} tables, skipping initialization") + # In production with existing database, only try to ensure admin user exists + if environment is Environment.PRODUCTION: + try: + existing_user = await get_user(config.admin_email) + if existing_user is None: + logger.warning("Admin user missing in existing database, creating...") + return await create_admin_user() + except Exception as e: + logger.warning(f"Could not check admin user in existing database: {e}") return None From 1773993a38978043fbc4c399fac2a66e2ea4a266 Mon Sep 17 00:00:00 2001 From: oscartobar Date: Fri, 31 Oct 2025 12:51:19 -0500 Subject: [PATCH 05/14] The modification is added to prevent the API from being displayed externally. --- .env.private | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 .env.private diff --git a/.env.private b/.env.private new file mode 100644 index 000000000..3295a8a10 --- /dev/null +++ b/.env.private @@ -0,0 +1,26 @@ +# ============================================================================= +# PRIVATE API CONFIGURATION (Recommended for production) +# ============================================================================= +# In this mode, the backend is NOT accessible from the Internet. +# The frontend uses API Routes as an internal proxy. +# Users NEVER see /api in URLs, everything is transparent. + +# API Mode +USE_PRIVATE_API=true + +# Backend (only accessible internally) +BACKEND_PORT_MAPPING= +CORS_ORIGINS=http://bracket-frontend:3000 +INTERNAL_API_URL=http://bracket-backend:8400 + +# Frontend - User only sees yourdomain.com +FRONTEND_PORT_MAPPING=172.16.0.4:3000:3000 +# This variable is NOT used in private mode, just for reference +PUBLIC_API_URL=https://yourdomain.com + +# General configuration +JWT_SECRET=change_me_in_production +ADMIN_EMAIL=admin@yourdomain.com +ADMIN_PASSWORD=change_me_in_production +BASE_URL=https://yourdomain.com +HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 \ No newline at end of file From 222f53bb7ad54fa6a720b162ead3f45557e8747e Mon Sep 17 00:00:00 2001 From: oscartobar Date: Fri, 31 Oct 2025 12:51:54 -0500 Subject: [PATCH 06/14] The modification is added to prevent the API from being displayed externally. --- .env.public | 24 +++++ API-MODES.md | 141 ++++++++++++++++++++++++++++ docker-compose.yml | 34 +++++-- frontend/src/pages/api/[...path].ts | 73 ++++++++++++++ frontend/src/services/adapter.tsx | 18 +++- switch-api-mode.sh | 51 ++++++++++ 6 files changed, 328 insertions(+), 13 deletions(-) create mode 100644 .env.public create mode 100644 API-MODES.md create mode 100644 frontend/src/pages/api/[...path].ts create mode 100755 switch-api-mode.sh diff --git a/.env.public b/.env.public new file mode 100644 index 000000000..d753d887b --- /dev/null +++ b/.env.public @@ -0,0 +1,24 @@ +# ============================================================================= +# PUBLIC API CONFIGURATION +# ============================================================================= +# In this mode, both frontend and backend are accessible from the Internet. +# The browser connects directly to the backend. + +# API Mode +USE_PRIVATE_API=false + +# Backend (publicly accessible) +BACKEND_PORT_MAPPING=172.16.0.4:8400:8400 +CORS_ORIGINS=https://yourdomain.com +INTERNAL_API_URL=http://bracket-backend:8400 + +# Frontend +FRONTEND_PORT_MAPPING=172.16.0.4:3000:3000 +PUBLIC_API_URL=https://api.yourdomain.com + +# General configuration +JWT_SECRET=change_me_in_production +ADMIN_EMAIL=admin@yourdomain.com +ADMIN_PASSWORD=change_me_in_production +BASE_URL=https://api.yourdomain.com +HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 \ No newline at end of file diff --git a/API-MODES.md b/API-MODES.md new file mode 100644 index 000000000..bcae4bcab --- /dev/null +++ b/API-MODES.md @@ -0,0 +1,141 @@ +# API Configuration: Public vs Private + +This project supports two API operation modes: + +## 🔒 Private Mode (Recommended) + +**Features:** +- ✅ Backend NOT accessible from Internet +- ✅ Enhanced security +- ✅ Frontend acts as transparent proxy +- ✅ No CORS issues +- ✅ Centralized logging +- ✅ **Users NEVER see /api in URLs** + +**What users see:** +- ✅ `https://yourdomain.com/login` +- ✅ `https://yourdomain.com/tournaments` +- ✅ Normal frontend URLs + +**What happens internally (invisible):** +- 🔄 `yourdomain.com/api/token` → `bracket-backend:8400/token` +- 🔄 Transparent server-side proxy + +**Configuration:** +```bash +./switch-api-mode.sh private +docker-compose down && docker-compose up -d +``` + +**Nginx (frontend only):** +```nginx +server { + server_name yourdomain.com; + location / { + proxy_pass http://172.16.0.4:3000; + } +} +``` + +## 🌐 Public Mode + +**Features:** +- ⚠️ Backend accessible from Internet +- ⚠️ Requires correct CORS configuration +- ⚠️ Two subdomains needed +- ✅ Potentially lower latency + +**What users see:** +- ✅ `https://yourdomain.com/login` +- ⚠️ Requests go to `https://api.yourdomain.com/token` + +**Configuration:** +```bash +./switch-api-mode.sh public +docker-compose down && docker-compose up -d +``` + +**Nginx (frontend + backend):** +```nginx +# Frontend +server { + server_name yourdomain.com; + location / { + proxy_pass http://172.16.0.4:3000; + } +} + +# Backend +server { + server_name api.yourdomain.com; + location / { + proxy_pass http://172.16.0.4:8400; + } +} +``` + +## Environment Variables + +| Variable | Description | Private | Public | +|----------|-------------|---------|---------| +| `USE_PRIVATE_API` | Operation mode | `true` | `false` | +| `BACKEND_PORT_MAPPING` | Backend port | empty | `172.16.0.4:8400:8400` | +| `CORS_ORIGINS` | Allowed domains | internal | public | +| `PUBLIC_API_URL` | Public API URL | `/api` | `https://api.yourdomain.com` | + +## Quick Switch + +```bash +# Secure mode (recommended) +./switch-api-mode.sh private + +# Public mode +./switch-api-mode.sh public +``` + +## Troubleshooting + +### Error: `bracket-backend:8400 not resolved` +- Verify that `USE_PRIVATE_API=true` in `.env` +- Make sure `/pages/api/[...path].ts` file exists + +### Error: `CORS policy` +- Public mode: verify `CORS_ORIGINS` in backend +- Private mode: should not occur + +### Backend not responding +- Private mode: check frontend logs +- Public mode: verify nginx points to `172.16.0.4:8400` + +## Variables de Entorno + +| Variable | Descripción | Privado | Público | +|----------|-------------|---------|---------| +| `USE_PRIVATE_API` | Modo de operación | `true` | `false` | +| `BACKEND_PORT_MAPPING` | Puerto del backend | vacío | `172.16.0.4:8400:8400` | +| `CORS_ORIGINS` | Dominios permitidos | interno | público | +| `PUBLIC_API_URL` | URL pública del API | `/api` | `https://api.pinar.campeonatos.co` | + +## Cambio Rápido + +```bash +# Modo seguro (recomendado) +./switch-api-mode.sh private + +# Modo público +./switch-api-mode.sh public +``` + +## Troubleshooting + +### Error: `bracket-backend:8400 not resolved` +- Verifica que `USE_PRIVATE_API=true` en `.env` +- Asegúrate de que el archivo `/pages/api/[...path].ts` existe + +### Error: `CORS policy` +- En modo público: verifica `CORS_ORIGINS` en backend +- En modo privado: no debería ocurrir + +### Backend no responde +- Modo privado: verifica logs del frontend +- Modo público: verifica que nginx apunte a `172.16.0.4:8400` \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index ac7468baf..dd2971c85 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,14 +11,20 @@ services: depends_on: - postgres environment: - ENVIRONMENT: DEVELOPMENT - CORS_ORIGINS: http://localhost:3000 - PG_DSN: postgresql://bracket_dev:bracket_dev@postgres:5432/bracket_dev + ENVIRONMENT: PRODUCTION + # Dynamic CORS based on mode + CORS_ORIGINS: ${CORS_ORIGINS:-http://bracket-frontend:3000} + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod + JWT_SECRET: ${JWT_SECRET:-change_me_in_production} + ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@yourdomain.com} + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change_me_in_production} + BASE_URL: ${BASE_URL:-https://yourdomain.com} image: ghcr.io/evroon/bracket-backend networks: - bracket_lan ports: - - 8400:8400 + # Expose port only if using public API + - "${BACKEND_PORT_MAPPING:-}" restart: unless-stopped volumes: - ./backend/static:/app/static @@ -26,18 +32,26 @@ services: bracket-frontend: container_name: bracket-frontend environment: - NEXT_PUBLIC_API_BASE_URL: http://localhost:8400 - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: 10000000-ffff-ffff-ffff-000000000001 + # Main variable to toggle between public/private + NEXT_PUBLIC_USE_PRIVATE_API: ${USE_PRIVATE_API:-true} + # URL for public API (only used if USE_PRIVATE_API=false) + NEXT_PUBLIC_API_BASE_URL: ${PUBLIC_API_URL:-https://api.yourdomain.com} + # Internal URL for proxy (only used if USE_PRIVATE_API=true) + # This variable is NOT visible to browser, only used server-side + INTERNAL_API_BASE_URL: ${INTERNAL_API_URL:-http://bracket-backend:8400} + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: ${HCAPTCHA_SITE_KEY:-10000000-ffff-ffff-ffff-000000000001} image: ghcr.io/evroon/bracket-frontend + networks: + - bracket_lan ports: - - 3000:3000 + - "${FRONTEND_PORT_MAPPING:-172.16.0.4:3000:3000}" restart: unless-stopped postgres: environment: - POSTGRES_DB: bracket_dev - POSTGRES_PASSWORD: bracket_dev - POSTGRES_USER: bracket_dev + POSTGRES_DB: bracket_prod + POSTGRES_PASSWORD: bracket_prod + POSTGRES_USER: bracket_prod image: postgres networks: - bracket_lan diff --git a/frontend/src/pages/api/[...path].ts b/frontend/src/pages/api/[...path].ts new file mode 100644 index 000000000..a472ad570 --- /dev/null +++ b/frontend/src/pages/api/[...path].ts @@ -0,0 +1,73 @@ +import { NextApiRequest, NextApiResponse } from 'next'; +import axios from 'axios'; + +const INTERNAL_API_URL = process.env.INTERNAL_API_BASE_URL || 'http://bracket-backend:8400'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const { path, ...query } = req.query; + const apiPath = Array.isArray(path) ? path.join('/') : path || ''; + + // Build internal backend URL + const url = `${INTERNAL_API_URL}/${apiPath}`; + + // Build query string only for valid parameters + const validQuery: Record = {}; + Object.entries(query).forEach(([key, value]) => { + if (typeof value === 'string') { + validQuery[key] = value; + } + }); + + const queryString = new URLSearchParams(validQuery).toString(); + const fullUrl = queryString ? `${url}?${queryString}` : url; + + // Configure headers, copying relevant ones from original request + const headers: Record = {}; + + // Copy Content-Type if exists + if (req.headers['content-type']) { + headers['Content-Type'] = req.headers['content-type']; + } + + // Pass Authorization header if exists + if (req.headers.authorization) { + headers.Authorization = req.headers.authorization; + } + + // Pass other relevant headers + if (req.headers.accept) { + headers.Accept = req.headers.accept; + } + + console.log(`[API Proxy] ${req.method} ${fullUrl}`); + + // Make request to internal backend + const response = await axios({ + method: req.method as any, + url: fullUrl, + data: req.method !== 'GET' && req.method !== 'HEAD' ? req.body : undefined, + headers, + timeout: 30000, + validateStatus: () => true, // Don't throw error on 4xx/5xx codes + }); + + // Copy important response headers + if (response.headers['content-type']) { + res.setHeader('Content-Type', response.headers['content-type']); + } + + // Return response + res.status(response.status).json(response.data); + } catch (error: any) { + console.error('[API Proxy] Error:', error.message); + + if (error.code === 'ECONNREFUSED') { + res.status(503).json({ error: 'Backend service unavailable' }); + } else if (error.code === 'ETIMEDOUT') { + res.status(504).json({ error: 'Backend timeout' }); + } else { + res.status(500).json({ error: 'Internal server error' }); + } + } +} \ No newline at end of file diff --git a/frontend/src/services/adapter.tsx b/frontend/src/services/adapter.tsx index 254738380..964250fb5 100644 --- a/frontend/src/services/adapter.tsx +++ b/frontend/src/services/adapter.tsx @@ -53,9 +53,21 @@ export function requestSucceeded(result: AxiosResponse | AxiosError) { } export function getBaseApiUrl() { - return process.env.NEXT_PUBLIC_API_BASE_URL != null - ? process.env.NEXT_PUBLIC_API_BASE_URL - : 'http://localhost:8400'; + // Check if internal proxy should be used (private API) + const usePrivateApi = process.env.NEXT_PUBLIC_USE_PRIVATE_API === 'true'; + + if (usePrivateApi) { + // Private mode: use frontend API routes as proxy + // Users never see /api in URLs, only used internally + return typeof window !== 'undefined' && window.location.origin + ? `${window.location.origin}/api` + : '/api'; + } else { + // Public mode: connect directly to backend + return process.env.NEXT_PUBLIC_API_BASE_URL != null + ? process.env.NEXT_PUBLIC_API_BASE_URL + : 'http://localhost:8400'; + } } export function createAxios() { diff --git a/switch-api-mode.sh b/switch-api-mode.sh new file mode 100755 index 000000000..a9c8425ad --- /dev/null +++ b/switch-api-mode.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Script to switch between public and private API modes +# Usage: ./switch-api-mode.sh [private|public] + +set -e + +MODE=${1:-private} + +case $MODE in + "private") + echo "🔒 Configuring PRIVATE API (secure mode)..." + cp .env.private .env + echo "✅ Backend will NOT be accessible from Internet" + echo "✅ Frontend will use TRANSPARENT internal proxy" + echo "✅ Users will NEVER see /api in URLs" + echo "✅ Only one domain needed: yourdomain.com" + echo "✅ Enhanced security" + ;; + "public") + echo "🌐 Configuring PUBLIC API..." + cp .env.public .env + echo "⚠️ Backend WILL be accessible from Internet" + echo "⚠️ Users will see requests to api.yourdomain.com" + echo "⚠️ Browser connects directly to backend" + echo "⚠️ Requires correct CORS configuration" + echo "⚠️ Two domains required" + ;; + *) + echo "❌ Invalid mode. Use: private or public" + echo "Example: ./switch-api-mode.sh private" + exit 1 + ;; +esac + +echo "" +echo "📝 .env file configured for mode: $MODE" +echo "🐳 Run: docker-compose down && docker-compose up -d" +echo "" + +if [ "$MODE" = "private" ]; then + echo "✅ NGINX CONFIGURATION FOR PRIVATE MODE:" + echo " - Only configure: yourdomain.com → 172.16.0.4:3000" + echo " - DO NOT configure api.yourdomain.com" + echo " - Users only see normal frontend URLs" +elif [ "$MODE" = "public" ]; then + echo "⚠️ IMPORTANT for public mode:" + echo " - Configure nginx for yourdomain.com → 172.16.0.4:3000" + echo " - Configure nginx for api.yourdomain.com → 172.16.0.4:8400" + echo " - Make sure CORS_ORIGINS is correct" +fi \ No newline at end of file From 8916e781037f618ad3c90f3691734e42744b336e Mon Sep 17 00:00:00 2001 From: Oscar Tobar Rios Date: Fri, 31 Oct 2025 14:44:16 -0500 Subject: [PATCH 07/14] Refactor environment variables in docker-compose.yml Updated environment variables in docker-compose.yml to use fixed values instead of defaults. Main variable to toggle between public/private --- docker-compose.yml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index dd2971c85..31723bae9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,18 +13,19 @@ services: environment: ENVIRONMENT: PRODUCTION # Dynamic CORS based on mode - CORS_ORIGINS: ${CORS_ORIGINS:-http://bracket-frontend:3000} + CORS_ORIGINS: http://bracket-frontend:3000 PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod - JWT_SECRET: ${JWT_SECRET:-change_me_in_production} - ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@yourdomain.com} - ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change_me_in_production} - BASE_URL: ${BASE_URL:-https://yourdomain.com} + JWT_SECRET: change_me_in_production + ADMIN_EMAIL: admin@yourdomain.com + ADMIN_PASSWORD: change_me_in_production + BASE_URL: https://yourdomain.com image: ghcr.io/evroon/bracket-backend networks: - bracket_lan ports: # Expose port only if using public API - - "${BACKEND_PORT_MAPPING:-}" + # 8400:8400 + - restart: unless-stopped volumes: - ./backend/static:/app/static @@ -33,18 +34,18 @@ services: container_name: bracket-frontend environment: # Main variable to toggle between public/private - NEXT_PUBLIC_USE_PRIVATE_API: ${USE_PRIVATE_API:-true} + NEXT_PUBLIC_USE_PRIVATE_API: true # URL for public API (only used if USE_PRIVATE_API=false) - NEXT_PUBLIC_API_BASE_URL: ${PUBLIC_API_URL:-https://api.yourdomain.com} + NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.com # Internal URL for proxy (only used if USE_PRIVATE_API=true) # This variable is NOT visible to browser, only used server-side - INTERNAL_API_BASE_URL: ${INTERNAL_API_URL:-http://bracket-backend:8400} - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: ${HCAPTCHA_SITE_KEY:-10000000-ffff-ffff-ffff-000000000001} + INTERNAL_API_BASE_URL: http://bracket-backend:8400 + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: 10000000-ffff-ffff-ffff-000000000001 image: ghcr.io/evroon/bracket-frontend networks: - bracket_lan ports: - - "${FRONTEND_PORT_MAPPING:-172.16.0.4:3000:3000}" + - 3000:3000 restart: unless-stopped postgres: From a0de5e42292ee247e8efb5ad432e7df841092074 Mon Sep 17 00:00:00 2001 From: oscartobar Date: Sat, 1 Nov 2025 10:19:15 -0500 Subject: [PATCH 08/14] The frontend Docker container is modified to have the modification in private mode. --- docker-compose.yml | 31 +++++++++++++++---------------- frontend/Dockerfile | 3 ++- frontend/next-env.d.ts | 1 + 3 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 31723bae9..34199e537 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,20 +12,19 @@ services: - postgres environment: ENVIRONMENT: PRODUCTION - # Dynamic CORS based on mode + # CORS - Internal communication between frontend and backend CORS_ORIGINS: http://bracket-frontend:3000 PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod JWT_SECRET: change_me_in_production ADMIN_EMAIL: admin@yourdomain.com ADMIN_PASSWORD: change_me_in_production - BASE_URL: https://yourdomain.com + BASE_URL: http://192.168.100.10:3000 image: ghcr.io/evroon/bracket-backend networks: - bracket_lan - ports: - # Expose port only if using public API - # 8400:8400 - - + # Backend is PRIVATE - Uncomment ports below for PUBLIC API mode + # ports: + # - "8400:8400" restart: unless-stopped volumes: - ./backend/static:/app/static @@ -33,19 +32,19 @@ services: bracket-frontend: container_name: bracket-frontend environment: - # Main variable to toggle between public/private - NEXT_PUBLIC_USE_PRIVATE_API: true - # URL for public API (only used if USE_PRIVATE_API=false) - NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.com - # Internal URL for proxy (only used if USE_PRIVATE_API=true) - # This variable is NOT visible to browser, only used server-side + # Private API mode - Uses internal proxy to communicate with backend + NEXT_PUBLIC_USE_PRIVATE_API: "true" + # Backend URL (for proxy reference) + NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 + # Internal URL for proxy (server-side only) INTERNAL_API_BASE_URL: http://bracket-backend:8400 - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: 10000000-ffff-ffff-ffff-000000000001 - image: ghcr.io/evroon/bracket-frontend + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + # Use local image with internal proxy + image: bracket-frontend-local networks: - bracket_lan ports: - - 3000:3000 + - "3000:3000" restart: unless-stopped postgres: @@ -53,7 +52,7 @@ services: POSTGRES_DB: bracket_prod POSTGRES_PASSWORD: bracket_prod POSTGRES_USER: bracket_prod - image: postgres + image: postgres:15 networks: - bracket_lan restart: always diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 3dda8be45..caa81fbfc 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -17,7 +17,8 @@ WORKDIR /app COPY . . COPY --from=deps /app/node_modules ./node_modules -RUN NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER \ +RUN NEXT_PUBLIC_USE_PRIVATE_API=true \ + NEXT_PUBLIC_API_BASE_URL=http://NEXT_PUBLIC_API_BASE_URL_PLACEHOLDER \ NEXT_PUBLIC_HCAPTCHA_SITE_KEY=NEXT_PUBLIC_HCAPTCHA_SITE_KEY_PLACEHOLDER \ yarn build diff --git a/frontend/next-env.d.ts b/frontend/next-env.d.ts index 52e831b43..254b73c16 100644 --- a/frontend/next-env.d.ts +++ b/frontend/next-env.d.ts @@ -1,5 +1,6 @@ /// /// +/// // NOTE: This file should not be edited // see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. From f68473e937defd761842c60f5db01c8b1b163b5e Mon Sep 17 00:00:00 2001 From: oscartobar Date: Sat, 1 Nov 2025 10:49:29 -0500 Subject: [PATCH 09/14] Update documentation APi Modes --- API-MODES.md | 569 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 488 insertions(+), 81 deletions(-) diff --git a/API-MODES.md b/API-MODES.md index bcae4bcab..b99265039 100644 --- a/API-MODES.md +++ b/API-MODES.md @@ -1,141 +1,548 @@ -# API Configuration: Public vs Private +# API Configuration: Public vs Private Modes# API Configuration: Public vs Private -This project supports two API operation modes: -## 🔒 Private Mode (Recommended) -**Features:** +This project supports two API operation modes for different deployment scenarios.This project supports two API operation modes: + + + +---## 🔒 Private Mode (Recommended) + + + +## 🔒 Private Mode (Recommended for Production)**Features:** + - ✅ Backend NOT accessible from Internet -- ✅ Enhanced security -- ✅ Frontend acts as transparent proxy + +### Overview- ✅ Enhanced security + +In **Private Mode**, the backend API is **NOT exposed to the internet**. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues.- ✅ Frontend acts as transparent proxy + - ✅ No CORS issues -- ✅ Centralized logging -- ✅ **Users NEVER see /api in URLs** -**What users see:** -- ✅ `https://yourdomain.com/login` -- ✅ `https://yourdomain.com/tournaments` -- ✅ Normal frontend URLs +### Features- ✅ Centralized logging -**What happens internally (invisible):** -- 🔄 `yourdomain.com/api/token` → `bracket-backend:8400/token` -- 🔄 Transparent server-side proxy +- ✅ **Backend NOT accessible from Internet** - Only internal Docker network access- ✅ **Users NEVER see /api in URLs** + +- ✅ **Enhanced security** - Attack surface reduced + +- ✅ **Frontend acts as transparent proxy** - Server-side forwarding via `/api/*` routes**What users see:** + +- ✅ **No CORS issues** - Same-origin requests- ✅ `https://yourdomain.com/login` + +- ✅ **Single domain** - Only frontend needs to be publicly exposed- ✅ `https://yourdomain.com/tournaments` + +- ✅ **Users NEVER see internal URLs** - All requests appear as frontend URLs- ✅ Normal frontend URLs + + + +### What Users See**What happens internally (invisible):** + +```- 🔄 `yourdomain.com/api/token` → `bracket-backend:8400/token` + +✅ https://yourdomain.com/login- 🔄 Transparent server-side proxy + +✅ https://yourdomain.com/tournaments + +✅ https://yourdomain.com/ (all routes are frontend URLs)**Configuration:** + +``````bash -**Configuration:** -```bash ./switch-api-mode.sh private -docker-compose down && docker-compose up -d -``` -**Nginx (frontend only):** -```nginx -server { +### What Happens Internally (Transparent)docker-compose down && docker-compose up -d + +`````` + +Browser Request: GET https://yourdomain.com/ + + ↓**Nginx (frontend only):** + +Next.js Frontend: Calls /api/clubs internally```nginx + + ↓server { + +Proxy Handler: /api/clubs → http://bracket-backend:8400/clubs server_name yourdomain.com; + + ↓ location / { + +Backend Response: Returns data to frontend proxy_pass http://172.16.0.4:3000; + + ↓ } + +User Receives: Data rendered on page} + +`````` + + + +### Docker Compose Configuration## 🌐 Public Mode + + + +**Method 1: Direct Configuration (Simplest)****Features:** + +- ⚠️ Backend accessible from Internet + +Edit `docker-compose.yml`:- ⚠️ Requires correct CORS configuration + +- ⚠️ Two subdomains needed + +```yaml- ✅ Potentially lower latency + +services: + + bracket-backend:**What users see:** + + container_name: bracket-backend- ✅ `https://yourdomain.com/login` + + environment:- ⚠️ Requests go to `https://api.yourdomain.com/token` + + ENVIRONMENT: PRODUCTION + + # CORS - Only allow internal frontend communication**Configuration:** + + CORS_ORIGINS: http://bracket-frontend:3000```bash + + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod./switch-api-mode.sh public + + JWT_SECRET: change_me_in_productiondocker-compose down && docker-compose up -d + + ADMIN_EMAIL: admin@yourdomain.com``` + + ADMIN_PASSWORD: change_me_in_production + + BASE_URL: https://yourdomain.com**Nginx (frontend + backend):** + + image: ghcr.io/evroon/bracket-backend```nginx + + networks:# Frontend + + - bracket_lanserver { + + # Backend is PRIVATE - NO ports exposed server_name yourdomain.com; + + restart: unless-stopped location / { + + proxy_pass http://172.16.0.4:3000; + + bracket-frontend: } + + container_name: bracket-frontend} + + environment: + + # Enable Private API mode# Backend + + NEXT_PUBLIC_USE_PRIVATE_API: "true"server { + + # Internal backend URL (for proxy) server_name api.yourdomain.com; + + NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 location / { + + INTERNAL_API_BASE_URL: http://bracket-backend:8400 proxy_pass http://172.16.0.4:8400; + + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" } + + image: bracket-frontend-local # Must use local image with proxy} + + networks:``` + + - bracket_lan + + ports:## Environment Variables + + - "3000:3000" # Only frontend is exposed + + restart: unless-stopped| Variable | Description | Private | Public | + +```|----------|-------------|---------|---------| + +| `USE_PRIVATE_API` | Operation mode | `true` | `false` | + +**Method 2: Using Environment Files**| `BACKEND_PORT_MAPPING` | Backend port | empty | `172.16.0.4:8400:8400` | + +| `CORS_ORIGINS` | Allowed domains | internal | public | + +Create `.env.private`:| `PUBLIC_API_URL` | Public API URL | `/api` | `https://api.yourdomain.com` | + + + +```bash## Quick Switch + +# Private API Mode - Backend NOT publicly accessible + +NEXT_PUBLIC_USE_PRIVATE_API=true```bash + +CORS_ORIGINS=http://bracket-frontend:3000# Secure mode (recommended) + +BACKEND_PORT_MAPPING=./switch-api-mode.sh private + +INTERNAL_API_URL=http://bracket-backend:8400 + +NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400# Public mode + +FRONTEND_PORT_MAPPING=3000:3000./switch-api-mode.sh public + +JWT_SECRET=change_me_in_production``` + +ADMIN_EMAIL=admin@yourdomain.com + +ADMIN_PASSWORD=change_me_in_production## Troubleshooting + +BASE_URL=https://yourdomain.com + +NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001### Error: `bracket-backend:8400 not resolved` + +```- Verify that `USE_PRIVATE_API=true` in `.env` + +- Make sure `/pages/api/[...path].ts` file exists + +Then use variables in `docker-compose.yml`: + +### Error: `CORS policy` + +```yaml- Public mode: verify `CORS_ORIGINS` in backend + +services:- Private mode: should not occur + + bracket-backend: + + environment:### Backend not responding + + CORS_ORIGINS: ${CORS_ORIGINS}- Private mode: check frontend logs + + JWT_SECRET: ${JWT_SECRET}- Public mode: verify nginx points to `172.16.0.4:8400` + + ADMIN_EMAIL: ${ADMIN_EMAIL} + + ADMIN_PASSWORD: ${ADMIN_PASSWORD}## Variables de Entorno + + BASE_URL: ${BASE_URL} + + ports:| Variable | Descripción | Privado | Público | + + - "${BACKEND_PORT_MAPPING:-}" # Empty = not exposed|----------|-------------|---------|---------| + +| `USE_PRIVATE_API` | Modo de operación | `true` | `false` | + + bracket-frontend:| `BACKEND_PORT_MAPPING` | Puerto del backend | vacío | `172.16.0.4:8400:8400` | + + environment:| `CORS_ORIGINS` | Dominios permitidos | interno | público | + + NEXT_PUBLIC_USE_PRIVATE_API: ${NEXT_PUBLIC_USE_PRIVATE_API}| `PUBLIC_API_URL` | URL pública del API | `/api` | `https://api.pinar.campeonatos.co` | + + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + + INTERNAL_API_BASE_URL: ${INTERNAL_API_URL}## Cambio Rápido + + ports: + + - "${FRONTEND_PORT_MAPPING}"```bash + +```# Modo seguro (recomendado) + +./switch-api-mode.sh private + +### Deployment + +# Modo público + +```bash./switch-api-mode.sh public + +# Using .env.private``` + +cp .env.private .env + +docker-compose down## Troubleshooting + +docker-compose up -d + +### Error: `bracket-backend:8400 not resolved` + +# Or with direct configuration- Verifica que `USE_PRIVATE_API=true` en `.env` + +docker-compose down- Asegúrate de que el archivo `/pages/api/[...path].ts` existe + +docker-compose up -d + +```### Error: `CORS policy` + +- En modo público: verifica `CORS_ORIGINS` en backend + +### Nginx Configuration (Frontend Only)- En modo privado: no debería ocurrir + + + +```nginx### Backend no responde + +server {- Modo privado: verifica logs del frontend + + listen 443 ssl http2;- Modo público: verifica que nginx apunte a `172.16.0.4:8400` server_name yourdomain.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + location / { - proxy_pass http://172.16.0.4:3000; + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } } ``` -## 🌐 Public Mode +--- -**Features:** -- ⚠️ Backend accessible from Internet -- ⚠️ Requires correct CORS configuration -- ⚠️ Two subdomains needed -- ✅ Potentially lower latency +## 🌐 Public Mode (Direct API Access) + +### Overview +In **Public Mode**, both frontend and backend are exposed to the internet. The frontend makes direct requests to the backend API. This requires proper CORS configuration and typically uses two subdomains. + +### Features +- ⚠️ **Backend accessible from Internet** - Requires security considerations +- ⚠️ **Requires CORS configuration** - Cross-origin requests must be allowed +- ⚠️ **Two domains/subdomains needed** - Frontend and API separately exposed +- ✅ **Potentially lower latency** - Direct backend communication +- ✅ **API can be used by other clients** - Third-party integrations possible -**What users see:** -- ✅ `https://yourdomain.com/login` -- ⚠️ Requests go to `https://api.yourdomain.com/token` +### What Users See +``` +✅ https://yourdomain.com/login +⚠️ Browser requests go to: https://api.yourdomain.com/token +⚠️ Browser requests go to: https://api.yourdomain.com/clubs +``` + +### Docker Compose Configuration + +**Method 1: Direct Configuration** + +Edit `docker-compose.yml`: + +```yaml +services: + bracket-backend: + container_name: bracket-backend + environment: + ENVIRONMENT: PRODUCTION + # CORS - Allow requests from frontend domain + CORS_ORIGINS: https://yourdomain.com,http://localhost:3000 + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod + JWT_SECRET: change_me_in_production + ADMIN_EMAIL: admin@yourdomain.com + ADMIN_PASSWORD: change_me_in_production + BASE_URL: https://api.yourdomain.com + image: ghcr.io/evroon/bracket-backend + networks: + - bracket_lan + # Backend is PUBLIC - Port exposed + ports: + - "8400:8400" + restart: unless-stopped + + bracket-frontend: + container_name: bracket-frontend + environment: + # Disable Private API mode + NEXT_PUBLIC_USE_PRIVATE_API: "false" + # Public backend URL + NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.com + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + image: ghcr.io/evroon/bracket-frontend # Can use official image + networks: + - bracket_lan + ports: + - "3000:3000" + restart: unless-stopped +``` + +**Method 2: Using Environment Files** + +Create `.env.public`: -**Configuration:** ```bash -./switch-api-mode.sh public -docker-compose down && docker-compose up -d +# Public API Mode - Backend IS publicly accessible +NEXT_PUBLIC_USE_PRIVATE_API=false +CORS_ORIGINS=https://yourdomain.com,http://localhost:3000 +BACKEND_PORT_MAPPING=8400:8400 +NEXT_PUBLIC_API_BASE_URL=https://api.yourdomain.com +INTERNAL_API_URL=http://bracket-backend:8400 +FRONTEND_PORT_MAPPING=3000:3000 +JWT_SECRET=change_me_in_production +ADMIN_EMAIL=admin@yourdomain.com +ADMIN_PASSWORD=change_me_in_production +BASE_URL=https://api.yourdomain.com +NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 +``` + +### Deployment + +```bash +# Using .env.public +cp .env.public .env +docker-compose down +docker-compose up -d ``` -**Nginx (frontend + backend):** +### Nginx Configuration (Frontend + Backend) + ```nginx # Frontend server { + listen 443 ssl http2; server_name yourdomain.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + location / { - proxy_pass http://172.16.0.4:3000; + proxy_pass http://localhost:3000; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; + proxy_cache_bypass $http_upgrade; } } -# Backend +# Backend API server { + listen 443 ssl http2; server_name api.yourdomain.com; + + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; + location / { - proxy_pass http://172.16.0.4:8400; + proxy_pass http://localhost:8400; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } } ``` -## Environment Variables +--- -| Variable | Description | Private | Public | -|----------|-------------|---------|---------| -| `USE_PRIVATE_API` | Operation mode | `true` | `false` | -| `BACKEND_PORT_MAPPING` | Backend port | empty | `172.16.0.4:8400:8400` | -| `CORS_ORIGINS` | Allowed domains | internal | public | -| `PUBLIC_API_URL` | Public API URL | `/api` | `https://api.yourdomain.com` | +## Environment Variables Reference + +### Frontend Environment Variables + +| Variable | Description | Private Mode | Public Mode | Required | +|----------|-------------|--------------|-------------|----------| +| `NEXT_PUBLIC_USE_PRIVATE_API` | Enables/disables private API mode | `"true"` | `"false"` | ✅ Yes | +| `NEXT_PUBLIC_API_BASE_URL` | Backend API URL visible to browser | `http://bracket-backend:8400` | `https://api.yourdomain.com` | ✅ Yes | +| `INTERNAL_API_BASE_URL` | Backend URL for server-side proxy | `http://bracket-backend:8400` | `http://bracket-backend:8400` | Private only | +| `NEXT_PUBLIC_HCAPTCHA_SITE_KEY` | hCaptcha site key for forms | `10000000-ffff-ffff-ffff-000000000001` (test key) | Same | ✅ Yes | + +**Important:** `NEXT_PUBLIC_*` variables are **embedded at build time** in Next.js. Changes require rebuilding the Docker image. + +### Backend Environment Variables -## Quick Switch +| Variable | Description | Private Mode | Public Mode | Required | +|----------|-------------|--------------|-------------|----------| +| `ENVIRONMENT` | Deployment environment | `PRODUCTION` | `PRODUCTION` | ✅ Yes | +| `CORS_ORIGINS` | Allowed origin domains | `http://bracket-frontend:3000` | `https://yourdomain.com` | ✅ Yes | +| `PG_DSN` | PostgreSQL connection string | `postgresql://user:pass@postgres:5432/db` | Same | ✅ Yes | +| `JWT_SECRET` | Secret for JWT token signing | Strong random string | Same | ✅ Yes | +| `ADMIN_EMAIL` | Initial admin user email | `admin@yourdomain.com` | Same | ✅ Yes | +| `ADMIN_PASSWORD` | Initial admin password | Strong password | Same | ✅ Yes | +| `BASE_URL` | Public base URL of application | `https://yourdomain.com` | `https://api.yourdomain.com` | ✅ Yes | + +### Docker Compose Variables + +| Variable | Description | Private Mode | Public Mode | +|----------|-------------|--------------|-------------| +| `BACKEND_PORT_MAPPING` | Backend container port mapping | (empty or omit) | `8400:8400` | +| `FRONTEND_PORT_MAPPING` | Frontend container port mapping | `3000:3000` | `3000:3000` | + +--- + +## Quick Mode Switching + +If using the `switch-api-mode.sh` script: ```bash -# Secure mode (recommended) +# Switch to Private Mode (secure) ./switch-api-mode.sh private +docker-compose down && docker-compose up -d -# Public mode +# Switch to Public Mode ./switch-api-mode.sh public +docker-compose down && docker-compose up -d ``` +--- + ## Troubleshooting -### Error: `bracket-backend:8400 not resolved` -- Verify that `USE_PRIVATE_API=true` in `.env` -- Make sure `/pages/api/[...path].ts` file exists +### Error: `bracket-backend:8400 net::ERR_NAME_NOT_RESOLVED` -### Error: `CORS policy` -- Public mode: verify `CORS_ORIGINS` in backend -- Private mode: should not occur +**Cause:** Frontend is trying to connect directly to internal Docker hostname from browser. + +**Solutions:** +1. Verify `NEXT_PUBLIC_USE_PRIVATE_API="true"` in frontend environment +2. Ensure frontend image is `bracket-frontend-local` (has proxy code) +3. Rebuild frontend image: `docker build -t bracket-frontend-local ./frontend` +4. Check that `/frontend/src/pages/api/[...path].ts` exists in your source code + +### Error: `CORS policy: No 'Access-Control-Allow-Origin' header` + +**Cause:** Backend CORS settings don't match frontend origin. + +**Solutions:** +1. **Private Mode:** Set `CORS_ORIGINS=http://bracket-frontend:3000` in backend +2. **Public Mode:** Set `CORS_ORIGINS=https://yourdomain.com` in backend (match frontend domain) +3. Restart backend: `docker-compose restart bracket-backend` ### Backend not responding -- Private mode: check frontend logs -- Public mode: verify nginx points to `172.16.0.4:8400` -## Variables de Entorno +**Private Mode:** +- Check frontend logs: `docker-compose logs bracket-frontend` +- Verify proxy is working: `docker exec bracket-frontend curl http://bracket-backend:8400` +- Ensure backend has no `ports:` section in docker-compose.yml -| Variable | Descripción | Privado | Público | -|----------|-------------|---------|---------| -| `USE_PRIVATE_API` | Modo de operación | `true` | `false` | -| `BACKEND_PORT_MAPPING` | Puerto del backend | vacío | `172.16.0.4:8400:8400` | -| `CORS_ORIGINS` | Dominios permitidos | interno | público | -| `PUBLIC_API_URL` | URL pública del API | `/api` | `https://api.pinar.campeonatos.co` | +**Public Mode:** +- Check backend logs: `docker-compose logs bracket-backend` +- Verify backend port is exposed: `docker-compose ps` should show `8400:8400` +- Test direct access: `curl http://localhost:8400` +- Check Nginx proxy configuration -## Cambio Rápido +### Frontend shows blank page or errors -```bash -# Modo seguro (recomendado) -./switch-api-mode.sh private +1. Check browser console (F12) for errors +2. Verify environment variables: `docker exec bracket-frontend env | grep NEXT_PUBLIC` +3. Ensure correct image is running: `docker inspect bracket-frontend | grep Image` +4. Clear browser cache (Ctrl+Shift+R) or try incognito mode -# Modo público -./switch-api-mode.sh public -``` +### Changes to environment variables not taking effect -## Troubleshooting +**For `NEXT_PUBLIC_*` variables:** +- These are **embedded at build time** in Next.js +- Must rebuild frontend image: `docker build --no-cache -t bracket-frontend-local ./frontend` +- Then restart: `docker-compose up -d` -### Error: `bracket-backend:8400 not resolved` -- Verifica que `USE_PRIVATE_API=true` en `.env` -- Asegúrate de que el archivo `/pages/api/[...path].ts` existe +**For backend variables:** +- Simply restart: `docker-compose restart bracket-backend` -### Error: `CORS policy` -- En modo público: verifica `CORS_ORIGINS` en backend -- En modo privado: no debería ocurrir +--- + +## Summary: Which Mode Should I Use? + +| Scenario | Recommended Mode | Reason | +|----------|------------------|--------| +| Production deployment with Cloudflare/Nginx | **Private** | Enhanced security, simpler SSL setup | +| Development/testing | **Private** | Easier setup, no CORS issues | +| Need third-party API access | **Public** | Backend must be directly accessible | +| Multi-client architecture (mobile app + web) | **Public** | Shared API endpoint | +| Simple single-app deployment | **Private** | Reduced attack surface | -### Backend no responde -- Modo privado: verifica logs del frontend -- Modo público: verifica que nginx apunte a `172.16.0.4:8400` \ No newline at end of file +**Default recommendation:** Use **Private Mode** for most deployments. From 5466144dfe72e4e6e09874de128be3be17179349 Mon Sep 17 00:00:00 2001 From: oscartobar Date: Sat, 1 Nov 2025 11:02:29 -0500 Subject: [PATCH 10/14] Update doc --- API-MODES.md | 789 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 605 insertions(+), 184 deletions(-) diff --git a/API-MODES.md b/API-MODES.md index b99265039..9bae7d267 100644 --- a/API-MODES.md +++ b/API-MODES.md @@ -1,424 +1,845 @@ -# API Configuration: Public vs Private Modes# API Configuration: Public vs Private +# API Configuration: Public vs Private Modes# API Configuration: Public vs Private Modes# API Configuration: Public vs Private -This project supports two API operation modes for different deployment scenarios.This project supports two API operation modes: +This project supports two API operation modes for different deployment scenarios. ----## 🔒 Private Mode (Recommended) +---This project supports two API operation modes for different deployment scenarios.This project supports two API operation modes: -## 🔒 Private Mode (Recommended for Production)**Features:** +## 🔒 Private Mode (Recommended for Production) -- ✅ Backend NOT accessible from Internet -### Overview- ✅ Enhanced security -In **Private Mode**, the backend API is **NOT exposed to the internet**. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues.- ✅ Frontend acts as transparent proxy +### Overview---## 🔒 Private Mode (Recommended) + + + +In **Private Mode**, the backend API is **NOT exposed to the internet**. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues. + + + +### Features## 🔒 Private Mode (Recommended for Production)**Features:** -- ✅ No CORS issues -### Features- ✅ Centralized logging -- ✅ **Backend NOT accessible from Internet** - Only internal Docker network access- ✅ **Users NEVER see /api in URLs** +- ✅ **Backend NOT accessible from Internet** - Only internal Docker network access- ✅ Backend NOT accessible from Internet - ✅ **Enhanced security** - Attack surface reduced -- ✅ **Frontend acts as transparent proxy** - Server-side forwarding via `/api/*` routes**What users see:** +- ✅ **Frontend acts as transparent proxy** - Server-side forwarding via `/api/*` routes### Overview- ✅ Enhanced security -- ✅ **No CORS issues** - Same-origin requests- ✅ `https://yourdomain.com/login` +- ✅ **No CORS issues** - Same-origin requests -- ✅ **Single domain** - Only frontend needs to be publicly exposed- ✅ `https://yourdomain.com/tournaments` +- ✅ **Single domain** - Only frontend needs to be publicly exposedIn **Private Mode**, the backend API is **NOT exposed to the internet**. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues.- ✅ Frontend acts as transparent proxy -- ✅ **Users NEVER see internal URLs** - All requests appear as frontend URLs- ✅ Normal frontend URLs +- ✅ **Users NEVER see internal URLs** - All requests appear as frontend URLs +- ✅ No CORS issues +### What Users See -### What Users See**What happens internally (invisible):** +### Features- ✅ Centralized logging -```- 🔄 `yourdomain.com/api/token` → `bracket-backend:8400/token` +``` -✅ https://yourdomain.com/login- 🔄 Transparent server-side proxy +✅ https://yourdomain.com/login- ✅ **Backend NOT accessible from Internet** - Only internal Docker network access- ✅ **Users NEVER see /api in URLs** ✅ https://yourdomain.com/tournaments -✅ https://yourdomain.com/ (all routes are frontend URLs)**Configuration:** +✅ https://yourdomain.com/ (all routes are frontend URLs)- ✅ **Enhanced security** - Attack surface reduced -``````bash +``` -./switch-api-mode.sh private +- ✅ **Frontend acts as transparent proxy** - Server-side forwarding via `/api/*` routes**What users see:** -### What Happens Internally (Transparent)docker-compose down && docker-compose up -d +### What Happens Internally (Transparent) -`````` +- ✅ **No CORS issues** - Same-origin requests- ✅ `https://yourdomain.com/login` -Browser Request: GET https://yourdomain.com/ +``` - ↓**Nginx (frontend only):** +Browser Request: GET https://yourdomain.com/- ✅ **Single domain** - Only frontend needs to be publicly exposed- ✅ `https://yourdomain.com/tournaments` -Next.js Frontend: Calls /api/clubs internally```nginx + ↓ - ↓server { +Next.js Frontend: Calls /api/clubs internally- ✅ **Users NEVER see internal URLs** - All requests appear as frontend URLs- ✅ Normal frontend URLs -Proxy Handler: /api/clubs → http://bracket-backend:8400/clubs server_name yourdomain.com; + ↓ - ↓ location / { +Proxy Handler: /api/clubs → http://bracket-backend:8400/clubs -Backend Response: Returns data to frontend proxy_pass http://172.16.0.4:3000; + ↓ - ↓ } +Backend Response: Returns data to frontend### What Users See**What happens internally (invisible):** -User Receives: Data rendered on page} + ↓ -`````` +User Receives: Data rendered on page```- 🔄 `yourdomain.com/api/token` → `bracket-backend:8400/token` +``` +✅ https://yourdomain.com/login- 🔄 Transparent server-side proxy -### Docker Compose Configuration## 🌐 Public Mode +### Docker Compose Configuration +✅ https://yourdomain.com/tournaments +#### Method 1: Direct Configuration (Simplest) -**Method 1: Direct Configuration (Simplest)****Features:** +✅ https://yourdomain.com/ (all routes are frontend URLs)**Configuration:** -- ⚠️ Backend accessible from Internet +Edit `docker-compose.yml`: -Edit `docker-compose.yml`:- ⚠️ Requires correct CORS configuration +``````bash -- ⚠️ Two subdomains needed +```yaml -```yaml- ✅ Potentially lower latency +services:./switch-api-mode.sh private -services: + bracket-backend: - bracket-backend:**What users see:** + container_name: bracket-backend### What Happens Internally (Transparent)docker-compose down && docker-compose up -d - container_name: bracket-backend- ✅ `https://yourdomain.com/login` + environment: - environment:- ⚠️ Requests go to `https://api.yourdomain.com/token` + ENVIRONMENT: PRODUCTION`````` - ENVIRONMENT: PRODUCTION + # CORS - Only allow internal frontend communication - # CORS - Only allow internal frontend communication**Configuration:** + CORS_ORIGINS: http://bracket-frontend:3000Browser Request: GET https://yourdomain.com/ - CORS_ORIGINS: http://bracket-frontend:3000```bash + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod - PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod./switch-api-mode.sh public + JWT_SECRET: change_me_in_production ↓**Nginx (frontend only):** - JWT_SECRET: change_me_in_productiondocker-compose down && docker-compose up -d + ADMIN_EMAIL: admin@yourdomain.com - ADMIN_EMAIL: admin@yourdomain.com``` + ADMIN_PASSWORD: change_me_in_productionNext.js Frontend: Calls /api/clubs internally```nginx - ADMIN_PASSWORD: change_me_in_production + BASE_URL: https://yourdomain.com - BASE_URL: https://yourdomain.com**Nginx (frontend + backend):** + image: ghcr.io/evroon/bracket-backend ↓server { - image: ghcr.io/evroon/bracket-backend```nginx + networks: - networks:# Frontend + - bracket_lanProxy Handler: /api/clubs → http://bracket-backend:8400/clubs server_name yourdomain.com; - - bracket_lanserver { + # Backend is PRIVATE - NO ports exposed - # Backend is PRIVATE - NO ports exposed server_name yourdomain.com; + restart: unless-stopped ↓ location / { - restart: unless-stopped location / { - proxy_pass http://172.16.0.4:3000; - bracket-frontend: } + bracket-frontend:Backend Response: Returns data to frontend proxy_pass http://172.16.0.4:3000; - container_name: bracket-frontend} + container_name: bracket-frontend - environment: + environment: ↓ } - # Enable Private API mode# Backend + # Enable Private API mode - NEXT_PUBLIC_USE_PRIVATE_API: "true"server { + NEXT_PUBLIC_USE_PRIVATE_API: "true"User Receives: Data rendered on page} - # Internal backend URL (for proxy) server_name api.yourdomain.com; + # Internal backend URL (for proxy) - NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 location / { + NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400`````` - INTERNAL_API_BASE_URL: http://bracket-backend:8400 proxy_pass http://172.16.0.4:8400; + INTERNAL_API_BASE_URL: http://bracket-backend:8400 - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" } + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" - image: bracket-frontend-local # Must use local image with proxy} + image: bracket-frontend-local # Must use local image with proxy - networks:``` + networks:### Docker Compose Configuration## 🌐 Public Mode - bracket_lan - ports:## Environment Variables + ports: - "3000:3000" # Only frontend is exposed - restart: unless-stopped| Variable | Description | Private | Public | + restart: unless-stopped**Method 1: Direct Configuration (Simplest)****Features:** -```|----------|-------------|---------|---------| +``` -| `USE_PRIVATE_API` | Operation mode | `true` | `false` | +- ⚠️ Backend accessible from Internet -**Method 2: Using Environment Files**| `BACKEND_PORT_MAPPING` | Backend port | empty | `172.16.0.4:8400:8400` | +#### Method 2: Using Environment Files -| `CORS_ORIGINS` | Allowed domains | internal | public | +Edit `docker-compose.yml`:- ⚠️ Requires correct CORS configuration -Create `.env.private`:| `PUBLIC_API_URL` | Public API URL | `/api` | `https://api.yourdomain.com` | +Create `.env.private`: +- ⚠️ Two subdomains needed +```bash -```bash## Quick Switch +# Private API Mode - Backend NOT publicly accessible```yaml- ✅ Potentially lower latency -# Private API Mode - Backend NOT publicly accessible +NEXT_PUBLIC_USE_PRIVATE_API=true -NEXT_PUBLIC_USE_PRIVATE_API=true```bash +CORS_ORIGINS=http://bracket-frontend:3000services: -CORS_ORIGINS=http://bracket-frontend:3000# Secure mode (recommended) +BACKEND_PORT_MAPPING= -BACKEND_PORT_MAPPING=./switch-api-mode.sh private +INTERNAL_API_URL=http://bracket-backend:8400 bracket-backend:**What users see:** -INTERNAL_API_URL=http://bracket-backend:8400 +NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400 -NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400# Public mode +FRONTEND_PORT_MAPPING=3000:3000 container_name: bracket-backend- ✅ `https://yourdomain.com/login` -FRONTEND_PORT_MAPPING=3000:3000./switch-api-mode.sh public +JWT_SECRET=change_me_in_production -JWT_SECRET=change_me_in_production``` +ADMIN_EMAIL=admin@yourdomain.com environment:- ⚠️ Requests go to `https://api.yourdomain.com/token` -ADMIN_EMAIL=admin@yourdomain.com +ADMIN_PASSWORD=change_me_in_production -ADMIN_PASSWORD=change_me_in_production## Troubleshooting +BASE_URL=https://yourdomain.com ENVIRONMENT: PRODUCTION -BASE_URL=https://yourdomain.com +NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 -NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001### Error: `bracket-backend:8400 not resolved` +``` # CORS - Only allow internal frontend communication**Configuration:** -```- Verify that `USE_PRIVATE_API=true` in `.env` -- Make sure `/pages/api/[...path].ts` file exists -Then use variables in `docker-compose.yml`: +Then use variables in `docker-compose.yml`: CORS_ORIGINS: http://bracket-frontend:3000```bash -### Error: `CORS policy` -```yaml- Public mode: verify `CORS_ORIGINS` in backend -services:- Private mode: should not occur +```yaml PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod./switch-api-mode.sh public - bracket-backend: +services: - environment:### Backend not responding + bracket-backend: JWT_SECRET: change_me_in_productiondocker-compose down && docker-compose up -d - CORS_ORIGINS: ${CORS_ORIGINS}- Private mode: check frontend logs + environment: - JWT_SECRET: ${JWT_SECRET}- Public mode: verify nginx points to `172.16.0.4:8400` + CORS_ORIGINS: ${CORS_ORIGINS} ADMIN_EMAIL: admin@yourdomain.com``` - ADMIN_EMAIL: ${ADMIN_EMAIL} + JWT_SECRET: ${JWT_SECRET} - ADMIN_PASSWORD: ${ADMIN_PASSWORD}## Variables de Entorno + ADMIN_EMAIL: ${ADMIN_EMAIL} ADMIN_PASSWORD: change_me_in_production - BASE_URL: ${BASE_URL} + ADMIN_PASSWORD: ${ADMIN_PASSWORD} - ports:| Variable | Descripción | Privado | Público | + BASE_URL: ${BASE_URL} BASE_URL: https://yourdomain.com**Nginx (frontend + backend):** - - "${BACKEND_PORT_MAPPING:-}" # Empty = not exposed|----------|-------------|---------|---------| + ports: -| `USE_PRIVATE_API` | Modo de operación | `true` | `false` | + - "${BACKEND_PORT_MAPPING:-}" # Empty = not exposed image: ghcr.io/evroon/bracket-backend```nginx - bracket-frontend:| `BACKEND_PORT_MAPPING` | Puerto del backend | vacío | `172.16.0.4:8400:8400` | - environment:| `CORS_ORIGINS` | Dominios permitidos | interno | público | - NEXT_PUBLIC_USE_PRIVATE_API: ${NEXT_PUBLIC_USE_PRIVATE_API}| `PUBLIC_API_URL` | URL pública del API | `/api` | `https://api.pinar.campeonatos.co` | + bracket-frontend: networks:# Frontend + + environment: + + NEXT_PUBLIC_USE_PRIVATE_API: ${NEXT_PUBLIC_USE_PRIVATE_API} - bracket_lanserver { NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} - INTERNAL_API_BASE_URL: ${INTERNAL_API_URL}## Cambio Rápido + INTERNAL_API_BASE_URL: ${INTERNAL_API_URL} # Backend is PRIVATE - NO ports exposed server_name yourdomain.com; ports: - - "${FRONTEND_PORT_MAPPING}"```bash + - "${FRONTEND_PORT_MAPPING}" restart: unless-stopped location / { -```# Modo seguro (recomendado) +``` -./switch-api-mode.sh private + proxy_pass http://172.16.0.4:3000; ### Deployment -# Modo público + bracket-frontend: } -```bash./switch-api-mode.sh public +```bash -# Using .env.private``` +# Using .env.private container_name: bracket-frontend} cp .env.private .env -docker-compose down## Troubleshooting +docker-compose down environment: docker-compose up -d -### Error: `bracket-backend:8400 not resolved` + # Enable Private API mode# Backend -# Or with direct configuration- Verifica que `USE_PRIVATE_API=true` en `.env` +# Or with direct configuration -docker-compose down- Asegúrate de que el archivo `/pages/api/[...path].ts` existe +docker-compose down NEXT_PUBLIC_USE_PRIVATE_API: "true"server { docker-compose up -d -```### Error: `CORS policy` +``` # Internal backend URL (for proxy) server_name api.yourdomain.com; -- En modo público: verifica `CORS_ORIGINS` en backend -### Nginx Configuration (Frontend Only)- En modo privado: no debería ocurrir +### Nginx Configuration (Frontend Only) NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 location / { -```nginx### Backend no responde -server {- Modo privado: verifica logs del frontend +```nginx INTERNAL_API_BASE_URL: http://bracket-backend:8400 proxy_pass http://172.16.0.4:8400; + +server { + + listen 443 ssl http2; NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" } - listen 443 ssl http2;- Modo público: verifica que nginx apunte a `172.16.0.4:8400` server_name yourdomain.com; + image: bracket-frontend-local # Must use local image with proxy} + ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; - location / { + ssl_certificate_key /path/to/key.pem; networks:``` + + + + location / { - bracket_lan + proxy_pass http://localhost:3000; - proxy_http_version 1.1; + + proxy_http_version 1.1; ports:## Environment Variables + proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; + + proxy_set_header Connection 'upgrade'; - "3000:3000" # Only frontend is exposed + proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; + + proxy_cache_bypass $http_upgrade; restart: unless-stopped| Variable | Description | Private | Public | + proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;```|----------|-------------|---------|---------| + proxy_set_header X-Forwarded-Proto $scheme; - } + + }| `USE_PRIVATE_API` | Operation mode | `true` | `false` | + } -``` ---- +```**Method 2: Using Environment Files**| `BACKEND_PORT_MAPPING` | Backend port | empty | `172.16.0.4:8400:8400` | + + + +---| `CORS_ORIGINS` | Allowed domains | internal | public | + + + +## 🌐 Public Mode (Direct API Access)Create `.env.private`:| `PUBLIC_API_URL` | Public API URL | `/api` | `https://api.yourdomain.com` | + -## 🌐 Public Mode (Direct API Access) ### Overview -In **Public Mode**, both frontend and backend are exposed to the internet. The frontend makes direct requests to the backend API. This requires proper CORS configuration and typically uses two subdomains. -### Features -- ⚠️ **Backend accessible from Internet** - Requires security considerations + + +In **Public Mode**, both frontend and backend are exposed to the internet. The frontend makes direct requests to the backend API. This requires proper CORS configuration and typically uses two subdomains.```bash## Quick Switch + + + +### Features# Private API Mode - Backend NOT publicly accessible + + + +- ⚠️ **Backend accessible from Internet** - Requires security considerationsNEXT_PUBLIC_USE_PRIVATE_API=true```bash + - ⚠️ **Requires CORS configuration** - Cross-origin requests must be allowed -- ⚠️ **Two domains/subdomains needed** - Frontend and API separately exposed + +- ⚠️ **Two domains/subdomains needed** - Frontend and API separately exposedCORS_ORIGINS=http://bracket-frontend:3000# Secure mode (recommended) + - ✅ **Potentially lower latency** - Direct backend communication -- ✅ **API can be used by other clients** - Third-party integrations possible -### What Users See -``` +- ✅ **API can be used by other clients** - Third-party integrations possibleBACKEND_PORT_MAPPING=./switch-api-mode.sh private + + + +### What Users SeeINTERNAL_API_URL=http://bracket-backend:8400 + + + +```NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400# Public mode + ✅ https://yourdomain.com/login -⚠️ Browser requests go to: https://api.yourdomain.com/token + +⚠️ Browser requests go to: https://api.yourdomain.com/tokenFRONTEND_PORT_MAPPING=3000:3000./switch-api-mode.sh public + ⚠️ Browser requests go to: https://api.yourdomain.com/clubs -``` -### Docker Compose Configuration +```JWT_SECRET=change_me_in_production``` -**Method 1: Direct Configuration** -Edit `docker-compose.yml`: -```yaml +### Docker Compose ConfigurationADMIN_EMAIL=admin@yourdomain.com + + + +#### Method 1: Direct ConfigurationADMIN_PASSWORD=change_me_in_production## Troubleshooting + + + +Edit `docker-compose.yml`:BASE_URL=https://yourdomain.com + + + +```yamlNEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001### Error: `bracket-backend:8400 not resolved` + services: - bracket-backend: + + bracket-backend:```- Verify that `USE_PRIVATE_API=true` in `.env` + container_name: bracket-backend - environment: + + environment:- Make sure `/pages/api/[...path].ts` file exists + ENVIRONMENT: PRODUCTION - # CORS - Allow requests from frontend domain + + # CORS - Allow requests from frontend domainThen use variables in `docker-compose.yml`: + CORS_ORIGINS: https://yourdomain.com,http://localhost:3000 - PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod + + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod### Error: `CORS policy` + JWT_SECRET: change_me_in_production - ADMIN_EMAIL: admin@yourdomain.com + + ADMIN_EMAIL: admin@yourdomain.com```yaml- Public mode: verify `CORS_ORIGINS` in backend + ADMIN_PASSWORD: change_me_in_production - BASE_URL: https://api.yourdomain.com + + BASE_URL: https://api.yourdomain.comservices:- Private mode: should not occur + image: ghcr.io/evroon/bracket-backend - networks: + + networks: bracket-backend: + - bracket_lan - # Backend is PUBLIC - Port exposed + + # Backend is PUBLIC - Port exposed environment:### Backend not responding + ports: - - "8400:8400" + + - "8400:8400" CORS_ORIGINS: ${CORS_ORIGINS}- Private mode: check frontend logs + restart: unless-stopped + JWT_SECRET: ${JWT_SECRET}- Public mode: verify nginx points to `172.16.0.4:8400` + bracket-frontend: - container_name: bracket-frontend + + container_name: bracket-frontend ADMIN_EMAIL: ${ADMIN_EMAIL} + environment: - # Disable Private API mode + + # Disable Private API mode ADMIN_PASSWORD: ${ADMIN_PASSWORD}## Variables de Entorno + NEXT_PUBLIC_USE_PRIVATE_API: "false" - # Public backend URL + + # Public backend URL BASE_URL: ${BASE_URL} + NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.com - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" ports:| Variable | Descripción | Privado | Público | + image: ghcr.io/evroon/bracket-frontend # Can use official image - networks: + + networks: - "${BACKEND_PORT_MAPPING:-}" # Empty = not exposed|----------|-------------|---------|---------| + - bracket_lan - ports: + + ports:| `USE_PRIVATE_API` | Modo de operación | `true` | `false` | + - "3000:3000" - restart: unless-stopped + + restart: unless-stopped bracket-frontend:| `BACKEND_PORT_MAPPING` | Puerto del backend | vacío | `172.16.0.4:8400:8400` | + ``` -**Method 2: Using Environment Files** + environment:| `CORS_ORIGINS` | Dominios permitidos | interno | público | + +#### Method 2: Using Environment Files + + NEXT_PUBLIC_USE_PRIVATE_API: ${NEXT_PUBLIC_USE_PRIVATE_API}| `PUBLIC_API_URL` | URL pública del API | `/api` | `https://api.pinar.campeonatos.co` | Create `.env.public`: + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + ```bash -# Public API Mode - Backend IS publicly accessible + +# Public API Mode - Backend IS publicly accessible INTERNAL_API_BASE_URL: ${INTERNAL_API_URL}## Cambio Rápido + NEXT_PUBLIC_USE_PRIVATE_API=false -CORS_ORIGINS=https://yourdomain.com,http://localhost:3000 + +CORS_ORIGINS=https://yourdomain.com,http://localhost:3000 ports: + BACKEND_PORT_MAPPING=8400:8400 -NEXT_PUBLIC_API_BASE_URL=https://api.yourdomain.com + +NEXT_PUBLIC_API_BASE_URL=https://api.yourdomain.com - "${FRONTEND_PORT_MAPPING}"```bash + INTERNAL_API_URL=http://bracket-backend:8400 -FRONTEND_PORT_MAPPING=3000:3000 + +FRONTEND_PORT_MAPPING=3000:3000```# Modo seguro (recomendado) + JWT_SECRET=change_me_in_production -ADMIN_EMAIL=admin@yourdomain.com + +ADMIN_EMAIL=admin@yourdomain.com./switch-api-mode.sh private + ADMIN_PASSWORD=change_me_in_production -BASE_URL=https://api.yourdomain.com + +BASE_URL=https://api.yourdomain.com### Deployment + NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 -``` -### Deployment +```# Modo público + + + +### Deployment```bash./switch-api-mode.sh public + + + +```bash# Using .env.private``` -```bash # Using .env.public -cp .env.public .env + +cp .env.public .envcp .env.private .env + docker-compose down -docker-compose up -d + +docker-compose up -ddocker-compose down## Troubleshooting + ``` +docker-compose up -d + ### Nginx Configuration (Frontend + Backend) +### Error: `bracket-backend:8400 not resolved` + ```nginx -# Frontend + +# Frontend# Or with direct configuration- Verifica que `USE_PRIVATE_API=true` en `.env` + server { - listen 443 ssl http2; + + listen 443 ssl http2;docker-compose down- Asegúrate de que el archivo `/pages/api/[...path].ts` existe + server_name yourdomain.com; +docker-compose up -d + ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; - location / { + ssl_certificate_key /path/to/key.pem;```### Error: `CORS policy` + + + + location / {- En modo público: verifica `CORS_ORIGINS` en backend + proxy_pass http://localhost:3000; - proxy_http_version 1.1; + + proxy_http_version 1.1;### Nginx Configuration (Frontend Only)- En modo privado: no debería ocurrir + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection 'upgrade'; + proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; + + proxy_cache_bypass $http_upgrade;```nginx### Backend no responde + } -} -# Backend API -server { +}server {- Modo privado: verifica logs del frontend + + + +# Backend API listen 443 ssl http2;- Modo público: verifica que nginx apunte a `172.16.0.4:8400` + +server { server_name yourdomain.com; + listen 443 ssl http2; - server_name api.yourdomain.com; + + server_name api.yourdomain.com; ssl_certificate /path/to/cert.pem; + + ssl_certificate_key /path/to/key.pem; ssl_certificate /path/to/cert.pem; + + ssl_certificate_key /path/to/key.pem; location / { + + proxy_pass http://localhost:3000; + + location / { proxy_http_version 1.1; + + proxy_pass http://localhost:8400; proxy_set_header Upgrade $http_upgrade; + + proxy_http_version 1.1; proxy_set_header Connection 'upgrade'; + + proxy_set_header Host $host; proxy_set_header Host $host; + + proxy_set_header X-Real-IP $remote_addr; proxy_cache_bypass $http_upgrade; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; + + proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + } proxy_set_header X-Forwarded-Proto $scheme; + +} } + +```} + +``` + +--- + +--- + +## Environment Variables Reference + +## 🌐 Public Mode (Direct API Access) + +### Frontend Environment Variables + +### Overview + +| Variable | Description | Private Mode | Public Mode | Required |In **Public Mode**, both frontend and backend are exposed to the internet. The frontend makes direct requests to the backend API. This requires proper CORS configuration and typically uses two subdomains. + +|----------|-------------|--------------|-------------|----------| + +| `NEXT_PUBLIC_USE_PRIVATE_API` | Enables/disables private API mode | `"true"` | `"false"` | ✅ Yes |### Features + +| `NEXT_PUBLIC_API_BASE_URL` | Backend API URL visible to browser | `http://bracket-backend:8400` | `https://api.yourdomain.com` | ✅ Yes |- ⚠️ **Backend accessible from Internet** - Requires security considerations + +| `INTERNAL_API_BASE_URL` | Backend URL for server-side proxy | `http://bracket-backend:8400` | `http://bracket-backend:8400` | Private only |- ⚠️ **Requires CORS configuration** - Cross-origin requests must be allowed + +| `NEXT_PUBLIC_HCAPTCHA_SITE_KEY` | hCaptcha site key for forms | `10000000-ffff-ffff-ffff-000000000001` (test key) | Same | ✅ Yes |- ⚠️ **Two domains/subdomains needed** - Frontend and API separately exposed + +- ✅ **Potentially lower latency** - Direct backend communication + +**Important:** `NEXT_PUBLIC_*` variables are **embedded at build time** in Next.js. Changes require rebuilding the Docker image.- ✅ **API can be used by other clients** - Third-party integrations possible + + + +### Backend Environment Variables### What Users See + +``` + +| Variable | Description | Private Mode | Public Mode | Required |✅ https://yourdomain.com/login + +|----------|-------------|--------------|-------------|----------|⚠️ Browser requests go to: https://api.yourdomain.com/token + +| `ENVIRONMENT` | Deployment environment | `PRODUCTION` | `PRODUCTION` | ✅ Yes |⚠️ Browser requests go to: https://api.yourdomain.com/clubs + +| `CORS_ORIGINS` | Allowed origin domains | `http://bracket-frontend:3000` | `https://yourdomain.com` | ✅ Yes |``` + +| `PG_DSN` | PostgreSQL connection string | `postgresql://user:pass@postgres:5432/db` | Same | ✅ Yes | + +| `JWT_SECRET` | Secret for JWT token signing | Strong random string | Same | ✅ Yes |### Docker Compose Configuration + +| `ADMIN_EMAIL` | Initial admin user email | `admin@yourdomain.com` | Same | ✅ Yes | + +| `ADMIN_PASSWORD` | Initial admin password | Strong password | Same | ✅ Yes |**Method 1: Direct Configuration** + +| `BASE_URL` | Public base URL of application | `https://yourdomain.com` | `https://api.yourdomain.com` | ✅ Yes | + +Edit `docker-compose.yml`: + +### Docker Compose Variables + +```yaml + +| Variable | Description | Private Mode | Public Mode |services: + +|----------|-------------|--------------|-------------| bracket-backend: + +| `BACKEND_PORT_MAPPING` | Backend container port mapping | (empty or omit) | `8400:8400` | container_name: bracket-backend + +| `FRONTEND_PORT_MAPPING` | Frontend container port mapping | `3000:3000` | `3000:3000` | environment: + + ENVIRONMENT: PRODUCTION + +--- # CORS - Allow requests from frontend domain + + CORS_ORIGINS: https://yourdomain.com,http://localhost:3000 + +## Quick Mode Switching PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod + + JWT_SECRET: change_me_in_production + +If using the `switch-api-mode.sh` script: ADMIN_EMAIL: admin@yourdomain.com + + ADMIN_PASSWORD: change_me_in_production + +```bash BASE_URL: https://api.yourdomain.com + +# Switch to Private Mode (secure) image: ghcr.io/evroon/bracket-backend + +./switch-api-mode.sh private networks: + +docker-compose down && docker-compose up -d - bracket_lan + + # Backend is PUBLIC - Port exposed + +# Switch to Public Mode ports: + +./switch-api-mode.sh public - "8400:8400" + +docker-compose down && docker-compose up -d restart: unless-stopped + +``` + + bracket-frontend: + +--- container_name: bracket-frontend + + environment: + +## Troubleshooting # Disable Private API mode + + NEXT_PUBLIC_USE_PRIVATE_API: "false" + +### Error: `bracket-backend:8400 net::ERR_NAME_NOT_RESOLVED` # Public backend URL + + NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.com + +**Cause:** Frontend is trying to connect directly to internal Docker hostname from browser. NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + + image: ghcr.io/evroon/bracket-frontend # Can use official image + +**Solutions:** networks: + + - bracket_lan + +1. Verify `NEXT_PUBLIC_USE_PRIVATE_API="true"` in frontend environment ports: + +2. Ensure frontend image is `bracket-frontend-local` (has proxy code) - "3000:3000" + +3. Rebuild frontend image: `docker build -t bracket-frontend-local ./frontend` restart: unless-stopped + +4. Check that `/frontend/src/pages/api/[...path].ts` exists in your source code``` + + + +### Error: `CORS policy: No 'Access-Control-Allow-Origin' header`**Method 2: Using Environment Files** + + + +**Cause:** Backend CORS settings don't match frontend origin.Create `.env.public`: + + + +**Solutions:**```bash + +# Public API Mode - Backend IS publicly accessible + +1. **Private Mode:** Set `CORS_ORIGINS=http://bracket-frontend:3000` in backendNEXT_PUBLIC_USE_PRIVATE_API=false + +2. **Public Mode:** Set `CORS_ORIGINS=https://yourdomain.com` in backend (match frontend domain)CORS_ORIGINS=https://yourdomain.com,http://localhost:3000 + +3. Restart backend: `docker-compose restart bracket-backend`BACKEND_PORT_MAPPING=8400:8400 + +NEXT_PUBLIC_API_BASE_URL=https://api.yourdomain.com + +### Backend not respondingINTERNAL_API_URL=http://bracket-backend:8400 + +FRONTEND_PORT_MAPPING=3000:3000 + +**Private Mode:**JWT_SECRET=change_me_in_production + +ADMIN_EMAIL=admin@yourdomain.com + +- Check frontend logs: `docker-compose logs bracket-frontend`ADMIN_PASSWORD=change_me_in_production + +- Verify proxy is working: `docker exec bracket-frontend curl http://bracket-backend:8400`BASE_URL=https://api.yourdomain.com + +- Ensure backend has no `ports:` section in docker-compose.ymlNEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 + +``` + +**Public Mode:** + +### Deployment + +- Check backend logs: `docker-compose logs bracket-backend` + +- Verify backend port is exposed: `docker-compose ps` should show `8400:8400````bash + +- Test direct access: `curl http://localhost:8400`# Using .env.public + +- Check Nginx proxy configurationcp .env.public .env + +docker-compose down + +### Frontend shows blank page or errorsdocker-compose up -d + +``` + +1. Check browser console (F12) for errors + +2. Verify environment variables: `docker exec bracket-frontend env | grep NEXT_PUBLIC`### Nginx Configuration (Frontend + Backend) + +3. Ensure correct image is running: `docker inspect bracket-frontend | grep Image` + +4. Clear browser cache (Ctrl+Shift+R) or try incognito mode```nginx + +# Frontend + +### Changes to environment variables not taking effectserver { + + listen 443 ssl http2; + +**For `NEXT_PUBLIC_*` variables:** server_name yourdomain.com; + + + +- These are **embedded at build time** in Next.js ssl_certificate /path/to/cert.pem; + +- Must rebuild frontend image: `docker build --no-cache -t bracket-frontend-local ./frontend` ssl_certificate_key /path/to/key.pem; + +- Then restart: `docker-compose up -d` + + location / { + +**For backend variables:** proxy_pass http://localhost:3000; + + proxy_http_version 1.1; + +- Simply restart: `docker-compose restart bracket-backend` proxy_set_header Upgrade $http_upgrade; + + proxy_set_header Connection 'upgrade'; + +--- proxy_set_header Host $host; + + proxy_cache_bypass $http_upgrade; + +## Summary: Which Mode Should I Use? } + +} + +| Scenario | Recommended Mode | Reason | + +|----------|------------------|--------|# Backend API + +| Production deployment with Cloudflare/Nginx | **Private** | Enhanced security, simpler SSL setup |server { + +| Development/testing | **Private** | Easier setup, no CORS issues | listen 443 ssl http2; + +| Need third-party API access | **Public** | Backend must be directly accessible | server_name api.yourdomain.com; + +| Multi-client architecture (mobile app + web) | **Public** | Shared API endpoint | + +| Simple single-app deployment | **Private** | Reduced attack surface | ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; +**Default recommendation:** Use **Private Mode** for most deployments. + location / { proxy_pass http://localhost:8400; proxy_http_version 1.1; From 21b3a16ef98474a3d2c9bb41f8bc50ebaed30cea Mon Sep 17 00:00:00 2001 From: oscartobar Date: Sat, 1 Nov 2025 11:43:15 -0500 Subject: [PATCH 11/14] update doc api mode --- API-MODES.md | 745 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 599 insertions(+), 146 deletions(-) diff --git a/API-MODES.md b/API-MODES.md index 9bae7d267..026d2ea44 100644 --- a/API-MODES.md +++ b/API-MODES.md @@ -1,4 +1,4 @@ -# API Configuration: Public vs Private Modes# API Configuration: Public vs Private Modes# API Configuration: Public vs Private +# API Configuration: Public vs Private Modes# API Configuration: Public vs Private Modes# API Configuration: Public vs Private Modes# API Configuration: Public vs Private @@ -6,451 +6,904 @@ This project supports two API operation modes for different deployment scenarios ----This project supports two API operation modes for different deployment scenarios.This project supports two API operation modes: +---This project supports two API operation modes for different deployment scenarios. -## 🔒 Private Mode (Recommended for Production) +## Private Mode (Recommended for Production) -### Overview---## 🔒 Private Mode (Recommended) +### Overview---This project supports two API operation modes for different deployment scenarios.This project supports two API operation modes: -In **Private Mode**, the backend API is **NOT exposed to the internet**. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues. +In Private Mode, the backend API is NOT exposed to the internet. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues. -### Features## 🔒 Private Mode (Recommended for Production)**Features:** +### Features## 🔒 Private Mode (Recommended for Production) -- ✅ **Backend NOT accessible from Internet** - Only internal Docker network access- ✅ Backend NOT accessible from Internet +- Backend NOT accessible from Internet - Only internal Docker network access -- ✅ **Enhanced security** - Attack surface reduced +- Enhanced security - Attack surface reduced -- ✅ **Frontend acts as transparent proxy** - Server-side forwarding via `/api/*` routes### Overview- ✅ Enhanced security +- Frontend acts as transparent proxy - Server-side forwarding via /api/* routes### Overview---## 🔒 Private Mode (Recommended) -- ✅ **No CORS issues** - Same-origin requests +- No CORS issues - Same-origin requests -- ✅ **Single domain** - Only frontend needs to be publicly exposedIn **Private Mode**, the backend API is **NOT exposed to the internet**. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues.- ✅ Frontend acts as transparent proxy +- Single domain - Only frontend needs to be publicly exposed -- ✅ **Users NEVER see internal URLs** - All requests appear as frontend URLs +- Users NEVER see internal URLs - All requests appear as frontend URLs -- ✅ No CORS issues +In **Private Mode**, the backend API is **NOT exposed to the internet**. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues. ### What Users See -### Features- ✅ Centralized logging -``` -✅ https://yourdomain.com/login- ✅ **Backend NOT accessible from Internet** - Only internal Docker network access- ✅ **Users NEVER see /api in URLs** +Users access: -✅ https://yourdomain.com/tournaments +- https://yourdomain.com/login### Features## 🔒 Private Mode (Recommended for Production)**Features:** + +- https://yourdomain.com/tournaments + +- All routes are frontend URLs + + + +### What Happens Internally- ✅ **Backend NOT accessible from Internet** - Only internal Docker network access- ✅ Backend NOT accessible from Internet + + + +The flow is transparent to users:- ✅ **Enhanced security** - Attack surface reduced + + + +Browser Request: GET https://yourdomain.com/- ✅ **Frontend acts as transparent proxy** - Server-side forwarding via `/api/*` routes### Overview- ✅ Enhanced security + +Next.js Frontend: Calls /api/clubs internally + +Proxy Handler: /api/clubs → http://bracket-backend:8400/clubs- ✅ **No CORS issues** - Same-origin requests + +Backend Response: Returns data to frontend + +User Receives: Data rendered on page- ✅ **Single domain** - Only frontend needs to be publicly exposedIn **Private Mode**, the backend API is **NOT exposed to the internet**. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues.- ✅ Frontend acts as transparent proxy + + + +### Docker Compose Configuration- ✅ **Users NEVER see internal URLs** - All requests appear as frontend URLs + + + +Method 1: Direct Configuration (Simplest)- ✅ No CORS issues -✅ https://yourdomain.com/ (all routes are frontend URLs)- ✅ **Enhanced security** - Attack surface reduced -``` -- ✅ **Frontend acts as transparent proxy** - Server-side forwarding via `/api/*` routes**What users see:** +Edit docker-compose.yml:### What Users See -### What Happens Internally (Transparent) + + +services:### Features- ✅ Centralized logging + + bracket-backend: + + container_name: bracket-backend``` + + environment: + + ENVIRONMENT: PRODUCTION✅ https://yourdomain.com/login- ✅ **Backend NOT accessible from Internet** - Only internal Docker network access- ✅ **Users NEVER see /api in URLs** + + CORS_ORIGINS: http://bracket-frontend:3000 + + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod✅ https://yourdomain.com/tournaments + + JWT_SECRET: change_me_in_production + + ADMIN_EMAIL: admin@yourdomain.com✅ https://yourdomain.com/ (all routes are frontend URLs)- ✅ **Enhanced security** - Attack surface reduced + + ADMIN_PASSWORD: change_me_in_production + + BASE_URL: https://yourdomain.com``` + + image: ghcr.io/evroon/bracket-backend + + networks:- ✅ **Frontend acts as transparent proxy** - Server-side forwarding via `/api/*` routes**What users see:** + + - bracket_lan + + # Backend is PRIVATE - NO ports exposed### What Happens Internally (Transparent) + + restart: unless-stopped - ✅ **No CORS issues** - Same-origin requests- ✅ `https://yourdomain.com/login` -``` + bracket-frontend: -Browser Request: GET https://yourdomain.com/- ✅ **Single domain** - Only frontend needs to be publicly exposed- ✅ `https://yourdomain.com/tournaments` + container_name: bracket-frontend``` - ↓ + environment: -Next.js Frontend: Calls /api/clubs internally- ✅ **Users NEVER see internal URLs** - All requests appear as frontend URLs- ✅ Normal frontend URLs + NEXT_PUBLIC_USE_PRIVATE_API: "true"Browser Request: GET https://yourdomain.com/- ✅ **Single domain** - Only frontend needs to be publicly exposed- ✅ `https://yourdomain.com/tournaments` - ↓ + NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 + + INTERNAL_API_BASE_URL: http://bracket-backend:8400 ↓ + + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + + image: bracket-frontend-localNext.js Frontend: Calls /api/clubs internally- ✅ **Users NEVER see internal URLs** - All requests appear as frontend URLs- ✅ Normal frontend URLs + + networks: + + - bracket_lan ↓ + + ports: -Proxy Handler: /api/clubs → http://bracket-backend:8400/clubs + - "3000:3000"Proxy Handler: /api/clubs → http://bracket-backend:8400/clubs + + restart: unless-stopped ↓ +Method 2: Using Environment Files + Backend Response: Returns data to frontend### What Users See**What happens internally (invisible):** +Create .env.private file: + ↓ -User Receives: Data rendered on page```- 🔄 `yourdomain.com/api/token` → `bracket-backend:8400/token` +NEXT_PUBLIC_USE_PRIVATE_API=true -``` +CORS_ORIGINS=http://bracket-frontend:3000User Receives: Data rendered on page```- 🔄 `yourdomain.com/api/token` → `bracket-backend:8400/token` + +BACKEND_PORT_MAPPING= + +INTERNAL_API_URL=http://bracket-backend:8400``` + +NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400 + +FRONTEND_PORT_MAPPING=3000:3000✅ https://yourdomain.com/login- 🔄 Transparent server-side proxy -✅ https://yourdomain.com/login- 🔄 Transparent server-side proxy +JWT_SECRET=change_me_in_production + +ADMIN_EMAIL=admin@yourdomain.com### Docker Compose Configuration -### Docker Compose Configuration +ADMIN_PASSWORD=change_me_in_production -✅ https://yourdomain.com/tournaments +BASE_URL=https://yourdomain.com✅ https://yourdomain.com/tournaments + +NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 #### Method 1: Direct Configuration (Simplest) +Then use variables in docker-compose.yml: + ✅ https://yourdomain.com/ (all routes are frontend URLs)**Configuration:** -Edit `docker-compose.yml`: +services: -``````bash + bracket-backend:Edit `docker-compose.yml`: -```yaml + environment: -services:./switch-api-mode.sh private + CORS_ORIGINS: ${CORS_ORIGINS}``````bash - bracket-backend: + JWT_SECRET: ${JWT_SECRET} - container_name: bracket-backend### What Happens Internally (Transparent)docker-compose down && docker-compose up -d + ADMIN_EMAIL: ${ADMIN_EMAIL}```yaml + + ADMIN_PASSWORD: ${ADMIN_PASSWORD} + + BASE_URL: ${BASE_URL}services:./switch-api-mode.sh private + + ports: + + - "${BACKEND_PORT_MAPPING:-}" bracket-backend: + + + + bracket-frontend: container_name: bracket-backend### What Happens Internally (Transparent)docker-compose down && docker-compose up -d environment: - ENVIRONMENT: PRODUCTION`````` + NEXT_PUBLIC_USE_PRIVATE_API: ${NEXT_PUBLIC_USE_PRIVATE_API} environment: + + NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + + INTERNAL_API_BASE_URL: ${INTERNAL_API_URL} ENVIRONMENT: PRODUCTION`````` + + ports: + + - "${FRONTEND_PORT_MAPPING}" # CORS - Only allow internal frontend communication + + + +### Deployment CORS_ORIGINS: http://bracket-frontend:3000Browser Request: GET https://yourdomain.com/ + + + +Using .env.private: PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod + + + +cp .env.private .env JWT_SECRET: change_me_in_production ↓**Nginx (frontend only):** + +docker-compose down - # CORS - Only allow internal frontend communication +docker-compose up -d ADMIN_EMAIL: admin@yourdomain.com - CORS_ORIGINS: http://bracket-frontend:3000Browser Request: GET https://yourdomain.com/ - PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod - JWT_SECRET: change_me_in_production ↓**Nginx (frontend only):** +Or with direct configuration: ADMIN_PASSWORD: change_me_in_productionNext.js Frontend: Calls /api/clubs internally```nginx - ADMIN_EMAIL: admin@yourdomain.com - ADMIN_PASSWORD: change_me_in_productionNext.js Frontend: Calls /api/clubs internally```nginx - BASE_URL: https://yourdomain.com +docker-compose down BASE_URL: https://yourdomain.com + +docker-compose up -d image: ghcr.io/evroon/bracket-backend ↓server { +### Nginx Configuration (Frontend Only) + networks: - - bracket_lanProxy Handler: /api/clubs → http://bracket-backend:8400/clubs server_name yourdomain.com; +server { + + listen 443 ssl http2; - bracket_lanProxy Handler: /api/clubs → http://bracket-backend:8400/clubs server_name yourdomain.com; + + server_name yourdomain.com; # Backend is PRIVATE - NO ports exposed - restart: unless-stopped ↓ location / { + ssl_certificate /path/to/cert.pem; + ssl_certificate_key /path/to/key.pem; restart: unless-stopped ↓ location / { - bracket-frontend:Backend Response: Returns data to frontend proxy_pass http://172.16.0.4:3000; - container_name: bracket-frontend + location / { + + proxy_pass http://localhost:3000; - environment: ↓ } + proxy_http_version 1.1; bracket-frontend:Backend Response: Returns data to frontend proxy_pass http://172.16.0.4:3000; - # Enable Private API mode + proxy_set_header Upgrade $http_upgrade; + + proxy_set_header Connection 'upgrade'; container_name: bracket-frontend + + proxy_set_header Host $host; + + proxy_cache_bypass $http_upgrade; environment: ↓ } + + proxy_set_header X-Real-IP $remote_addr; - NEXT_PUBLIC_USE_PRIVATE_API: "true"User Receives: Data rendered on page} + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Enable Private API mode + + proxy_set_header X-Forwarded-Proto $scheme; + + } NEXT_PUBLIC_USE_PRIVATE_API: "true"User Receives: Data rendered on page} + +} # Internal backend URL (for proxy) +--- + NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400`````` +## Public Mode (Direct API Access) + INTERNAL_API_BASE_URL: http://bracket-backend:8400 +### Overview + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" +In Public Mode, both frontend and backend are exposed to the internet. The frontend makes direct requests to the backend API. This requires proper CORS configuration and typically uses two subdomains. + image: bracket-frontend-local # Must use local image with proxy +### Features + networks:### Docker Compose Configuration## 🌐 Public Mode - - bracket_lan +- Backend accessible from Internet - Requires security considerations - ports: +- Requires CORS configuration - Cross-origin requests must be allowed - bracket_lan + +- Two domains/subdomains needed - Frontend and API separately exposed + +- Potentially lower latency - Direct backend communication ports: + +- API can be used by other clients - Third-party integrations possible - "3000:3000" # Only frontend is exposed +### What Users See + restart: unless-stopped**Method 1: Direct Configuration (Simplest)****Features:** -``` +Users access: -- ⚠️ Backend accessible from Internet +- https://yourdomain.com/login``` -#### Method 2: Using Environment Files +- Browser requests go to: https://api.yourdomain.com/token -Edit `docker-compose.yml`:- ⚠️ Requires correct CORS configuration +- Browser requests go to: https://api.yourdomain.com/clubs- ⚠️ Backend accessible from Internet -Create `.env.private`: -- ⚠️ Two subdomains needed -```bash +### Docker Compose Configuration#### Method 2: Using Environment Files -# Private API Mode - Backend NOT publicly accessible```yaml- ✅ Potentially lower latency -NEXT_PUBLIC_USE_PRIVATE_API=true -CORS_ORIGINS=http://bracket-frontend:3000services: +Method 1: Direct ConfigurationEdit `docker-compose.yml`:- ⚠️ Requires correct CORS configuration -BACKEND_PORT_MAPPING= -INTERNAL_API_URL=http://bracket-backend:8400 bracket-backend:**What users see:** -NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400 +Edit docker-compose.yml:Create `.env.private`: -FRONTEND_PORT_MAPPING=3000:3000 container_name: bracket-backend- ✅ `https://yourdomain.com/login` -JWT_SECRET=change_me_in_production -ADMIN_EMAIL=admin@yourdomain.com environment:- ⚠️ Requests go to `https://api.yourdomain.com/token` +services:- ⚠️ Two subdomains needed -ADMIN_PASSWORD=change_me_in_production + bracket-backend: -BASE_URL=https://yourdomain.com ENVIRONMENT: PRODUCTION + container_name: bracket-backend```bash -NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 + environment: + + ENVIRONMENT: PRODUCTION# Private API Mode - Backend NOT publicly accessible```yaml- ✅ Potentially lower latency + + CORS_ORIGINS: https://yourdomain.com,http://localhost:3000 + + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prodNEXT_PUBLIC_USE_PRIVATE_API=true + + JWT_SECRET: change_me_in_production + + ADMIN_EMAIL: admin@yourdomain.comCORS_ORIGINS=http://bracket-frontend:3000services: + + ADMIN_PASSWORD: change_me_in_production + + BASE_URL: https://api.yourdomain.comBACKEND_PORT_MAPPING= + + image: ghcr.io/evroon/bracket-backend + + networks:INTERNAL_API_URL=http://bracket-backend:8400 bracket-backend:**What users see:** + + - bracket_lan + + ports:NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400 + + - "8400:8400" + + restart: unless-stoppedFRONTEND_PORT_MAPPING=3000:3000 container_name: bracket-backend- ✅ `https://yourdomain.com/login` + + + + bracket-frontend:JWT_SECRET=change_me_in_production + + container_name: bracket-frontend + + environment:ADMIN_EMAIL=admin@yourdomain.com environment:- ⚠️ Requests go to `https://api.yourdomain.com/token` + + NEXT_PUBLIC_USE_PRIVATE_API: "false" + + NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.comADMIN_PASSWORD=change_me_in_production + + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + + image: ghcr.io/evroon/bracket-frontendBASE_URL=https://yourdomain.com ENVIRONMENT: PRODUCTION + + networks: + + - bracket_lanNEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 + + ports: + + - "3000:3000"``` # CORS - Only allow internal frontend communication**Configuration:** + + restart: unless-stopped -``` # CORS - Only allow internal frontend communication**Configuration:** +Method 2: Using Environment Files Then use variables in `docker-compose.yml`: CORS_ORIGINS: http://bracket-frontend:3000```bash +Create .env.public file: -```yaml PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod./switch-api-mode.sh public -services: +NEXT_PUBLIC_USE_PRIVATE_API=false - bracket-backend: JWT_SECRET: change_me_in_productiondocker-compose down && docker-compose up -d +CORS_ORIGINS=https://yourdomain.com,http://localhost:3000```yaml PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod./switch-api-mode.sh public - environment: +BACKEND_PORT_MAPPING=8400:8400 + +NEXT_PUBLIC_API_BASE_URL=https://api.yourdomain.comservices: + +INTERNAL_API_URL=http://bracket-backend:8400 + +FRONTEND_PORT_MAPPING=3000:3000 bracket-backend: JWT_SECRET: change_me_in_productiondocker-compose down && docker-compose up -d + +JWT_SECRET=change_me_in_production + +ADMIN_EMAIL=admin@yourdomain.com environment: + +ADMIN_PASSWORD=change_me_in_production - CORS_ORIGINS: ${CORS_ORIGINS} ADMIN_EMAIL: admin@yourdomain.com``` +BASE_URL=https://api.yourdomain.com CORS_ORIGINS: ${CORS_ORIGINS} ADMIN_EMAIL: admin@yourdomain.com``` + +NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 JWT_SECRET: ${JWT_SECRET} +### Deployment + ADMIN_EMAIL: ${ADMIN_EMAIL} ADMIN_PASSWORD: change_me_in_production +Using .env.public: + ADMIN_PASSWORD: ${ADMIN_PASSWORD} - BASE_URL: ${BASE_URL} BASE_URL: https://yourdomain.com**Nginx (frontend + backend):** +cp .env.public .env + +docker-compose down BASE_URL: ${BASE_URL} BASE_URL: https://yourdomain.com**Nginx (frontend + backend):** + +docker-compose up -d ports: +### Nginx Configuration (Frontend + Backend) + - "${BACKEND_PORT_MAPPING:-}" # Empty = not exposed image: ghcr.io/evroon/bracket-backend```nginx +Frontend: + + +server { + + listen 443 ssl http2; bracket-frontend: networks:# Frontend - bracket-frontend: networks:# Frontend + server_name yourdomain.com; environment: - NEXT_PUBLIC_USE_PRIVATE_API: ${NEXT_PUBLIC_USE_PRIVATE_API} - bracket_lanserver { + ssl_certificate /path/to/cert.pem; - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} + ssl_certificate_key /path/to/key.pem; NEXT_PUBLIC_USE_PRIVATE_API: ${NEXT_PUBLIC_USE_PRIVATE_API} - bracket_lanserver { - INTERNAL_API_BASE_URL: ${INTERNAL_API_URL} # Backend is PRIVATE - NO ports exposed server_name yourdomain.com; - ports: - - "${FRONTEND_PORT_MAPPING}" restart: unless-stopped location / { + location / { NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} -``` + proxy_pass http://localhost:3000; - proxy_pass http://172.16.0.4:3000; + proxy_http_version 1.1; INTERNAL_API_BASE_URL: ${INTERNAL_API_URL} # Backend is PRIVATE - NO ports exposed server_name yourdomain.com; -### Deployment + proxy_set_header Upgrade $http_upgrade; - bracket-frontend: } + proxy_set_header Connection 'upgrade'; ports: -```bash + proxy_set_header Host $host; + + proxy_cache_bypass $http_upgrade; - "${FRONTEND_PORT_MAPPING}" restart: unless-stopped location / { + + } + +}``` + + + +Backend API: proxy_pass http://172.16.0.4:3000; + + + +server {### Deployment + + listen 443 ssl http2; + + server_name api.yourdomain.com; bracket-frontend: } + + + + ssl_certificate /path/to/cert.pem;```bash + + ssl_certificate_key /path/to/key.pem; # Using .env.private container_name: bracket-frontend} -cp .env.private .env + location / { -docker-compose down environment: + proxy_pass http://localhost:8400;cp .env.private .env -docker-compose up -d + proxy_http_version 1.1; + + proxy_set_header Host $host;docker-compose down environment: + + proxy_set_header X-Real-IP $remote_addr; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;docker-compose up -d - # Enable Private API mode# Backend + proxy_set_header X-Forwarded-Proto $scheme; + + } # Enable Private API mode# Backend + +} # Or with direct configuration +--- + docker-compose down NEXT_PUBLIC_USE_PRIVATE_API: "true"server { +## Environment Variables Reference + docker-compose up -d +### Frontend Environment Variables + ``` # Internal backend URL (for proxy) server_name api.yourdomain.com; +Variable: NEXT_PUBLIC_USE_PRIVATE_API +Description: Enables/disables private API mode -### Nginx Configuration (Frontend Only) NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 location / { +Private Mode: "true" +Public Mode: "false"### Nginx Configuration (Frontend Only) NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 location / { +Required: Yes -```nginx INTERNAL_API_BASE_URL: http://bracket-backend:8400 proxy_pass http://172.16.0.4:8400; -server { + +Variable: NEXT_PUBLIC_API_BASE_URL + +Description: Backend API URL visible to browser```nginx INTERNAL_API_BASE_URL: http://bracket-backend:8400 proxy_pass http://172.16.0.4:8400; + +Private Mode: http://bracket-backend:8400 + +Public Mode: https://api.yourdomain.comserver { + +Required: Yes listen 443 ssl http2; NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" } - server_name yourdomain.com; +Variable: INTERNAL_API_BASE_URL + +Description: Backend URL for server-side proxy server_name yourdomain.com; + +Private Mode: http://bracket-backend:8400 + +Public Mode: http://bracket-backend:8400 image: bracket-frontend-local # Must use local image with proxy} - image: bracket-frontend-local # Must use local image with proxy} +Required: Private only ssl_certificate /path/to/cert.pem; - ssl_certificate_key /path/to/key.pem; networks:``` +Variable: NEXT_PUBLIC_HCAPTCHA_SITE_KEY +Description: hCaptcha site key for forms ssl_certificate_key /path/to/key.pem; networks:``` +Private Mode: 10000000-ffff-ffff-ffff-000000000001 (test key) + +Public Mode: Same + +Required: Yes location / { - bracket_lan +Important: NEXT_PUBLIC_* variables are embedded at build time in Next.js. Changes require rebuilding the Docker image. + proxy_pass http://localhost:3000; +### Backend Environment Variables + proxy_http_version 1.1; ports:## Environment Variables - proxy_set_header Upgrade $http_upgrade; +Variable: ENVIRONMENT - proxy_set_header Connection 'upgrade'; - "3000:3000" # Only frontend is exposed +Description: Deployment environment proxy_set_header Upgrade $http_upgrade; + +Private Mode: PRODUCTION + +Public Mode: PRODUCTION proxy_set_header Connection 'upgrade'; - "3000:3000" # Only frontend is exposed + +Required: Yes proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; restart: unless-stopped| Variable | Description | Private | Public | +Variable: CORS_ORIGINS - proxy_set_header X-Real-IP $remote_addr; +Description: Allowed origin domains proxy_cache_bypass $http_upgrade; restart: unless-stopped| Variable | Description | Private | Public | + +Private Mode: http://bracket-frontend:3000 + +Public Mode: https://yourdomain.com proxy_set_header X-Real-IP $remote_addr; + +Required: Yes proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;```|----------|-------------|---------|---------| - proxy_set_header X-Forwarded-Proto $scheme; +Variable: PG_DSN + +Description: PostgreSQL connection string proxy_set_header X-Forwarded-Proto $scheme; - }| `USE_PRIVATE_API` | Operation mode | `true` | `false` | +Private Mode: postgresql://user:pass@postgres:5432/db + +Public Mode: Same }| `USE_PRIVATE_API` | Operation mode | `true` | `false` | + +Required: Yes } -```**Method 2: Using Environment Files**| `BACKEND_PORT_MAPPING` | Backend port | empty | `172.16.0.4:8400:8400` | +Variable: JWT_SECRET + +Description: Secret for JWT token signing```**Method 2: Using Environment Files**| `BACKEND_PORT_MAPPING` | Backend port | empty | `172.16.0.4:8400:8400` | +Private Mode: Strong random string +Public Mode: Same + +Required: Yes ---| `CORS_ORIGINS` | Allowed domains | internal | public | +Variable: ADMIN_EMAIL +Description: Initial admin user email -## 🌐 Public Mode (Direct API Access)Create `.env.private`:| `PUBLIC_API_URL` | Public API URL | `/api` | `https://api.yourdomain.com` | +Private Mode: admin@yourdomain.com +Public Mode: Same## 🌐 Public Mode (Direct API Access)Create `.env.private`:| `PUBLIC_API_URL` | Public API URL | `/api` | `https://api.yourdomain.com` | +Required: Yes -### Overview +Variable: ADMIN_PASSWORD + +Description: Initial admin password### Overview + +Private Mode: Strong password + +Public Mode: Same + +Required: Yes In **Public Mode**, both frontend and backend are exposed to the internet. The frontend makes direct requests to the backend API. This requires proper CORS configuration and typically uses two subdomains.```bash## Quick Switch +Variable: BASE_URL + +Description: Public base URL of application +Private Mode: https://yourdomain.com -### Features# Private API Mode - Backend NOT publicly accessible +Public Mode: https://api.yourdomain.com### Features# Private API Mode - Backend NOT publicly accessible +Required: Yes + +### Docker Compose Variables + - ⚠️ **Backend accessible from Internet** - Requires security considerationsNEXT_PUBLIC_USE_PRIVATE_API=true```bash -- ⚠️ **Requires CORS configuration** - Cross-origin requests must be allowed +Variable: BACKEND_PORT_MAPPING -- ⚠️ **Two domains/subdomains needed** - Frontend and API separately exposedCORS_ORIGINS=http://bracket-frontend:3000# Secure mode (recommended) +Description: Backend container port mapping- ⚠️ **Requires CORS configuration** - Cross-origin requests must be allowed + +Private Mode: (empty or omit) + +Public Mode: 8400:8400- ⚠️ **Two domains/subdomains needed** - Frontend and API separately exposedCORS_ORIGINS=http://bracket-frontend:3000# Secure mode (recommended) -- ✅ **Potentially lower latency** - Direct backend communication -- ✅ **API can be used by other clients** - Third-party integrations possibleBACKEND_PORT_MAPPING=./switch-api-mode.sh private +Variable: FRONTEND_PORT_MAPPING- ✅ **Potentially lower latency** - Direct backend communication +Description: Frontend container port mapping + +Private Mode: 3000:3000- ✅ **API can be used by other clients** - Third-party integrations possibleBACKEND_PORT_MAPPING=./switch-api-mode.sh private + +Public Mode: 3000:3000 + + + +--- ### What Users SeeINTERNAL_API_URL=http://bracket-backend:8400 +## Quick Mode Switching + + +If using the switch-api-mode.sh script: ```NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400# Public mode +Switch to Private Mode (secure): + ✅ https://yourdomain.com/login -⚠️ Browser requests go to: https://api.yourdomain.com/tokenFRONTEND_PORT_MAPPING=3000:3000./switch-api-mode.sh public +./switch-api-mode.sh private + +docker-compose down && docker-compose up -d⚠️ Browser requests go to: https://api.yourdomain.com/tokenFRONTEND_PORT_MAPPING=3000:3000./switch-api-mode.sh public -⚠️ Browser requests go to: https://api.yourdomain.com/clubs -```JWT_SECRET=change_me_in_production``` +Switch to Public Mode:⚠️ Browser requests go to: https://api.yourdomain.com/clubs + +./switch-api-mode.sh public```JWT_SECRET=change_me_in_production``` + +docker-compose down && docker-compose up -d + + + +--- + ### Docker Compose ConfigurationADMIN_EMAIL=admin@yourdomain.com +## Troubleshooting + +### Error: bracket-backend:8400 net::ERR_NAME_NOT_RESOLVED + #### Method 1: Direct ConfigurationADMIN_PASSWORD=change_me_in_production## Troubleshooting +Cause: Frontend is trying to connect directly to internal Docker hostname from browser. + + +Solutions: -Edit `docker-compose.yml`:BASE_URL=https://yourdomain.com +1. Verify NEXT_PUBLIC_USE_PRIVATE_API="true" in frontend environmentEdit `docker-compose.yml`:BASE_URL=https://yourdomain.com +2. Ensure frontend image is bracket-frontend-local (has proxy code) +3. Rebuild frontend image: docker build -t bracket-frontend-local ./frontend + +4. Check that /frontend/src/pages/api/[...path].ts exists in your source code ```yamlNEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001### Error: `bracket-backend:8400 not resolved` +### Error: CORS policy: No 'Access-Control-Allow-Origin' header + services: +Cause: Backend CORS settings don't match frontend origin. + bracket-backend:```- Verify that `USE_PRIVATE_API=true` in `.env` - container_name: bracket-backend +Solutions: - environment:- Make sure `/pages/api/[...path].ts` file exists +1. Private Mode: Set CORS_ORIGINS=http://bracket-frontend:3000 in backend container_name: bracket-backend - ENVIRONMENT: PRODUCTION +2. Public Mode: Set CORS_ORIGINS=https://yourdomain.com in backend (match frontend domain) - # CORS - Allow requests from frontend domainThen use variables in `docker-compose.yml`: +3. Restart backend: docker-compose restart bracket-backend environment:- Make sure `/pages/api/[...path].ts` file exists - CORS_ORIGINS: https://yourdomain.com,http://localhost:3000 + + +### Backend not responding ENVIRONMENT: PRODUCTION + + + +Private Mode: # CORS - Allow requests from frontend domainThen use variables in `docker-compose.yml`: + +- Check frontend logs: docker-compose logs bracket-frontend + +- Verify proxy is working: docker exec bracket-frontend curl http://bracket-backend:8400 CORS_ORIGINS: https://yourdomain.com,http://localhost:3000 + +- Ensure backend has no ports: section in docker-compose.yml PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod### Error: `CORS policy` - JWT_SECRET: change_me_in_production +Public Mode: + +- Check backend logs: docker-compose logs bracket-backend JWT_SECRET: change_me_in_production + +- Verify backend port is exposed: docker-compose ps should show 8400:8400 - ADMIN_EMAIL: admin@yourdomain.com```yaml- Public mode: verify `CORS_ORIGINS` in backend +- Test direct access: curl http://localhost:8400 ADMIN_EMAIL: admin@yourdomain.com```yaml- Public mode: verify `CORS_ORIGINS` in backend + +- Check Nginx proxy configuration ADMIN_PASSWORD: change_me_in_production +### Frontend shows blank page or errors + BASE_URL: https://api.yourdomain.comservices:- Private mode: should not occur - image: ghcr.io/evroon/bracket-backend +1. Check browser console (F12) for errors - networks: bracket-backend: +2. Verify environment variables: docker exec bracket-frontend env | grep NEXT_PUBLIC image: ghcr.io/evroon/bracket-backend - - bracket_lan +3. Ensure correct image is running: docker inspect bracket-frontend | grep Image + +4. Clear browser cache (Ctrl+Shift+R) or try incognito mode networks: bracket-backend: - # Backend is PUBLIC - Port exposed environment:### Backend not responding - ports: + +### Changes to environment variables not taking effect - bracket_lan + + + +For NEXT_PUBLIC_* variables: # Backend is PUBLIC - Port exposed environment:### Backend not responding + +- These are embedded at build time in Next.js + +- Must rebuild frontend image: docker build --no-cache -t bracket-frontend-local ./frontend ports: + +- Then restart: docker-compose up -d - "8400:8400" CORS_ORIGINS: ${CORS_ORIGINS}- Private mode: check frontend logs - restart: unless-stopped +For backend variables: - JWT_SECRET: ${JWT_SECRET}- Public mode: verify nginx points to `172.16.0.4:8400` +- Simply restart: docker-compose restart bracket-backend restart: unless-stopped - bracket-frontend: - container_name: bracket-frontend ADMIN_EMAIL: ${ADMIN_EMAIL} - environment: +--- JWT_SECRET: ${JWT_SECRET}- Public mode: verify nginx points to `172.16.0.4:8400` - # Disable Private API mode ADMIN_PASSWORD: ${ADMIN_PASSWORD}## Variables de Entorno - NEXT_PUBLIC_USE_PRIVATE_API: "false" - # Public backend URL BASE_URL: ${BASE_URL} +## Summary: Which Mode Should I Use? bracket-frontend: - NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.com - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" ports:| Variable | Descripción | Privado | Público | - image: ghcr.io/evroon/bracket-frontend # Can use official image +Scenario: Production deployment with Cloudflare/Nginx container_name: bracket-frontend ADMIN_EMAIL: ${ADMIN_EMAIL} - networks: - "${BACKEND_PORT_MAPPING:-}" # Empty = not exposed|----------|-------------|---------|---------| +Recommended Mode: Private - - bracket_lan +Reason: Enhanced security, simpler SSL setup environment: + + + +Scenario: Development/testing # Disable Private API mode ADMIN_PASSWORD: ${ADMIN_PASSWORD}## Variables de Entorno + +Recommended Mode: Private + +Reason: Easier setup, no CORS issues NEXT_PUBLIC_USE_PRIVATE_API: "false" + + + +Scenario: Need third-party API access # Public backend URL BASE_URL: ${BASE_URL} + +Recommended Mode: Public + +Reason: Backend must be directly accessible NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.com + + + +Scenario: Multi-client architecture (mobile app + web) NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" ports:| Variable | Descripción | Privado | Público | + +Recommended Mode: Public + +Reason: Shared API endpoint image: ghcr.io/evroon/bracket-frontend # Can use official image + + + +Scenario: Simple single-app deployment networks: - "${BACKEND_PORT_MAPPING:-}" # Empty = not exposed|----------|-------------|---------|---------| + +Recommended Mode: Private + +Reason: Reduced attack surface - bracket_lan + + + +Default recommendation: Use Private Mode for most deployments. ports:| `USE_PRIVATE_API` | Modo de operación | `true` | `false` | - ports:| `USE_PRIVATE_API` | Modo de operación | `true` | `false` | - "3000:3000" From a977359ba8bdd6f61feb8092d901206eec244391 Mon Sep 17 00:00:00 2001 From: oscartobar Date: Sat, 1 Nov 2025 11:56:36 -0500 Subject: [PATCH 12/14] update api-modes --- API-MODES.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/API-MODES.md b/API-MODES.md index 026d2ea44..c6571acf9 100644 --- a/API-MODES.md +++ b/API-MODES.md @@ -1,3 +1,96 @@ +API modes: Private vs Public + +Overview +- The application supports two modes of API access. +- Private mode (recommended): only the frontend is public; the backend stays private and is reached via the frontend’s internal proxy. +- Public mode: both frontend and backend are public; the frontend calls the backend directly. + +Private mode (recommended) +- What it is + - The backend is not exposed to the internet. + - The frontend exposes an internal proxy under /api that forwards requests to the backend inside the Docker network. + - This eliminates CORS issues and reduces the attack surface. +- How it works + - Browser → Frontend (public) → /api/... → Frontend proxy → Backend (private) → Response → Browser. +- Configure + - Frontend + - NEXT_PUBLIC_USE_PRIVATE_API = "true" (string) + - INTERNAL_API_BASE_URL points to the backend service URL on the Docker network (for example: http://bracket-backend:8400) + - Expose only the frontend port publicly (for example: 3000) + - Backend + - Do not publish the backend port; the backend must be reachable only from the Docker network + - CORS_ORIGINS must include the frontend container origin (for example: http://bracket-frontend:3000) + - BASE_URL is your public site URL (for example: https://yourdomain.com) +- When to use + - Production behind Cloudflare/Nginx + - Single-frontend deployments where the backend should not be directly reachable + +Public mode (direct API) +- What it is + - Both frontend and backend are publicly accessible. + - The frontend calls the backend directly using the public API URL. +- Configure + - Frontend + - NEXT_PUBLIC_USE_PRIVATE_API = "false" (string) + - NEXT_PUBLIC_API_BASE_URL is the public API URL (for example: https://api.yourdomain.com) + - Backend + - Publish the backend port (for example: 8400) + - CORS_ORIGINS must include the public frontend domain (for example: https://yourdomain.com) + - BASE_URL is the public API base URL (for example: https://api.yourdomain.com) +- When to use + - Multi-client scenarios (mobile + web) or when third parties must call your API directly + +Environment variables +- Frontend + - NEXT_PUBLIC_USE_PRIVATE_API: "true" or "false" to select Private or Public mode + - NEXT_PUBLIC_API_BASE_URL: public backend URL used by the browser in Public mode + - INTERNAL_API_BASE_URL: internal backend URL used by the proxy in Private mode + - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: hCaptcha site key used by forms +- Backend + - ENVIRONMENT: typically PRODUCTION + - CORS_ORIGINS: comma-separated list of allowed origins + - PG_DSN: PostgreSQL connection string + - JWT_SECRET: secret for signing tokens + - ADMIN_EMAIL and ADMIN_PASSWORD: initial admin user + - BASE_URL: public base URL of the app (Private mode: site URL; Public mode: API URL) +- Important + - Variables prefixed with NEXT_PUBLIC_ are embedded at build time in Next.js. Changing them requires rebuilding the frontend image and restarting the container. + - Backend variable changes usually require only a container restart. + +Docker Compose hints +- Private mode + - Publish only the frontend service port. + - Do not publish the backend port; both services must share the same private Docker network. + - Set NEXT_PUBLIC_USE_PRIVATE_API to "true" and INTERNAL_API_BASE_URL to the backend service URL. + - Set backend CORS_ORIGINS to the frontend container origin (for example: http://bracket-frontend:3000). +- Public mode + - Publish both frontend and backend ports. + - Set NEXT_PUBLIC_USE_PRIVATE_API to "false" and NEXT_PUBLIC_API_BASE_URL to the public API URL. + - Set backend CORS_ORIGINS to include the public frontend domain. + +Reverse proxy notes +- Private mode + - Only proxy the frontend (for example: Cloudflare/Nginx → frontend:3000). + - Do not expose the backend externally. +- Public mode + - Proxy both frontend and backend using separate hostnames (for example: yourdomain.com → frontend, api.yourdomain.com → backend). + +Troubleshooting +- Browser shows requests to bracket-backend:8400 or net::ERR_NAME_NOT_RESOLVED + - Cause: the browser is trying to reach the internal Docker hostname directly. + - Fix: use Private mode (NEXT_PUBLIC_USE_PRIVATE_API = "true") so the frontend proxy is used, or set NEXT_PUBLIC_API_BASE_URL to a public URL in Public mode. +- CORS policy errors + - Cause: backend CORS_ORIGINS does not match the actual frontend origin. + - Fix: in Private mode, allow http://bracket-frontend:3000; in Public mode, allow the public frontend domain. +- NEXT_PUBLIC_* changes not reflected + - Cause: these variables are baked into the Next.js build. + - Fix: rebuild the frontend image without cache and restart the container. +- Verifying Private mode in the browser + - In the Network tab, API calls should appear under /api/... on the same origin, not direct calls to the backend hostname. + +Which mode should I use? +- Use Private mode for most deployments: simpler SSL, no CORS, smaller attack surface. +- Use Public mode when the API must be directly reachable by other clients or services. # API Configuration: Public vs Private Modes# API Configuration: Public vs Private Modes# API Configuration: Public vs Private Modes# API Configuration: Public vs Private From 953efb643b980cdbe321d7a11cc01a85b61b83c7 Mon Sep 17 00:00:00 2001 From: Oscar Tobar Rios Date: Sat, 1 Nov 2025 12:32:41 -0500 Subject: [PATCH 13/14] Update doc fast api modes --- API-MODES.md | 1582 +++++--------------------------------------------- 1 file changed, 147 insertions(+), 1435 deletions(-) diff --git a/API-MODES.md b/API-MODES.md index c6571acf9..d60b2e8e2 100644 --- a/API-MODES.md +++ b/API-MODES.md @@ -1,18 +1,18 @@ -API modes: Private vs Public +# API modes: Private vs Public Overview - The application supports two modes of API access. - Private mode (recommended): only the frontend is public; the backend stays private and is reached via the frontend’s internal proxy. - Public mode: both frontend and backend are public; the frontend calls the backend directly. -Private mode (recommended) -- What it is +# Private mode (recommended) +## What it is - The backend is not exposed to the internet. - The frontend exposes an internal proxy under /api that forwards requests to the backend inside the Docker network. - This eliminates CORS issues and reduces the attack surface. -- How it works +## How it works - Browser → Frontend (public) → /api/... → Frontend proxy → Backend (private) → Response → Browser. -- Configure +## Configure - Frontend - NEXT_PUBLIC_USE_PRIVATE_API = "true" (string) - INTERNAL_API_BASE_URL points to the backend service URL on the Docker network (for example: http://bracket-backend:8400) @@ -21,15 +21,81 @@ Private mode (recommended) - Do not publish the backend port; the backend must be reachable only from the Docker network - CORS_ORIGINS must include the frontend container origin (for example: http://bracket-frontend:3000) - BASE_URL is your public site URL (for example: https://yourdomain.com) -- When to use +## When to use - Production behind Cloudflare/Nginx - Single-frontend deployments where the backend should not be directly reachable -Public mode (direct API) -- What it is +## Example Docker-Compose in Private Mode +```bash +networks: + bracket_lan: + driver: bridge + +volumes: + bracket_pg_data: + +services: + bracket-backend: + container_name: bracket-backend + depends_on: + - postgres + environment: + ENVIRONMENT: PRODUCTION + # CORS - Internal communication between frontend and backend + CORS_ORIGINS: http://bracket-frontend:3000 + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod + JWT_SECRET: change_me_in_production + ADMIN_EMAIL: admin@yourdomain.com + ADMIN_PASSWORD: change_me_in_production + BASE_URL: https://youdomain.com + image: ghcr.io/evroon/bracket-backend + networks: + - bracket_lan + # Backend is PRIVATE - Uncomment ports below for PUBLIC API mode + # ports: + # - "8400:8400" + restart: unless-stopped + volumes: + - ./backend/static:/app/static + + bracket-frontend: + container_name: bracket-frontend + environment: + # Private API mode - Uses internal proxy to communicate with backend + NEXT_PUBLIC_USE_PRIVATE_API: "true" + # Backend URL (for proxy reference) + NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 + # Internal URL for proxy (server-side only) + INTERNAL_API_BASE_URL: http://bracket-backend:8400 + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + # Use local image with internal proxy + image: ghcr.io/evroon/bracket-frontend + networks: + - bracket_lan + ports: + - "3000:3000" + restart: unless-stopped + + postgres: + environment: + POSTGRES_DB: bracket_prod + POSTGRES_PASSWORD: bracket_prod + POSTGRES_USER: bracket_prod + image: postgres + networks: + - bracket_lan + restart: always + volumes: + - bracket_pg_data:/var/lib/postgresql + +``` + + +# Public mode (direct API) +## What it is - Both frontend and backend are publicly accessible. - The frontend calls the backend directly using the public API URL. -- Configure +## Configure - Frontend - NEXT_PUBLIC_USE_PRIVATE_API = "false" (string) - NEXT_PUBLIC_API_BASE_URL is the public API URL (for example: https://api.yourdomain.com) @@ -37,10 +103,76 @@ Public mode (direct API) - Publish the backend port (for example: 8400) - CORS_ORIGINS must include the public frontend domain (for example: https://yourdomain.com) - BASE_URL is the public API base URL (for example: https://api.yourdomain.com) -- When to use +## When to use - Multi-client scenarios (mobile + web) or when third parties must call your API directly -Environment variables +## Example Docker-Compose Direct Api +```bash +networks: + bracket_lan: + driver: bridge + +volumes: + bracket_pg_data: + +services: + bracket-backend: + container_name: bracket-backend + depends_on: + - postgres + environment: + # Private API mode - Uses internal proxy to communicate with backend + ENVIRONMENT: PRODUCTION + # CORS - Internal communication between frontend and backend + CORS_ORIGINS: http://bracket-frontend:3000 + PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod + JWT_SECRET: change_me_in_production + ADMIN_EMAIL: admin@yourdomain.com + ADMIN_PASSWORD: change_me_in_production + BASE_URL: https://youdomain.com + image: ghcr.io/evroon/bracket-backend + networks: + - bracket_lan + # Backend for PUBLIC API mode + ports: + - "8400:8400" + restart: unless-stopped + volumes: + - ./backend/static:/app/static + + bracket-frontend: + container_name: bracket-frontend + environment: + # Private API mode - Uses internal proxy to communicate with backend + NEXT_PUBLIC_USE_PRIVATE_API: "false" + # Backend URL (for proxy reference) + NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 + # Internal URL for proxy (server-side only) + INTERNAL_API_BASE_URL: http://bracket-backend:8400 + NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" + # Use local image with internal proxy + image: ghcr.io/evroon/bracket-frontend + networks: + - bracket_lan + ports: + - "3000:3000" + restart: unless-stopped + + postgres: + environment: + POSTGRES_DB: bracket_prod + POSTGRES_PASSWORD: bracket_prod + POSTGRES_USER: bracket_prod + image: postgres + networks: + - bracket_lan + restart: always + volumes: + - bracket_pg_data:/var/lib/postgresql + +``` + +# Environment variables - Frontend - NEXT_PUBLIC_USE_PRIVATE_API: "true" or "false" to select Private or Public mode - NEXT_PUBLIC_API_BASE_URL: public backend URL used by the browser in Public mode @@ -57,7 +189,7 @@ Environment variables - Variables prefixed with NEXT_PUBLIC_ are embedded at build time in Next.js. Changing them requires rebuilding the frontend image and restarting the container. - Backend variable changes usually require only a container restart. -Docker Compose hints +# Docker Compose hints - Private mode - Publish only the frontend service port. - Do not publish the backend port; both services must share the same private Docker network. @@ -68,14 +200,14 @@ Docker Compose hints - Set NEXT_PUBLIC_USE_PRIVATE_API to "false" and NEXT_PUBLIC_API_BASE_URL to the public API URL. - Set backend CORS_ORIGINS to include the public frontend domain. -Reverse proxy notes +# Reverse proxy notes - Private mode - Only proxy the frontend (for example: Cloudflare/Nginx → frontend:3000). - Do not expose the backend externally. - Public mode - Proxy both frontend and backend using separate hostnames (for example: yourdomain.com → frontend, api.yourdomain.com → backend). -Troubleshooting +# Troubleshooting - Browser shows requests to bracket-backend:8400 or net::ERR_NAME_NOT_RESOLVED - Cause: the browser is trying to reach the internal Docker hostname directly. - Fix: use Private mode (NEXT_PUBLIC_USE_PRIVATE_API = "true") so the frontend proxy is used, or set NEXT_PUBLIC_API_BASE_URL to a public URL in Public mode. @@ -88,1428 +220,8 @@ Troubleshooting - Verifying Private mode in the browser - In the Network tab, API calls should appear under /api/... on the same origin, not direct calls to the backend hostname. -Which mode should I use? +# Which mode should I use? - Use Private mode for most deployments: simpler SSL, no CORS, smaller attack surface. - Use Public mode when the API must be directly reachable by other clients or services. -# API Configuration: Public vs Private Modes# API Configuration: Public vs Private Modes# API Configuration: Public vs Private Modes# API Configuration: Public vs Private - - - -This project supports two API operation modes for different deployment scenarios. - - - ----This project supports two API operation modes for different deployment scenarios. - - - -## Private Mode (Recommended for Production) - - - -### Overview---This project supports two API operation modes for different deployment scenarios.This project supports two API operation modes: - - - -In Private Mode, the backend API is NOT exposed to the internet. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues. - - - -### Features## 🔒 Private Mode (Recommended for Production) - - - -- Backend NOT accessible from Internet - Only internal Docker network access - -- Enhanced security - Attack surface reduced - -- Frontend acts as transparent proxy - Server-side forwarding via /api/* routes### Overview---## 🔒 Private Mode (Recommended) - -- No CORS issues - Same-origin requests - -- Single domain - Only frontend needs to be publicly exposed - -- Users NEVER see internal URLs - All requests appear as frontend URLs - -In **Private Mode**, the backend API is **NOT exposed to the internet**. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues. - -### What Users See - - - -Users access: - -- https://yourdomain.com/login### Features## 🔒 Private Mode (Recommended for Production)**Features:** - -- https://yourdomain.com/tournaments - -- All routes are frontend URLs - - - -### What Happens Internally- ✅ **Backend NOT accessible from Internet** - Only internal Docker network access- ✅ Backend NOT accessible from Internet - - - -The flow is transparent to users:- ✅ **Enhanced security** - Attack surface reduced - - - -Browser Request: GET https://yourdomain.com/- ✅ **Frontend acts as transparent proxy** - Server-side forwarding via `/api/*` routes### Overview- ✅ Enhanced security - -Next.js Frontend: Calls /api/clubs internally - -Proxy Handler: /api/clubs → http://bracket-backend:8400/clubs- ✅ **No CORS issues** - Same-origin requests - -Backend Response: Returns data to frontend - -User Receives: Data rendered on page- ✅ **Single domain** - Only frontend needs to be publicly exposedIn **Private Mode**, the backend API is **NOT exposed to the internet**. The frontend acts as a transparent proxy, forwarding all API requests internally to the backend container. This provides enhanced security and eliminates CORS issues.- ✅ Frontend acts as transparent proxy - - - -### Docker Compose Configuration- ✅ **Users NEVER see internal URLs** - All requests appear as frontend URLs - - - -Method 1: Direct Configuration (Simplest)- ✅ No CORS issues - - - -Edit docker-compose.yml:### What Users See - - - -services:### Features- ✅ Centralized logging - - bracket-backend: - - container_name: bracket-backend``` - - environment: - - ENVIRONMENT: PRODUCTION✅ https://yourdomain.com/login- ✅ **Backend NOT accessible from Internet** - Only internal Docker network access- ✅ **Users NEVER see /api in URLs** - - CORS_ORIGINS: http://bracket-frontend:3000 - - PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod✅ https://yourdomain.com/tournaments - - JWT_SECRET: change_me_in_production - - ADMIN_EMAIL: admin@yourdomain.com✅ https://yourdomain.com/ (all routes are frontend URLs)- ✅ **Enhanced security** - Attack surface reduced - - ADMIN_PASSWORD: change_me_in_production - - BASE_URL: https://yourdomain.com``` - - image: ghcr.io/evroon/bracket-backend - - networks:- ✅ **Frontend acts as transparent proxy** - Server-side forwarding via `/api/*` routes**What users see:** - - - bracket_lan - - # Backend is PRIVATE - NO ports exposed### What Happens Internally (Transparent) - - restart: unless-stopped - -- ✅ **No CORS issues** - Same-origin requests- ✅ `https://yourdomain.com/login` - - bracket-frontend: - - container_name: bracket-frontend``` - - environment: - - NEXT_PUBLIC_USE_PRIVATE_API: "true"Browser Request: GET https://yourdomain.com/- ✅ **Single domain** - Only frontend needs to be publicly exposed- ✅ `https://yourdomain.com/tournaments` - - NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 - - INTERNAL_API_BASE_URL: http://bracket-backend:8400 ↓ - - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" - - image: bracket-frontend-localNext.js Frontend: Calls /api/clubs internally- ✅ **Users NEVER see internal URLs** - All requests appear as frontend URLs- ✅ Normal frontend URLs - - networks: - - - bracket_lan ↓ - - ports: - - - "3000:3000"Proxy Handler: /api/clubs → http://bracket-backend:8400/clubs - - restart: unless-stopped - - ↓ - -Method 2: Using Environment Files - -Backend Response: Returns data to frontend### What Users See**What happens internally (invisible):** - -Create .env.private file: - - ↓ - -NEXT_PUBLIC_USE_PRIVATE_API=true - -CORS_ORIGINS=http://bracket-frontend:3000User Receives: Data rendered on page```- 🔄 `yourdomain.com/api/token` → `bracket-backend:8400/token` - -BACKEND_PORT_MAPPING= - -INTERNAL_API_URL=http://bracket-backend:8400``` - -NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400 - -FRONTEND_PORT_MAPPING=3000:3000✅ https://yourdomain.com/login- 🔄 Transparent server-side proxy - -JWT_SECRET=change_me_in_production - -ADMIN_EMAIL=admin@yourdomain.com### Docker Compose Configuration - -ADMIN_PASSWORD=change_me_in_production - -BASE_URL=https://yourdomain.com✅ https://yourdomain.com/tournaments - -NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 - -#### Method 1: Direct Configuration (Simplest) - -Then use variables in docker-compose.yml: - -✅ https://yourdomain.com/ (all routes are frontend URLs)**Configuration:** - -services: - - bracket-backend:Edit `docker-compose.yml`: - - environment: - - CORS_ORIGINS: ${CORS_ORIGINS}``````bash - - JWT_SECRET: ${JWT_SECRET} - - ADMIN_EMAIL: ${ADMIN_EMAIL}```yaml - - ADMIN_PASSWORD: ${ADMIN_PASSWORD} - - BASE_URL: ${BASE_URL}services:./switch-api-mode.sh private - - ports: - - - "${BACKEND_PORT_MAPPING:-}" bracket-backend: - - - - bracket-frontend: container_name: bracket-backend### What Happens Internally (Transparent)docker-compose down && docker-compose up -d - - environment: - - NEXT_PUBLIC_USE_PRIVATE_API: ${NEXT_PUBLIC_USE_PRIVATE_API} environment: - - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} - - INTERNAL_API_BASE_URL: ${INTERNAL_API_URL} ENVIRONMENT: PRODUCTION`````` - - ports: - - - "${FRONTEND_PORT_MAPPING}" # CORS - Only allow internal frontend communication - - - -### Deployment CORS_ORIGINS: http://bracket-frontend:3000Browser Request: GET https://yourdomain.com/ - - - -Using .env.private: PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod - - - -cp .env.private .env JWT_SECRET: change_me_in_production ↓**Nginx (frontend only):** - -docker-compose down - -docker-compose up -d ADMIN_EMAIL: admin@yourdomain.com - - - -Or with direct configuration: ADMIN_PASSWORD: change_me_in_productionNext.js Frontend: Calls /api/clubs internally```nginx - - - -docker-compose down BASE_URL: https://yourdomain.com - -docker-compose up -d - - image: ghcr.io/evroon/bracket-backend ↓server { - -### Nginx Configuration (Frontend Only) - - networks: - -server { - - listen 443 ssl http2; - bracket_lanProxy Handler: /api/clubs → http://bracket-backend:8400/clubs server_name yourdomain.com; - - server_name yourdomain.com; - - # Backend is PRIVATE - NO ports exposed - - ssl_certificate /path/to/cert.pem; - - ssl_certificate_key /path/to/key.pem; restart: unless-stopped ↓ location / { - - - - location / { - - proxy_pass http://localhost:3000; - - proxy_http_version 1.1; bracket-frontend:Backend Response: Returns data to frontend proxy_pass http://172.16.0.4:3000; - - proxy_set_header Upgrade $http_upgrade; - - proxy_set_header Connection 'upgrade'; container_name: bracket-frontend - - proxy_set_header Host $host; - - proxy_cache_bypass $http_upgrade; environment: ↓ } - - proxy_set_header X-Real-IP $remote_addr; - - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # Enable Private API mode - - proxy_set_header X-Forwarded-Proto $scheme; - - } NEXT_PUBLIC_USE_PRIVATE_API: "true"User Receives: Data rendered on page} - -} - - # Internal backend URL (for proxy) - ---- - - NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400`````` - -## Public Mode (Direct API Access) - - INTERNAL_API_BASE_URL: http://bracket-backend:8400 - -### Overview - - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" - -In Public Mode, both frontend and backend are exposed to the internet. The frontend makes direct requests to the backend API. This requires proper CORS configuration and typically uses two subdomains. - - image: bracket-frontend-local # Must use local image with proxy - -### Features - - networks:### Docker Compose Configuration## 🌐 Public Mode - -- Backend accessible from Internet - Requires security considerations - -- Requires CORS configuration - Cross-origin requests must be allowed - bracket_lan - -- Two domains/subdomains needed - Frontend and API separately exposed - -- Potentially lower latency - Direct backend communication ports: - -- API can be used by other clients - Third-party integrations possible - - - "3000:3000" # Only frontend is exposed - -### What Users See - - restart: unless-stopped**Method 1: Direct Configuration (Simplest)****Features:** - -Users access: - -- https://yourdomain.com/login``` - -- Browser requests go to: https://api.yourdomain.com/token - -- Browser requests go to: https://api.yourdomain.com/clubs- ⚠️ Backend accessible from Internet - - - -### Docker Compose Configuration#### Method 2: Using Environment Files - - - -Method 1: Direct ConfigurationEdit `docker-compose.yml`:- ⚠️ Requires correct CORS configuration - - - -Edit docker-compose.yml:Create `.env.private`: - - - -services:- ⚠️ Two subdomains needed - - bracket-backend: - - container_name: bracket-backend```bash - - environment: - - ENVIRONMENT: PRODUCTION# Private API Mode - Backend NOT publicly accessible```yaml- ✅ Potentially lower latency - - CORS_ORIGINS: https://yourdomain.com,http://localhost:3000 - - PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prodNEXT_PUBLIC_USE_PRIVATE_API=true - - JWT_SECRET: change_me_in_production - - ADMIN_EMAIL: admin@yourdomain.comCORS_ORIGINS=http://bracket-frontend:3000services: - - ADMIN_PASSWORD: change_me_in_production - - BASE_URL: https://api.yourdomain.comBACKEND_PORT_MAPPING= - - image: ghcr.io/evroon/bracket-backend - - networks:INTERNAL_API_URL=http://bracket-backend:8400 bracket-backend:**What users see:** - - - bracket_lan - - ports:NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400 - - - "8400:8400" - - restart: unless-stoppedFRONTEND_PORT_MAPPING=3000:3000 container_name: bracket-backend- ✅ `https://yourdomain.com/login` - - - - bracket-frontend:JWT_SECRET=change_me_in_production - - container_name: bracket-frontend - - environment:ADMIN_EMAIL=admin@yourdomain.com environment:- ⚠️ Requests go to `https://api.yourdomain.com/token` - - NEXT_PUBLIC_USE_PRIVATE_API: "false" - - NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.comADMIN_PASSWORD=change_me_in_production - - NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" - - image: ghcr.io/evroon/bracket-frontendBASE_URL=https://yourdomain.com ENVIRONMENT: PRODUCTION - - networks: - - - bracket_lanNEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 - - ports: - - - "3000:3000"``` # CORS - Only allow internal frontend communication**Configuration:** - - restart: unless-stopped - - - -Method 2: Using Environment Files - -Then use variables in `docker-compose.yml`: CORS_ORIGINS: http://bracket-frontend:3000```bash - -Create .env.public file: - - - -NEXT_PUBLIC_USE_PRIVATE_API=false - -CORS_ORIGINS=https://yourdomain.com,http://localhost:3000```yaml PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod./switch-api-mode.sh public - -BACKEND_PORT_MAPPING=8400:8400 - -NEXT_PUBLIC_API_BASE_URL=https://api.yourdomain.comservices: - -INTERNAL_API_URL=http://bracket-backend:8400 - -FRONTEND_PORT_MAPPING=3000:3000 bracket-backend: JWT_SECRET: change_me_in_productiondocker-compose down && docker-compose up -d - -JWT_SECRET=change_me_in_production - -ADMIN_EMAIL=admin@yourdomain.com environment: - -ADMIN_PASSWORD=change_me_in_production - -BASE_URL=https://api.yourdomain.com CORS_ORIGINS: ${CORS_ORIGINS} ADMIN_EMAIL: admin@yourdomain.com``` - -NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 - - JWT_SECRET: ${JWT_SECRET} - -### Deployment - - ADMIN_EMAIL: ${ADMIN_EMAIL} ADMIN_PASSWORD: change_me_in_production - -Using .env.public: - - ADMIN_PASSWORD: ${ADMIN_PASSWORD} - -cp .env.public .env - -docker-compose down BASE_URL: ${BASE_URL} BASE_URL: https://yourdomain.com**Nginx (frontend + backend):** - -docker-compose up -d - - ports: - -### Nginx Configuration (Frontend + Backend) - - - "${BACKEND_PORT_MAPPING:-}" # Empty = not exposed image: ghcr.io/evroon/bracket-backend```nginx - -Frontend: - - - -server { - - listen 443 ssl http2; bracket-frontend: networks:# Frontend - - server_name yourdomain.com; - - environment: - - ssl_certificate /path/to/cert.pem; - - ssl_certificate_key /path/to/key.pem; NEXT_PUBLIC_USE_PRIVATE_API: ${NEXT_PUBLIC_USE_PRIVATE_API} - bracket_lanserver { - - - - location / { NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} - - proxy_pass http://localhost:3000; - - proxy_http_version 1.1; INTERNAL_API_BASE_URL: ${INTERNAL_API_URL} # Backend is PRIVATE - NO ports exposed server_name yourdomain.com; - - proxy_set_header Upgrade $http_upgrade; - - proxy_set_header Connection 'upgrade'; ports: - - proxy_set_header Host $host; - - proxy_cache_bypass $http_upgrade; - "${FRONTEND_PORT_MAPPING}" restart: unless-stopped location / { - - } - -}``` - - - -Backend API: proxy_pass http://172.16.0.4:3000; - - - -server {### Deployment - - listen 443 ssl http2; - - server_name api.yourdomain.com; bracket-frontend: } - - - - ssl_certificate /path/to/cert.pem;```bash - - ssl_certificate_key /path/to/key.pem; - -# Using .env.private container_name: bracket-frontend} - - location / { - - proxy_pass http://localhost:8400;cp .env.private .env - - proxy_http_version 1.1; - - proxy_set_header Host $host;docker-compose down environment: - - proxy_set_header X-Real-IP $remote_addr; - - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;docker-compose up -d - - proxy_set_header X-Forwarded-Proto $scheme; - - } # Enable Private API mode# Backend - -} - -# Or with direct configuration - ---- - -docker-compose down NEXT_PUBLIC_USE_PRIVATE_API: "true"server { - -## Environment Variables Reference - -docker-compose up -d - -### Frontend Environment Variables - -``` # Internal backend URL (for proxy) server_name api.yourdomain.com; - -Variable: NEXT_PUBLIC_USE_PRIVATE_API - -Description: Enables/disables private API mode - -Private Mode: "true" - -Public Mode: "false"### Nginx Configuration (Frontend Only) NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 location / { - -Required: Yes - - - -Variable: NEXT_PUBLIC_API_BASE_URL - -Description: Backend API URL visible to browser```nginx INTERNAL_API_BASE_URL: http://bracket-backend:8400 proxy_pass http://172.16.0.4:8400; - -Private Mode: http://bracket-backend:8400 - -Public Mode: https://api.yourdomain.comserver { - -Required: Yes - - listen 443 ssl http2; NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" } - -Variable: INTERNAL_API_BASE_URL - -Description: Backend URL for server-side proxy server_name yourdomain.com; - -Private Mode: http://bracket-backend:8400 - -Public Mode: http://bracket-backend:8400 image: bracket-frontend-local # Must use local image with proxy} - -Required: Private only - - ssl_certificate /path/to/cert.pem; - -Variable: NEXT_PUBLIC_HCAPTCHA_SITE_KEY - -Description: hCaptcha site key for forms ssl_certificate_key /path/to/key.pem; networks:``` - -Private Mode: 10000000-ffff-ffff-ffff-000000000001 (test key) - -Public Mode: Same - -Required: Yes - - location / { - bracket_lan - -Important: NEXT_PUBLIC_* variables are embedded at build time in Next.js. Changes require rebuilding the Docker image. - - proxy_pass http://localhost:3000; - -### Backend Environment Variables - - proxy_http_version 1.1; ports:## Environment Variables - -Variable: ENVIRONMENT - -Description: Deployment environment proxy_set_header Upgrade $http_upgrade; - -Private Mode: PRODUCTION - -Public Mode: PRODUCTION proxy_set_header Connection 'upgrade'; - "3000:3000" # Only frontend is exposed - -Required: Yes - - proxy_set_header Host $host; - -Variable: CORS_ORIGINS - -Description: Allowed origin domains proxy_cache_bypass $http_upgrade; restart: unless-stopped| Variable | Description | Private | Public | - -Private Mode: http://bracket-frontend:3000 - -Public Mode: https://yourdomain.com proxy_set_header X-Real-IP $remote_addr; - -Required: Yes - - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;```|----------|-------------|---------|---------| - -Variable: PG_DSN - -Description: PostgreSQL connection string proxy_set_header X-Forwarded-Proto $scheme; - -Private Mode: postgresql://user:pass@postgres:5432/db - -Public Mode: Same }| `USE_PRIVATE_API` | Operation mode | `true` | `false` | - -Required: Yes - -} - -Variable: JWT_SECRET - -Description: Secret for JWT token signing```**Method 2: Using Environment Files**| `BACKEND_PORT_MAPPING` | Backend port | empty | `172.16.0.4:8400:8400` | - -Private Mode: Strong random string - -Public Mode: Same - -Required: Yes - ----| `CORS_ORIGINS` | Allowed domains | internal | public | - -Variable: ADMIN_EMAIL - -Description: Initial admin user email - -Private Mode: admin@yourdomain.com - -Public Mode: Same## 🌐 Public Mode (Direct API Access)Create `.env.private`:| `PUBLIC_API_URL` | Public API URL | `/api` | `https://api.yourdomain.com` | - -Required: Yes - - - -Variable: ADMIN_PASSWORD - -Description: Initial admin password### Overview - -Private Mode: Strong password - -Public Mode: Same - -Required: Yes - -In **Public Mode**, both frontend and backend are exposed to the internet. The frontend makes direct requests to the backend API. This requires proper CORS configuration and typically uses two subdomains.```bash## Quick Switch - -Variable: BASE_URL - -Description: Public base URL of application - -Private Mode: https://yourdomain.com - -Public Mode: https://api.yourdomain.com### Features# Private API Mode - Backend NOT publicly accessible - -Required: Yes - - - -### Docker Compose Variables - -- ⚠️ **Backend accessible from Internet** - Requires security considerationsNEXT_PUBLIC_USE_PRIVATE_API=true```bash - -Variable: BACKEND_PORT_MAPPING - -Description: Backend container port mapping- ⚠️ **Requires CORS configuration** - Cross-origin requests must be allowed - -Private Mode: (empty or omit) - -Public Mode: 8400:8400- ⚠️ **Two domains/subdomains needed** - Frontend and API separately exposedCORS_ORIGINS=http://bracket-frontend:3000# Secure mode (recommended) - - - -Variable: FRONTEND_PORT_MAPPING- ✅ **Potentially lower latency** - Direct backend communication - -Description: Frontend container port mapping - -Private Mode: 3000:3000- ✅ **API can be used by other clients** - Third-party integrations possibleBACKEND_PORT_MAPPING=./switch-api-mode.sh private - -Public Mode: 3000:3000 - - - ---- - -### What Users SeeINTERNAL_API_URL=http://bracket-backend:8400 - -## Quick Mode Switching - - - -If using the switch-api-mode.sh script: - -```NEXT_PUBLIC_API_BASE_URL=http://bracket-backend:8400# Public mode - -Switch to Private Mode (secure): - -✅ https://yourdomain.com/login - -./switch-api-mode.sh private - -docker-compose down && docker-compose up -d⚠️ Browser requests go to: https://api.yourdomain.com/tokenFRONTEND_PORT_MAPPING=3000:3000./switch-api-mode.sh public - - - -Switch to Public Mode:⚠️ Browser requests go to: https://api.yourdomain.com/clubs - - - -./switch-api-mode.sh public```JWT_SECRET=change_me_in_production``` - -docker-compose down && docker-compose up -d - - - ---- - -### Docker Compose ConfigurationADMIN_EMAIL=admin@yourdomain.com - -## Troubleshooting - - - -### Error: bracket-backend:8400 net::ERR_NAME_NOT_RESOLVED - -#### Method 1: Direct ConfigurationADMIN_PASSWORD=change_me_in_production## Troubleshooting - -Cause: Frontend is trying to connect directly to internal Docker hostname from browser. - - - -Solutions: - -1. Verify NEXT_PUBLIC_USE_PRIVATE_API="true" in frontend environmentEdit `docker-compose.yml`:BASE_URL=https://yourdomain.com - -2. Ensure frontend image is bracket-frontend-local (has proxy code) - -3. Rebuild frontend image: docker build -t bracket-frontend-local ./frontend - -4. Check that /frontend/src/pages/api/[...path].ts exists in your source code - -```yamlNEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001### Error: `bracket-backend:8400 not resolved` - -### Error: CORS policy: No 'Access-Control-Allow-Origin' header - -services: - -Cause: Backend CORS settings don't match frontend origin. - - bracket-backend:```- Verify that `USE_PRIVATE_API=true` in `.env` - -Solutions: - -1. Private Mode: Set CORS_ORIGINS=http://bracket-frontend:3000 in backend container_name: bracket-backend - -2. Public Mode: Set CORS_ORIGINS=https://yourdomain.com in backend (match frontend domain) - -3. Restart backend: docker-compose restart bracket-backend environment:- Make sure `/pages/api/[...path].ts` file exists - - - -### Backend not responding ENVIRONMENT: PRODUCTION - - - -Private Mode: # CORS - Allow requests from frontend domainThen use variables in `docker-compose.yml`: - -- Check frontend logs: docker-compose logs bracket-frontend - -- Verify proxy is working: docker exec bracket-frontend curl http://bracket-backend:8400 CORS_ORIGINS: https://yourdomain.com,http://localhost:3000 - -- Ensure backend has no ports: section in docker-compose.yml - - PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod### Error: `CORS policy` - -Public Mode: - -- Check backend logs: docker-compose logs bracket-backend JWT_SECRET: change_me_in_production - -- Verify backend port is exposed: docker-compose ps should show 8400:8400 - -- Test direct access: curl http://localhost:8400 ADMIN_EMAIL: admin@yourdomain.com```yaml- Public mode: verify `CORS_ORIGINS` in backend - -- Check Nginx proxy configuration - - ADMIN_PASSWORD: change_me_in_production - -### Frontend shows blank page or errors - - BASE_URL: https://api.yourdomain.comservices:- Private mode: should not occur - -1. Check browser console (F12) for errors - -2. Verify environment variables: docker exec bracket-frontend env | grep NEXT_PUBLIC image: ghcr.io/evroon/bracket-backend - -3. Ensure correct image is running: docker inspect bracket-frontend | grep Image - -4. Clear browser cache (Ctrl+Shift+R) or try incognito mode networks: bracket-backend: - - - -### Changes to environment variables not taking effect - bracket_lan - - - -For NEXT_PUBLIC_* variables: # Backend is PUBLIC - Port exposed environment:### Backend not responding - -- These are embedded at build time in Next.js - -- Must rebuild frontend image: docker build --no-cache -t bracket-frontend-local ./frontend ports: - -- Then restart: docker-compose up -d - - - "8400:8400" CORS_ORIGINS: ${CORS_ORIGINS}- Private mode: check frontend logs - -For backend variables: - -- Simply restart: docker-compose restart bracket-backend restart: unless-stopped - - - ---- JWT_SECRET: ${JWT_SECRET}- Public mode: verify nginx points to `172.16.0.4:8400` - - - -## Summary: Which Mode Should I Use? bracket-frontend: - - - -Scenario: Production deployment with Cloudflare/Nginx container_name: bracket-frontend ADMIN_EMAIL: ${ADMIN_EMAIL} - -Recommended Mode: Private - -Reason: Enhanced security, simpler SSL setup environment: - - - -Scenario: Development/testing # Disable Private API mode ADMIN_PASSWORD: ${ADMIN_PASSWORD}## Variables de Entorno - -Recommended Mode: Private - -Reason: Easier setup, no CORS issues NEXT_PUBLIC_USE_PRIVATE_API: "false" - - - -Scenario: Need third-party API access # Public backend URL BASE_URL: ${BASE_URL} - -Recommended Mode: Public - -Reason: Backend must be directly accessible NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.com - - - -Scenario: Multi-client architecture (mobile app + web) NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" ports:| Variable | Descripción | Privado | Público | - -Recommended Mode: Public - -Reason: Shared API endpoint image: ghcr.io/evroon/bracket-frontend # Can use official image - - - -Scenario: Simple single-app deployment networks: - "${BACKEND_PORT_MAPPING:-}" # Empty = not exposed|----------|-------------|---------|---------| - -Recommended Mode: Private - -Reason: Reduced attack surface - bracket_lan - - - -Default recommendation: Use Private Mode for most deployments. ports:| `USE_PRIVATE_API` | Modo de operación | `true` | `false` | - - - - "3000:3000" - - restart: unless-stopped bracket-frontend:| `BACKEND_PORT_MAPPING` | Puerto del backend | vacío | `172.16.0.4:8400:8400` | - -``` - - environment:| `CORS_ORIGINS` | Dominios permitidos | interno | público | - -#### Method 2: Using Environment Files - - NEXT_PUBLIC_USE_PRIVATE_API: ${NEXT_PUBLIC_USE_PRIVATE_API}| `PUBLIC_API_URL` | URL pública del API | `/api` | `https://api.pinar.campeonatos.co` | - -Create `.env.public`: - - NEXT_PUBLIC_API_BASE_URL: ${NEXT_PUBLIC_API_BASE_URL} - -```bash - -# Public API Mode - Backend IS publicly accessible INTERNAL_API_BASE_URL: ${INTERNAL_API_URL}## Cambio Rápido - -NEXT_PUBLIC_USE_PRIVATE_API=false - -CORS_ORIGINS=https://yourdomain.com,http://localhost:3000 ports: - -BACKEND_PORT_MAPPING=8400:8400 - -NEXT_PUBLIC_API_BASE_URL=https://api.yourdomain.com - "${FRONTEND_PORT_MAPPING}"```bash - -INTERNAL_API_URL=http://bracket-backend:8400 - -FRONTEND_PORT_MAPPING=3000:3000```# Modo seguro (recomendado) - -JWT_SECRET=change_me_in_production - -ADMIN_EMAIL=admin@yourdomain.com./switch-api-mode.sh private - -ADMIN_PASSWORD=change_me_in_production - -BASE_URL=https://api.yourdomain.com### Deployment - -NEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 - -```# Modo público - - - -### Deployment```bash./switch-api-mode.sh public - - - -```bash# Using .env.private``` - -# Using .env.public - -cp .env.public .envcp .env.private .env - -docker-compose down - -docker-compose up -ddocker-compose down## Troubleshooting - -``` - -docker-compose up -d - -### Nginx Configuration (Frontend + Backend) - -### Error: `bracket-backend:8400 not resolved` - -```nginx - -# Frontend# Or with direct configuration- Verifica que `USE_PRIVATE_API=true` en `.env` - -server { - - listen 443 ssl http2;docker-compose down- Asegúrate de que el archivo `/pages/api/[...path].ts` existe - - server_name yourdomain.com; - -docker-compose up -d - - ssl_certificate /path/to/cert.pem; - - ssl_certificate_key /path/to/key.pem;```### Error: `CORS policy` - - - - location / {- En modo público: verifica `CORS_ORIGINS` en backend - - proxy_pass http://localhost:3000; - - proxy_http_version 1.1;### Nginx Configuration (Frontend Only)- En modo privado: no debería ocurrir - - proxy_set_header Upgrade $http_upgrade; - - proxy_set_header Connection 'upgrade'; - - proxy_set_header Host $host; - - proxy_cache_bypass $http_upgrade;```nginx### Backend no responde - - } - -}server {- Modo privado: verifica logs del frontend - - - -# Backend API listen 443 ssl http2;- Modo público: verifica que nginx apunte a `172.16.0.4:8400` - -server { server_name yourdomain.com; - - listen 443 ssl http2; - - server_name api.yourdomain.com; ssl_certificate /path/to/cert.pem; - - ssl_certificate_key /path/to/key.pem; - - ssl_certificate /path/to/cert.pem; - - ssl_certificate_key /path/to/key.pem; location / { - - proxy_pass http://localhost:3000; - - location / { proxy_http_version 1.1; - - proxy_pass http://localhost:8400; proxy_set_header Upgrade $http_upgrade; - - proxy_http_version 1.1; proxy_set_header Connection 'upgrade'; - - proxy_set_header Host $host; proxy_set_header Host $host; - - proxy_set_header X-Real-IP $remote_addr; proxy_cache_bypass $http_upgrade; - - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; - - proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - - } proxy_set_header X-Forwarded-Proto $scheme; - -} } - -```} - -``` - ---- - ---- - -## Environment Variables Reference - -## 🌐 Public Mode (Direct API Access) - -### Frontend Environment Variables - -### Overview - -| Variable | Description | Private Mode | Public Mode | Required |In **Public Mode**, both frontend and backend are exposed to the internet. The frontend makes direct requests to the backend API. This requires proper CORS configuration and typically uses two subdomains. - -|----------|-------------|--------------|-------------|----------| - -| `NEXT_PUBLIC_USE_PRIVATE_API` | Enables/disables private API mode | `"true"` | `"false"` | ✅ Yes |### Features - -| `NEXT_PUBLIC_API_BASE_URL` | Backend API URL visible to browser | `http://bracket-backend:8400` | `https://api.yourdomain.com` | ✅ Yes |- ⚠️ **Backend accessible from Internet** - Requires security considerations - -| `INTERNAL_API_BASE_URL` | Backend URL for server-side proxy | `http://bracket-backend:8400` | `http://bracket-backend:8400` | Private only |- ⚠️ **Requires CORS configuration** - Cross-origin requests must be allowed - -| `NEXT_PUBLIC_HCAPTCHA_SITE_KEY` | hCaptcha site key for forms | `10000000-ffff-ffff-ffff-000000000001` (test key) | Same | ✅ Yes |- ⚠️ **Two domains/subdomains needed** - Frontend and API separately exposed - -- ✅ **Potentially lower latency** - Direct backend communication - -**Important:** `NEXT_PUBLIC_*` variables are **embedded at build time** in Next.js. Changes require rebuilding the Docker image.- ✅ **API can be used by other clients** - Third-party integrations possible - - - -### Backend Environment Variables### What Users See - -``` - -| Variable | Description | Private Mode | Public Mode | Required |✅ https://yourdomain.com/login - -|----------|-------------|--------------|-------------|----------|⚠️ Browser requests go to: https://api.yourdomain.com/token - -| `ENVIRONMENT` | Deployment environment | `PRODUCTION` | `PRODUCTION` | ✅ Yes |⚠️ Browser requests go to: https://api.yourdomain.com/clubs - -| `CORS_ORIGINS` | Allowed origin domains | `http://bracket-frontend:3000` | `https://yourdomain.com` | ✅ Yes |``` - -| `PG_DSN` | PostgreSQL connection string | `postgresql://user:pass@postgres:5432/db` | Same | ✅ Yes | - -| `JWT_SECRET` | Secret for JWT token signing | Strong random string | Same | ✅ Yes |### Docker Compose Configuration - -| `ADMIN_EMAIL` | Initial admin user email | `admin@yourdomain.com` | Same | ✅ Yes | - -| `ADMIN_PASSWORD` | Initial admin password | Strong password | Same | ✅ Yes |**Method 1: Direct Configuration** - -| `BASE_URL` | Public base URL of application | `https://yourdomain.com` | `https://api.yourdomain.com` | ✅ Yes | - -Edit `docker-compose.yml`: - -### Docker Compose Variables - -```yaml - -| Variable | Description | Private Mode | Public Mode |services: - -|----------|-------------|--------------|-------------| bracket-backend: - -| `BACKEND_PORT_MAPPING` | Backend container port mapping | (empty or omit) | `8400:8400` | container_name: bracket-backend - -| `FRONTEND_PORT_MAPPING` | Frontend container port mapping | `3000:3000` | `3000:3000` | environment: - - ENVIRONMENT: PRODUCTION - ---- # CORS - Allow requests from frontend domain - - CORS_ORIGINS: https://yourdomain.com,http://localhost:3000 - -## Quick Mode Switching PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod - - JWT_SECRET: change_me_in_production - -If using the `switch-api-mode.sh` script: ADMIN_EMAIL: admin@yourdomain.com - - ADMIN_PASSWORD: change_me_in_production - -```bash BASE_URL: https://api.yourdomain.com - -# Switch to Private Mode (secure) image: ghcr.io/evroon/bracket-backend - -./switch-api-mode.sh private networks: - -docker-compose down && docker-compose up -d - bracket_lan - - # Backend is PUBLIC - Port exposed - -# Switch to Public Mode ports: - -./switch-api-mode.sh public - "8400:8400" - -docker-compose down && docker-compose up -d restart: unless-stopped - -``` - - bracket-frontend: - ---- container_name: bracket-frontend - - environment: - -## Troubleshooting # Disable Private API mode - - NEXT_PUBLIC_USE_PRIVATE_API: "false" - -### Error: `bracket-backend:8400 net::ERR_NAME_NOT_RESOLVED` # Public backend URL - - NEXT_PUBLIC_API_BASE_URL: https://api.yourdomain.com - -**Cause:** Frontend is trying to connect directly to internal Docker hostname from browser. NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" - - image: ghcr.io/evroon/bracket-frontend # Can use official image - -**Solutions:** networks: - - - bracket_lan - -1. Verify `NEXT_PUBLIC_USE_PRIVATE_API="true"` in frontend environment ports: - -2. Ensure frontend image is `bracket-frontend-local` (has proxy code) - "3000:3000" - -3. Rebuild frontend image: `docker build -t bracket-frontend-local ./frontend` restart: unless-stopped - -4. Check that `/frontend/src/pages/api/[...path].ts` exists in your source code``` - - - -### Error: `CORS policy: No 'Access-Control-Allow-Origin' header`**Method 2: Using Environment Files** - - - -**Cause:** Backend CORS settings don't match frontend origin.Create `.env.public`: - - - -**Solutions:**```bash - -# Public API Mode - Backend IS publicly accessible - -1. **Private Mode:** Set `CORS_ORIGINS=http://bracket-frontend:3000` in backendNEXT_PUBLIC_USE_PRIVATE_API=false - -2. **Public Mode:** Set `CORS_ORIGINS=https://yourdomain.com` in backend (match frontend domain)CORS_ORIGINS=https://yourdomain.com,http://localhost:3000 - -3. Restart backend: `docker-compose restart bracket-backend`BACKEND_PORT_MAPPING=8400:8400 - -NEXT_PUBLIC_API_BASE_URL=https://api.yourdomain.com - -### Backend not respondingINTERNAL_API_URL=http://bracket-backend:8400 - -FRONTEND_PORT_MAPPING=3000:3000 - -**Private Mode:**JWT_SECRET=change_me_in_production - -ADMIN_EMAIL=admin@yourdomain.com - -- Check frontend logs: `docker-compose logs bracket-frontend`ADMIN_PASSWORD=change_me_in_production - -- Verify proxy is working: `docker exec bracket-frontend curl http://bracket-backend:8400`BASE_URL=https://api.yourdomain.com - -- Ensure backend has no `ports:` section in docker-compose.ymlNEXT_PUBLIC_HCAPTCHA_SITE_KEY=10000000-ffff-ffff-ffff-000000000001 - -``` - -**Public Mode:** - -### Deployment - -- Check backend logs: `docker-compose logs bracket-backend` - -- Verify backend port is exposed: `docker-compose ps` should show `8400:8400````bash - -- Test direct access: `curl http://localhost:8400`# Using .env.public - -- Check Nginx proxy configurationcp .env.public .env - -docker-compose down - -### Frontend shows blank page or errorsdocker-compose up -d - -``` - -1. Check browser console (F12) for errors - -2. Verify environment variables: `docker exec bracket-frontend env | grep NEXT_PUBLIC`### Nginx Configuration (Frontend + Backend) - -3. Ensure correct image is running: `docker inspect bracket-frontend | grep Image` - -4. Clear browser cache (Ctrl+Shift+R) or try incognito mode```nginx - -# Frontend - -### Changes to environment variables not taking effectserver { - - listen 443 ssl http2; - -**For `NEXT_PUBLIC_*` variables:** server_name yourdomain.com; - - - -- These are **embedded at build time** in Next.js ssl_certificate /path/to/cert.pem; - -- Must rebuild frontend image: `docker build --no-cache -t bracket-frontend-local ./frontend` ssl_certificate_key /path/to/key.pem; - -- Then restart: `docker-compose up -d` - - location / { - -**For backend variables:** proxy_pass http://localhost:3000; - - proxy_http_version 1.1; - -- Simply restart: `docker-compose restart bracket-backend` proxy_set_header Upgrade $http_upgrade; - - proxy_set_header Connection 'upgrade'; - ---- proxy_set_header Host $host; - - proxy_cache_bypass $http_upgrade; - -## Summary: Which Mode Should I Use? } - -} - -| Scenario | Recommended Mode | Reason | - -|----------|------------------|--------|# Backend API - -| Production deployment with Cloudflare/Nginx | **Private** | Enhanced security, simpler SSL setup |server { - -| Development/testing | **Private** | Easier setup, no CORS issues | listen 443 ssl http2; - -| Need third-party API access | **Public** | Backend must be directly accessible | server_name api.yourdomain.com; - -| Multi-client architecture (mobile app + web) | **Public** | Shared API endpoint | - -| Simple single-app deployment | **Private** | Reduced attack surface | ssl_certificate /path/to/cert.pem; - - ssl_certificate_key /path/to/key.pem; - -**Default recommendation:** Use **Private Mode** for most deployments. - - location / { - proxy_pass http://localhost:8400; - proxy_http_version 1.1; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} -``` - ---- - -## Environment Variables Reference - -### Frontend Environment Variables - -| Variable | Description | Private Mode | Public Mode | Required | -|----------|-------------|--------------|-------------|----------| -| `NEXT_PUBLIC_USE_PRIVATE_API` | Enables/disables private API mode | `"true"` | `"false"` | ✅ Yes | -| `NEXT_PUBLIC_API_BASE_URL` | Backend API URL visible to browser | `http://bracket-backend:8400` | `https://api.yourdomain.com` | ✅ Yes | -| `INTERNAL_API_BASE_URL` | Backend URL for server-side proxy | `http://bracket-backend:8400` | `http://bracket-backend:8400` | Private only | -| `NEXT_PUBLIC_HCAPTCHA_SITE_KEY` | hCaptcha site key for forms | `10000000-ffff-ffff-ffff-000000000001` (test key) | Same | ✅ Yes | - -**Important:** `NEXT_PUBLIC_*` variables are **embedded at build time** in Next.js. Changes require rebuilding the Docker image. - -### Backend Environment Variables - -| Variable | Description | Private Mode | Public Mode | Required | -|----------|-------------|--------------|-------------|----------| -| `ENVIRONMENT` | Deployment environment | `PRODUCTION` | `PRODUCTION` | ✅ Yes | -| `CORS_ORIGINS` | Allowed origin domains | `http://bracket-frontend:3000` | `https://yourdomain.com` | ✅ Yes | -| `PG_DSN` | PostgreSQL connection string | `postgresql://user:pass@postgres:5432/db` | Same | ✅ Yes | -| `JWT_SECRET` | Secret for JWT token signing | Strong random string | Same | ✅ Yes | -| `ADMIN_EMAIL` | Initial admin user email | `admin@yourdomain.com` | Same | ✅ Yes | -| `ADMIN_PASSWORD` | Initial admin password | Strong password | Same | ✅ Yes | -| `BASE_URL` | Public base URL of application | `https://yourdomain.com` | `https://api.yourdomain.com` | ✅ Yes | - -### Docker Compose Variables - -| Variable | Description | Private Mode | Public Mode | -|----------|-------------|--------------|-------------| -| `BACKEND_PORT_MAPPING` | Backend container port mapping | (empty or omit) | `8400:8400` | -| `FRONTEND_PORT_MAPPING` | Frontend container port mapping | `3000:3000` | `3000:3000` | - ---- - -## Quick Mode Switching - -If using the `switch-api-mode.sh` script: - -```bash -# Switch to Private Mode (secure) -./switch-api-mode.sh private -docker-compose down && docker-compose up -d - -# Switch to Public Mode -./switch-api-mode.sh public -docker-compose down && docker-compose up -d -``` - ---- - -## Troubleshooting - -### Error: `bracket-backend:8400 net::ERR_NAME_NOT_RESOLVED` - -**Cause:** Frontend is trying to connect directly to internal Docker hostname from browser. - -**Solutions:** -1. Verify `NEXT_PUBLIC_USE_PRIVATE_API="true"` in frontend environment -2. Ensure frontend image is `bracket-frontend-local` (has proxy code) -3. Rebuild frontend image: `docker build -t bracket-frontend-local ./frontend` -4. Check that `/frontend/src/pages/api/[...path].ts` exists in your source code - -### Error: `CORS policy: No 'Access-Control-Allow-Origin' header` - -**Cause:** Backend CORS settings don't match frontend origin. - -**Solutions:** -1. **Private Mode:** Set `CORS_ORIGINS=http://bracket-frontend:3000` in backend -2. **Public Mode:** Set `CORS_ORIGINS=https://yourdomain.com` in backend (match frontend domain) -3. Restart backend: `docker-compose restart bracket-backend` - -### Backend not responding - -**Private Mode:** -- Check frontend logs: `docker-compose logs bracket-frontend` -- Verify proxy is working: `docker exec bracket-frontend curl http://bracket-backend:8400` -- Ensure backend has no `ports:` section in docker-compose.yml - -**Public Mode:** -- Check backend logs: `docker-compose logs bracket-backend` -- Verify backend port is exposed: `docker-compose ps` should show `8400:8400` -- Test direct access: `curl http://localhost:8400` -- Check Nginx proxy configuration - -### Frontend shows blank page or errors - -1. Check browser console (F12) for errors -2. Verify environment variables: `docker exec bracket-frontend env | grep NEXT_PUBLIC` -3. Ensure correct image is running: `docker inspect bracket-frontend | grep Image` -4. Clear browser cache (Ctrl+Shift+R) or try incognito mode - -### Changes to environment variables not taking effect - -**For `NEXT_PUBLIC_*` variables:** -- These are **embedded at build time** in Next.js -- Must rebuild frontend image: `docker build --no-cache -t bracket-frontend-local ./frontend` -- Then restart: `docker-compose up -d` - -**For backend variables:** -- Simply restart: `docker-compose restart bracket-backend` - ---- - -## Summary: Which Mode Should I Use? - -| Scenario | Recommended Mode | Reason | -|----------|------------------|--------| -| Production deployment with Cloudflare/Nginx | **Private** | Enhanced security, simpler SSL setup | -| Development/testing | **Private** | Easier setup, no CORS issues | -| Need third-party API access | **Public** | Backend must be directly accessible | -| Multi-client architecture (mobile app + web) | **Public** | Shared API endpoint | -| Simple single-app deployment | **Private** | Reduced attack surface | **Default recommendation:** Use **Private Mode** for most deployments. From 9f1b786a042e0ab85be0e5c788c1f5933845e6a0 Mon Sep 17 00:00:00 2001 From: Oscar Tobar Rios Date: Sat, 1 Nov 2025 12:59:55 -0500 Subject: [PATCH 14/14] doc api-modes add examples --- API-MODES.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/API-MODES.md b/API-MODES.md index d60b2e8e2..4ebe7fe92 100644 --- a/API-MODES.md +++ b/API-MODES.md @@ -124,7 +124,7 @@ services: # Private API mode - Uses internal proxy to communicate with backend ENVIRONMENT: PRODUCTION # CORS - Internal communication between frontend and backend - CORS_ORIGINS: http://bracket-frontend:3000 + CORS_ORIGINS: https://youdomain.com PG_DSN: postgresql://bracket_prod:bracket_prod@postgres:5432/bracket_prod JWT_SECRET: change_me_in_production ADMIN_EMAIL: admin@yourdomain.com @@ -146,9 +146,7 @@ services: # Private API mode - Uses internal proxy to communicate with backend NEXT_PUBLIC_USE_PRIVATE_API: "false" # Backend URL (for proxy reference) - NEXT_PUBLIC_API_BASE_URL: http://bracket-backend:8400 - # Internal URL for proxy (server-side only) - INTERNAL_API_BASE_URL: http://bracket-backend:8400 + NEXT_PUBLIC_API_BASE_URL: https://api.youdomain.com NEXT_PUBLIC_HCAPTCHA_SITE_KEY: "10000000-ffff-ffff-ffff-000000000001" # Use local image with internal proxy image: ghcr.io/evroon/bracket-frontend