Backend for GoupixDex: Pokémon TCG scanning, pricing (PokéWallet), inventory articles, JWT auth, public access requests + admin moderation, Vinted publishing & wardrobe sync (nodriver, single or batch), and eBay France listing (per-user OAuth + Inventory API — see EBAY.md).
- FastAPI + Uvicorn — REST API, OpenAPI/Swagger at
/docs - SQLAlchemy 2 ORM — models under
models/(not Pydantic) - MariaDB / MySQL — via PyMySQL (
mysql+pymysql://…) - JWT (python-jose) + bcrypt + Fernet (symmetric encryption for Vinted credentials and eBay tokens, key derived from
JWT_SECRET) - Pydantic — request/response validation only (
schemas/) - Custom SQL migrations in
migrations/(no Alembic) - Docker Compose — API (hot reload), MariaDB, phpMyAdmin
api/
main.py # FastAPI app (routers + middleware)
desktop_vinted_server.py # Local HTTP worker (127.0.0.1:18766) for Tauri desktop:
# Vinted publish + wardrobe sync via nodriver/Chromium
config.py # AppSettings + legacy Vinted CLI defaults
cli_vinted_listings.py # Standalone nodriver CLI (legacy batch from items.json)
app_types/ # TypedDict (PokéWallet, Groq, Vinted, eBay payloads)
core/
database.py # Engine, SessionLocal, get_db
security.py # JWT + password hashing + Fernet helpers
deps.py # get_current_user / get_current_admin (Bearer or query token)
win32_asyncio.py # Force ProactorEventLoop on Windows (nodriver subprocesses)
models/ # SQLAlchemy ORM (User, Article, Image, MarginSettings)
schemas/ # Pydantic request/response bodies only
routes/
auth.py # /auth/login (blocks pending/rejected/banned)
users.py # /users (admin) + /users/me + /users/me/vinted
access_requests.py # /access-requests (public + admin) + /password-setup/{token}
articles.py # CRUD + /vinted-batch + /ebay-batch + SSE streams
settings_route.py # /settings (margin %, vinted_enabled, ebay_enabled, …)
ebay_route.py # /ebay/oauth/* + /ebay/onboarding/setup + /ebay/status
pricing_route.py # PokéWallet preview / suggestions
stats_route.py # /stats/dashboard (CA, marges, split Vinted/eBay)
scan.py # /scan-card (multipart, OCR + pricing preview, no DB)
services/
ocr_service.py # Groq vision wrapper
pricing_service.py # PokéWallet + EUR/USD average
scan_service.py # Title/description templates
article_service.py
auth_service.py
access_request_service.py # submit / approve / reject / ban / setup token
vinted_publish_service.py
vinted_batch_*.py # Grouped Vinted publishing (one browser, sequential)
vinted_progress_session_service.py
vinted_chromium_profile_cookie_service.py
desktop_vinted_runner_service.py
desktop_wardrobe_sync_runner_service.py
vinted_wardrobe/ # Wardrobe sync (catalog + sold + descriptions)
goupix_vinted_wardrobe_sync_service.py
vinted_catalog_service.py
vinted_sold_items_service.py
vinted_http_service.py
wardrobe_job_store_service.py
ebay_oauth_service.py # Authorization URL + code exchange
ebay_publish_service.py # Inventory item + offer + publish
ebay_seller_metadata_service.py
ebay_onboarding_service.py # FR location + business policies
ebay_background_service.py
combined_marketplace_service.py
user_settings_service.py
supabase_storage_service.py
stats_service.py
poke_wallet_*.py # PokéWallet client + reference prices
groq_vision_service.py
migrations/ # 001…005 (run via run_migrations.py)
seeders/ # user_seeder, margin_seeder, article_seeder, run_all
- Python 3.10+
- MariaDB or MySQL (local or Docker)
- Optional:
POKE_WALLET_API_KEY,GROQ_API_KEY, Chromium for Vinted (see below),EBAY_CLIENT_ID/EBAY_CLIENT_SECRET/EBAY_REDIRECT_URIfor eBay
Vinted publishing launches Chromium through nodriver. On a headless machine (VPS, container, CI), VINTED_BROWSER_HEADLESS=false requires a DISPLAY: this repository uses Xvfb so no manual display setup is needed.
| Environment | Behavior |
|---|---|
Docker Compose (api/) |
The image installs chromium, xvfb, and xauth; the service command wraps uvicorn with xvfb-run. No manual step required. |
GitHub → VPS deploy (.github/workflows/deploy-api.yml) |
apt install includes xvfb and xauth; the systemd unit runs Uvicorn under xvfb-run. |
| Linux without Docker (venv + custom systemd) | Install xvfb and xauth (apt install xvfb xauth), then run the API with: xvfb-run -a --server-args="-screen 0 1920x1080x24" uvicorn … when VINTED_BROWSER_HEADLESS=false. |
| Windows / macOS desktop (Tauri) | Vinted publishing & wardrobe sync go through desktop_vinted_server.py on 127.0.0.1:18766. No Xvfb needed — the user's local Chromium is used. |
GitHub Actions (optional secret): VINTED_BROWSER_HEADLESS — if missing or empty, the value generated on the VPS remains true (historical behavior). Set false for a headed Chromium session on Xvfb (often less likely to be blocked by Vinted than built-in headless mode). Cloudflare / hosting IPs can still block requests: it is possible to get a 403 from curl on the VPS while a browser returns 200.
Useful variables in .env / .env.example: VINTED_BROWSER_HEADLESS, VINTED_CHROME_EXECUTABLE (often /usr/bin/chromium on Linux), VINTED_BROWSER_DISCREET*.
Windows / macOS (desktop app): with VINTED_BROWSER_HEADLESS=false, VINTED_BROWSER_DISCREET=true (default) keeps --start-maximized, then applies --window-position (off-screen) + --start-minimized to stay discreet while keeping a headed rendering path. Disable minimization with VINTED_BROWSER_DISCREET_MINIMIZE=false. On Linux + full-screen Xvfb, set VINTED_BROWSER_DISCREET=false to keep standard behavior.
cd api
python -m venv .venv
.venv\Scripts\activate
pip install -r requirements.txtCopy .env.example to .env and set at least DATABASE_URL and JWT_SECRET.
python migrations/run_migrations.pyMigrations applied in order:
| File | Purpose |
|---|---|
001_initial.sql |
users, articles, images, settings (margin) |
002_article_vinted_published.sql |
articles.vinted_published_at |
003_marketplaces_ebay.sql |
Vinted/eBay toggles, eBay tokens & policies |
004_article_sale_price.sql |
articles.sell_price + sold_at |
005_access_requests.sql |
users.is_admin, status, request_message, password_setup_token, password nullable |
-
User — set
SEED_USER_EMAILandSEED_USER_PASSWORDin.env. The seeder always marks this account as admin + approved (idempotent on re-run):python seeders/user_seeder.py
-
Margin (20% default) — creates a
settingsrow for any user that does not have one yet:python seeders/margin_seeder.py
Override default percent with
SEED_MARGIN_PERCENT(e.g.25). -
Fake articles — requires an existing user. Uses
SEED_ARTICLE_USER_EMAILor falls back toSEED_USER_EMAIL. Deletes previous[DEV] …titles for that user, then inserts four sample articles (one marked sold):python seeders/article_seeder.py
-
Everything in order (same env vars as above):
python seeders/run_all.py
uvicorn main:app --reload --host 0.0.0.0 --port 8000On Windows, if you use --reload with Vinted publishing (nodriver / Chrome), uvicorn can pick an event loop without subprocess support → NotImplementedError in nodriver. Use instead:
python run_dev.pyor explicitly (Windows only — stdlib ProactorEventLoop class):
uvicorn main:app --reload --host 0.0.0.0 --port 8000 --loop asyncio.windows_events:ProactorEventLoop- Swagger UI: http://127.0.0.1:8000/docs
- Health:
GET /health - Article images are stored in Supabase Storage (public URLs in DB).
The Tauri desktop bundle launches a second FastAPI process on the user's machine to keep nodriver / Chromium off the production VPS:
python desktop_vinted_server.py| Variable | Default | Role |
|---|---|---|
GOUPIX_VINTED_LOCAL_PORT |
18766 |
Port listening on 127.0.0.1 (Tauri dev overrides to 18767 by default to avoid conflicts with installed desktop app) |
GOUPIX_REMOTE_API |
(none) | Remote API URL (used when the client does not send X-Goupix-Remote-Api) |
Installed desktop worker — Vinted (Chrome) env files: on startup the sidecar loads dotenv files in order (later files override earlier keys):
worker_bundled.env— embedded in the PyInstaller binary (defaults in-repo; the Desktop build & release workflow may overwrite this file before PyInstaller from optional secretsVINTED_DESKTOP_BROWSER_*)..envin the install directory next togoupix-vinted-worker(same folder as the sidecar executable).%LOCALAPPDATA%\GoupixDex\.envon Windows (and the equivalent user-data paths on macOS/Linux)..envin the process current working directory (typical in dev when runningpython desktop_vinted_server.pyfromapi/).
Optional GitHub secrets for the Desktop workflow (same variable names as in worker_bundled.env): VINTED_DESKTOP_BROWSER_HEADLESS, VINTED_DESKTOP_BROWSER_DISCREET, VINTED_DESKTOP_BROWSER_DISCREET_MINIMIZE, VINTED_DESKTOP_BROWSER_DISCREET_X, VINTED_DESKTOP_BROWSER_DISCREET_Y. If unset, the CI step writes defaults false / 0 matching api/worker_bundled.env.
It exposes job-style endpoints used by the Nuxt frontend (useWardrobeLocalSync, useVintedPublishStream, useVintedBatchStream, useWardrobeSyncStream):
POST /vinted/wardrobe-sync/jobs+GET /vinted/wardrobe-sync/jobs/{id}(poll)GET /vinted/wardrobe-sync/jobs/{id}/stream(SSE log stream)POST /vinted/publish/jobs+ SSE stream for grouped/single Vinted publishes- Vinted listing image proxy (Vinted CDN hosts only)
| Method | Path | Notes |
|---|---|---|
| POST | /auth/login |
JWT Bearer; rejects pending, rejected, banned |
| POST | /access-requests |
Public — submit a /request (creates a pending user) |
| POST | /access-requests/{id}/approve | /reject | /ban |
Admin only |
| POST | /access-requests/{id}/password-link |
Admin only — issues a one-time setup token |
| GET / POST | /password-setup/{token} |
Public — fetch info / set password (token consumed) |
| GET | /users |
Admin only — list with margin %, Vinted/eBay flags, status |
| POST / DELETE | /users, /users/{id} |
Admin only for create/delete |
| GET / PUT | /users/me, /users/{id} |
Self for own row |
| PUT | /users/me/vinted |
Self-service: link/update encrypted Vinted credentials |
| GET | /users/me/vinted-decrypted |
Plaintext Vinted creds for the local desktop worker |
| GET / PUT | /settings |
Margin % (default 20), vinted_enabled, ebay_enabled, eBay listing config |
| POST | /scan-card |
Multipart image + optional margin_percent; OCR + pricing preview (no DB) |
| CRUD | /articles |
Create uses multipart (fields + images files); auto Vinted/eBay attempt |
| PATCH | /articles/{id}/sold |
Mark sold + sell_price |
| POST | /articles/vinted-batch + SSE |
Grouped Vinted publishing (single browser, sequential) |
| POST | /articles/ebay-batch |
Sequential eBay Inventory publishing |
| GET | /ebay/oauth/authorize-url |
Build the consent URL (per-user OAuth state) |
| POST | /ebay/oauth/exchange | /disconnect |
Store / clear encrypted tokens on the user row |
| POST | /ebay/onboarding/setup |
Create FR inventory location + business policies |
| POST | /ebay/policies/fulfillment/ensure |
Create/update « GoupixDex — Envoi » shipping policy |
| GET | /ebay/status | /ebay/seller-setup |
Connection state and metadata |
| GET | /stats/dashboard |
KPIs, revenue timeline, channel split |
Vinted credentials: on POST /articles, publication uses the authenticated user's vinted_email and encrypted vinted_password (Fernet, key derived from JWT_SECRET). Legacy rows still holding a bcrypt hash cannot be decrypted — re-save the Vinted password from Settings → Marketplace (VintedAccountCard), the seeder, or PUT /users/me/vinted. As a last-resort fallback, set VINTED_EMAIL_OR_USERNAME / VINTED_PASSWORD in the environment.
eBay credentials: see EBAY.md. Refresh and access tokens are stored encrypted on the user row; ensure_ebay_access_token refreshes silently when expired.
- A visitor submits
/request(email + optional message) → backend creates a user withstatus='pending', no password. - The seeded admin (
SEED_USER_EMAIL) sees the request in/users. - Admin can approve / reject / ban the user, or generate a one-time password setup link that the user opens at
/setup-password/{token}. - Once consumed, the token is invalidated and the user can sign in with their new password.
python cli_vinted_listings.pyUses config.py listing defaults and items.json / images/ at the project root.
From api/:
docker compose up --build- API: port 8000 (migrations + optional seed at startup, then uvicorn under
xvfb-runfor Vinted) - MariaDB: 3306
- phpMyAdmin: 8080
- The image includes Chromium, Xvfb, and xauth;
VINTED_*,EBAY_*, and Supabase variables can be defined in a.envfile next todocker-compose.yml(${…}substitution — see Compose env files).
Create the admin user after startup: docker compose exec api python seeders/user_seeder.py (with env vars set), or use POST /access-requests and let the seeded admin approve and issue a password link.
The Deploy API workflow installs xvfb and xauth, writes the systemd unit with ExecStart=/usr/bin/xvfb-run … uvicorn, and can set VINTED_BROWSER_HEADLESS via the secret of the same name (default true when the secret is missing). No manual Xvfb installation is required on the VPS after deploying through this CI.
- Vinted UI and anti-bot policies may break automation; production VPS deployments often hit Cloudflare / IP rate limits — the desktop worker (Tauri) is the recommended path for Vinted.
- Wardrobe sync requires a valid Vinted session cookie (captured via the local nodriver run, or
VINTED_COOKIEenv var as fallback). - Average price mixes Cardmarket (EUR) and TCGPlayer (USD) using
USD_TO_EURinconfig(AppSettings.usd_to_eur). - eBay sandbox requires test buyer/seller accounts and category/policy IDs valid for the chosen marketplace (default
EBAY_FR).