docker/Dockerfile used 3.14-slim and deploy/Dockerfile used 3.11-slim.
Unify on 3.12 — the version the CI matrix tests against and the local
venv runs — so the image matches what's actually validated. Update the
docs that referenced 3.11 to match.
Cloud Run was 500-ing every async route on the eventlet worker:
RuntimeError: asyncio.run() cannot be called from a running event loop
RuntimeError: You cannot use AsyncToSync in the same thread as an
async event loop - just await the async function directly.
Root cause: standard Flask decorators (@login_required from Flask-Login,
@limiter.limit from Flask-Limiter, and Flask's own async-view wrapper)
call ``flask.current_app.ensure_sync(obj)(*a, **k)``, which uses
``asgiref.AsyncToSync``, which calls ``asyncio.run()``. Under eventlet's
greenlet hub ``asyncio.run()`` raises because the hub *is* the running
loop. Every ``async def`` route wrapped by any of those decorators (i.e.
all of /api/moderate, /auth/*, /dashboard/*) dies at the decorator
boundary before the view body ever executes. Sentry AUTOMOD-3B/3C/3D.
The dev server didn't show this because werkzeug gives each request a
fresh OS thread with no existing event loop, so asgiref can spin up its
own loop fine.
gthread worker + SOCKETIO_ASYNC_MODE=threading:
- Real OS threads for each request → asgiref's AsyncToSync works.
- Flask-SocketIO threading mode supports WebSockets for our
dashboard-scale realtime traffic.
- Dev (run.py) and prod now use the same async mode, so behaviour
matches end-to-end.
- 50 threads/worker = plenty of concurrency for HTTP + long-lived
WebSocket connections.
Dropped eventlet and psycogreen from requirements.txt — they were pinned
specifically for the eventlet worker, neither is used any more.
Security / correctness
- Privilege escalation: rule/api-key/project-update routes now gate on
can_manage_members instead of is_member; plain members could previously
rewrite or disable moderation rules and delete API keys.
- Project.is_owner() AttributeError on non-admin API-user deletion (route
+ template) replaced with user_id comparison.
- Flask-Limiter on /auth/login, /auth/register, /auth/change-password,
and /api/moderate (per API key id).
- Remove silent SECRET_KEY fallback: docker-compose fails hard if unset;
production config validator refuses to boot without SECRET_KEY and
OPENAI_API_KEY.
- Require verified email in Google OAuth auto-link flow.
- User-supplied regex now evaluated via the regex package with a 1s
timeout, closing the ReDoS vector.
- Cap batched AI chunk count (max 40) and concurrent workers (200 -> 10).
- openai>=2.32 (from 1.55.3 so reasoning_effort kwarg works); add regex,
Flask-Limiter, gunicorn, eventlet, psycogreen; drop the stdlib-shadowing
pypi asyncio package.
Production runtime
- New wsgi.py: eventlet.monkey_patch + psycogreen.patch_psycopg so DB
queries cooperate with the event loop; sets SOCKETIO_ASYNC_MODE=eventlet
for app startup.
- Dockerfiles run as non-root app user and CMD gunicorn with
--worker-class eventlet. run.py refuses FLASK_CONFIG=production so a
misconfigured deploy fails loudly.
- cloudbuild.yaml stops managing env vars/secrets on deploy — it was
about to clobber secret-backed env vars with plain ones. Runtime config
is now owned by the Cloud Run service.
SocketIO async-mode fix (was causing full site lockup during API calls)
- async_mode picked via SOCKETIO_ASYNC_MODE env var, default "threading".
The previous sys.modules auto-detect false-positived because
flask_socketio / python-engineio import eventlet on load whenever it is
installed, pinning a non-monkey-patched eventlet server in front of
blocking stdlib I/O.
Real-time dashboard updates
- New content_received WebSocket event fires when the API accepts a
submission; UI renders a "Processing…" row immediately and updates it
in place when moderation_update arrives.
- Fix stale updated_at timestamp in moderation_update payloads.
Batched AI rule evaluation
- Replace the per-rule parallel fan-out (N OpenAI calls) with a single
batched call evaluating all AI-prompt rules at once. On a 6-rule
default project this drops safe-content cost from 5-12s to ~1-4s.
- response_format=json_object + max_completion_tokens=4000 so the model
emits strict JSON and stops at the close-brace.
- OPENAI_REASONING_EFFORT config: minimal/low/medium/high for gpt-5 /
o-series; accepts "none" on gpt-5.4. Works around the gpt-5-nano case
where reasoning tokens ate the full output budget and returned empty
responses.
Client errors
- Malformed / non-UTF-8 JSON bodies now return 400 with a clear message
about UTF-8 encoding instead of a generic 500.
Logging
- Single INFO line per moderation: the existing
"Content <id>: decision in Zs" summary.
- Per-stage timing, websocket scheduling, and batched-AI call details
moved to DEBUG so they are available when diagnosing latency without
cluttering steady-state logs.
Introduces Docker configuration files including Dockerfile, docker-compose.yml, environment template, and documentation to enable containerized deployment of the AutoModerate application with PostgreSQL. This setup streamlines local development and production deployment using Docker.