Backoffice Plan
PopChoice backoffice work is tracked under
#493. The backoffice is an
operational app for catalog health, TMDB match review, and later manual data
repair. It must not be implemented inside the user-facing apps/web app.
Boundary Decision
Use a dedicated workspace app:
apps/backoffice/The deployment model matches apps/bull-board:
- build and publish a separate
ghcr.io/shchilkin/popchoice/backofficecontainer image; - run it as a separate
backofficeservice incoolify.compose.yml; - expose container port
3000; - assign a private/admin Coolify domain; shared operator auth can stay optional until the full role-based backoffice model is ready;
- pin it with the same
IMAGE_TAGrelease bundle asweb,workers,bull-board, anddocs.
The backoffice should be treated as an operator surface, not a product route. It can use the same database and Redis network as the app stack, but it should own its own UI, API boundaries, process command, health check, and deployment configuration.
Initial Scope
The first backoffice release should be read-only:
- #549: catalog-health
overview for missing metadata, duplicate identities, stale TMDB data, and
missing cast/director/genre/keyword coverage. This is implemented as the
first read-only
apps/backofficescreen. - #550: TMDB match review
queue for
tmdb_match_reviewsrows.
Mutation flows belong later:
- #551: review actions and audit history for applying, rejecting, or deferring catalog fixes.
Shared operator auth is the login model for public exposure:
- #548: shared login
protection for
apps/backofficeandapps/bull-board. OPERATOR_AUTH_USERNAMEandOPERATOR_AUTH_PASSWORDprotect Bull Board now and should be reused by the future backoffice app.- User-facing app login stays separate; operator credentials must not be added
to normal
apps/webroutes.
Code Sharing
Use shared boundaries deliberately:
apps/backofficeowns operator pages, forms, tables, route handlers, and UI state.packages/sharedis the preferred home for cross-app database/query helpers once both backoffice and services need the same behavior.services/movie-backfillkeeps the CLI entrypoint forcatalog:health, but shared catalog-health query logic now lives inpackages/sharedso the browser UI and CLI use the same SQL semantics.apps/webremains user-facing and should not import or host admin review UI.apps/bull-boardremains the queue monitoring app; shared auth should wrap it instead of merging it with backoffice.
Local Development
Use workspace scripts that mirror the other apps:
npm run dev:backoffice
npm run build:backoffice
npm run start --workspace=apps/backofficeRun npm run copy:env after editing root .env; it copies values into
apps/backoffice/.env for the local dev script. The app needs at least:
DATABASE_URLfor catalog-health and TMDB review data;REDIS_URLonly when a screen needs queue state or job actions;OPERATOR_AUTH_USERNAMEandOPERATOR_AUTH_PASSWORDwhen testing protected operator routes locally;CATALOG_HEALTH_SAMPLE_LIMITandCATALOG_HEALTH_STALE_DAYSwhen tuning the report shape.
Production Deployment
coolify.compose.yml includes a backoffice service beside bull-board:
backoffice:
image: ${APP_IMAGE_PREFIX:-ghcr.io/shchilkin/popchoice}/backoffice:${IMAGE_TAG:-development}
pull_policy: always
restart: unless-stopped
environment:
NODE_ENV: production
DATABASE_URL: postgresql://${POSTGRES_USER:-popchoice}:${POSTGRES_PASSWORD}@${SERVICE_NAME_DB:-db}:5432/${POSTGRES_DB:-popchoice}
REDIS_URL: redis://${SERVICE_NAME_REDIS:-redis}:6379
PORT: 3000
OPERATOR_AUTH_REQUIRED: ${OPERATOR_AUTH_REQUIRED:-0}
OPERATOR_AUTH_USERNAME: ${OPERATOR_AUTH_USERNAME:-}
OPERATOR_AUTH_PASSWORD: ${OPERATOR_AUTH_PASSWORD:-}
OPERATOR_AUTH_REALM: ${OPERATOR_AUTH_REALM:-PopChoice Operators}
OPERATOR_AUTH_RATE_LIMIT_MAX: ${OPERATOR_AUTH_RATE_LIMIT_MAX:-30}
OPERATOR_AUTH_RATE_LIMIT_WINDOW_SECONDS: ${OPERATOR_AUTH_RATE_LIMIT_WINDOW_SECONDS:-900}
CATALOG_HEALTH_SAMPLE_LIMIT: ${CATALOG_HEALTH_SAMPLE_LIMIT:-5}
CATALOG_HEALTH_STALE_DAYS: ${CATALOG_HEALTH_STALE_DAYS:-180}
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
expose:
- '3000'In Coolify, assign a private/admin domain to backoffice with port 3000,
just like bull-board. During the pet-project phase, operator auth can be
optional; set OPERATOR_AUTH_USERNAME and OPERATOR_AUTH_PASSWORD when you want
the shared login prompt, or set OPERATOR_AUTH_REQUIRED=1 for fail-closed
behavior. The shared Bull Board/backoffice rate limiter counts unsuccessful
requests only and can be tuned with OPERATOR_AUTH_RATE_LIMIT_MAX and
OPERATOR_AUTH_RATE_LIMIT_WINDOW_SECONDS.
Preview Policy
Do not expose backoffice on every PR preview by default. If a PR needs manual backoffice testing, add a temporary preview domain intentionally and remove it after review. This keeps admin surfaces quieter and avoids certificate churn from operational tools.