Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .env.private
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions .env.public
Original file line number Diff line number Diff line change
@@ -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
225 changes: 225 additions & 0 deletions API-MODES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
# 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

## 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
- 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

## 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: 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
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: 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
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
- 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.

**Default recommendation:** Use **Private Mode** for most deployments.
3 changes: 2 additions & 1 deletion backend/alembic.ini
Original file line number Diff line number Diff line change
@@ -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]
Expand Down
3 changes: 2 additions & 1 deletion backend/alembic/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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 ###
33 changes: 31 additions & 2 deletions backend/bracket/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,39 @@
@asynccontextmanager
async def lifespan(_: FastAPI) -> AsyncIterator[None]:
await database.connect()

# 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

# 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:
alembic_run_migrations()
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()
Expand Down
Loading
Loading