Overview
Over the course of one push (early July 2026), I embarked on a project to build out a small cluster of self-hosted customer/utility tools for breadtoasting.com. The goal was twofold: provide my game-server customers with real self-service and equip the site itself with useful public tools.
Background
I had an existing internal AMP (game server control panel) API client that I extended to create a FastAPI service for the customer portal. This tool would allow customers to log in, see the status of their assigned server instances, and manage them without needing to contact me directly. Additionally, I built a meme maker, a PDF toolkit, an IT-Tools subdomain, and a LAN-only file converter named ConvertX.
How It Works
Customer Portal (breadtoasting.com/portal)
Service Setup:
docker run -d --name customer-portal \ -e DB_URL="sqlite:///./customer_portal.db" \ -p 8000:8000 \ -v /path/to/customer_data:/app/data \ my_customer_portal_imageUser Authentication:
from passlib.context import CryptContext pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") def verify_password(plain_password, hashed_password): return pwd_context.verify(plain_password, hashed_password) def get_password_hash(password): return pwd_context.hash(password)Rate Limiting:
from fastapi import Depends, FastAPI, HTTPException, status from fastapi.security import OAuth2PasswordBearer app = FastAPI() oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") @app.post("/token") async def login(...): # Rate limiting logic here ...Admin API for Assignment:
from fastapi import Depends, HTTPException, status @app.post("/assign_instance") async def assign_instance(customer_id: int, instance_id: int, token: str = Depends(oauth2_scheme)): # Admin-level assignment logic here ...
Meme Maker (breadtoasting.com/meme-maker.html)
- Client-Side Implementation:
<canvas id="memeCanvas" width="800" height="600"></canvas> <script> const canvas = document.getElementById('memeCanvas'); const ctx = canvas.getContext('2d'); function drawText(text, x, y) { ctx.font = '30px Arial'; ctx.fillText(text, x, y); } // Event listeners and drawing logic here </script>
PDF Toolkit (breadtoasting.com/pdf/)
Tool Setup:
docker run -d --name pdf-toolkit \ -v /path/to/pdfs:/app/data \ -p 8080:8080 \ my_pdf_toolkit_imageEnvironment Variable Gotcha:
# Before upgrade, environment variable was set to "DISABLE_LOGIN=1" # After upgrading to a newer version, the variable name changed to "NO_AUTH=1" # This change caused all requests to return 401s until it was caught and fixed.
IT-Tools (tools.breadtoasting.com)
- Service Setup:
docker run -d --name it-tools \ -p 8090:8090 \ -v /path/to/it_tools_data:/app/data \ my_it_tools_image
ConvertX (LAN-only)
Service Setup:
docker run -d --name convertx \ -p 5000:5000 \ -v /path/to/convertx_data:/app/data \ my_convertx_imageRestrictions:
# ConvertX is deliberately kept off the public internet and off the Cloudflare tunnel. # It's only reachable from inside the home network due to its CPU-heavy nature for server-side media conversion.
Results
The customer portal was shipped quietly first, verified against a couple of real instances before being pointed at customers. The meme maker received a v2 pass based on actual use and user feedback. The PDF toolkit and IT-Tools subdomain were self-hosted open-source tools that covered the required use cases efficiently.
Lessons Learned
- Prefer Self-Hosting: Prefer self-hosting existing solid open-source tools over building custom ones from scratch when the use case is generic enough.
- Rate Limiting and Security: Implement basic per-IP rate limiting on signups and logins to prevent abuse, but ensure that customer actions are server-side with admin credentials.
- Client-Side Controls: Hide or gate any control that can silently destroy work when there's no undo available, especially in mobile-first designs.
TODO:
- Fill in the exact version numbers for tools used.
- Add benchmarks and performance metrics where applicable.
- Document why certain decisions were made (e.g., why ConvertX is LAN-only).