Production-oriented platform for asynchronous real-estate scraping with API, worker, UI, MySQL, and Redis.
Real Estate SaaS Core is an on-premise scraping platform designed for collecting, storing, and reviewing rental listings from multiple sources.
Current runtime includes:
apionFastAPIscraperas a background async workerstreamlit_uifor internal operatorsmysqlfor persistent storageredisfor worker state and operational flags
The project is designed to run primarily through Docker Compose and includes:
- structured logging
- health checks
- background scraping
- MySQL schema bootstrap
- Streamlit authentication
- integration hooks for webhooks / CRM delivery
- Async multi-site scraper built on
httpx,asyncio,BeautifulSoup - REST API for health checks, leads, agencies, and manual scrape triggering
- Streamlit dashboard for operators
- MySQL persistence with typed repository layer
- Redis-backed worker status and run metadata
- Dockerized deployment with health checks and restart policies
- Optional Sentry integration
- Optional webhook, AmoCRM, and Bitrix24 integrations
+-------------------+
| Streamlit UI |
| :8501 |
+---------+---------+
|
v
+-------------------+
| FastAPI API |
| :8000 |
+----+----------+---+
| |
v v
+-----------+ +--------+
| MySQL 8 | | Redis |
+-----------+ +--------+
^
|
+----+----+
| Scraper |
| Worker |
+---------+
- Python
3.11 - FastAPI
- Streamlit
- MySQL
8 - Redis
7 - Docker Compose
- Poetry for local dependency management
app/
api/main.py FastAPI entrypoint and routes
core/config.py typed application settings
core/logging.py structured logging and optional Sentry setup
db/mysql.py MySQL pool and schema initialization
models/schemas.py API response models
services/async_scraper.py async scraping engine
services/repository.py database access layer
scraper_worker.py long-running worker loop
ui/streamlit_app.py operator dashboard
ui/users.yaml Streamlit auth users
ui/generate_password_hashes.py
integrations/
amocrm.py optional AmoCRM integration
bitrix24.py optional Bitrix24 integration
webhooks.py optional webhook delivery
Dockerfile
docker-compose.yml
.env.example
Makefile
requirements.txt
pyproject.toml
utils.py
Built-in source configuration is defined in app/core/config.py and can be overridden via SCRAPER_SITES.
Default built-in sources:
imoti.bgalo.bgdom.ria.comolx.ualun.ua
For controlled production rollouts it is recommended to limit active sources explicitly through .env, for example:
SCRAPER_SITES=["imoti.bg","alo.bg"]- Docker
24+ - Docker Compose
v2+
- Python
3.11.x - Poetry
1.8+ - MySQL
8 - Redis
7
cp .env.example .envUpdate at minimum:
MYSQL_PASSWORDMYSQL_ROOT_PASSWORDSTREAMLIT_COOKIE_KEYSTREAMLIT_JWT_SECRET
Recommended:
- set
APP_ENV=prod - keep
LOG_FORMAT=json - enable
SENTRY_DSNif you use Sentry - restrict
SCRAPER_SITESto the sources you actually want to run
Generate a password hash:
python -m app.ui.generate_password_hashes "your-strong-password"Put the resulting hash into app/ui/users.yaml.
Example:
credentials:
usernames:
admin:
email: "ops@example.com"
name: "Operations"
password: "$2b$12$replace_with_generated_hash"docker compose up -d --buildOr via Makefile:
make up-buildcurl http://localhost:8000/health
curl http://localhost:8501/_stcore/health
docker compose ps- UI:
http://localhost:8501 - API docs:
http://localhost:8000/docs - API health:
http://localhost:8000/health
curl -X POST http://localhost:8000/trigger-scrapedocker compose logs -f scraperdocker compose exec redis redis-cli MGET \
scrape:worker_status \
scrape:last_status \
scrape:last_total_scraped \
scrape:last_writtendocker compose exec mysql sh -lc '
mysql -uroot -p"$MYSQL_ROOT_PASSWORD" "$MYSQL_DATABASE" -e "
SELECT source_site, COUNT(*) AS total
FROM listings
GROUP BY source_site
ORDER BY total DESC;
"'Main configuration lives in .env.example.
APP_NAMEapplication nameAPP_ENVenvironment name, usuallyprodordevAPP_DEBUGdebug mode
MYSQL_HOSTMYSQL_PORTMYSQL_USERMYSQL_PASSWORDMYSQL_DATABASEMYSQL_ROOT_PASSWORD
REDIS_HOSTREDIS_PORTREDIS_DB
SCRAPE_TIMEOUT_SECONDSSCRAPE_INTERVAL_SECONDSSCRAPE_CONCURRENCYSCRAPE_RETRY_COUNTSCRAPE_BACKOFF_BASE_SECONDSSCRAPE_BACKOFF_CAP_SECONDSSCRAPE_DELAY_MIN_SECONDSSCRAPE_DELAY_MAX_SECONDSSCRAPE_FOLLOW_REDIRECTSSCRAPE_VERIFY_SSLSCRAPE_DETAIL_PAGESHTTP_MAX_CONNECTIONSHTTP_MAX_KEEPALIVE_CONNECTIONSCITY_FILTERSCRAPER_SITES
PROXY_ENABLEDPROXY_LISTPROXY_ROTATION_STRATEGYPROXY_MAX_RETRIES
LOG_LEVELLOG_FORMATLOG_TO_FILELOG_DIRSENTRY_DSNSENTRY_ENVIRONMENTSENTRY_TRACES_SAMPLE_RATE
STREAMLIT_COOKIE_NAMESTREAMLIT_COOKIE_KEYSTREAMLIT_COOKIE_EXPIRY_DAYSSTREAMLIT_JWT_SECRETSTREAMLIT_USERS_YAML_PATH
WEBHOOKS_*AMOCRM_*BITRIX24_*
These variables are still present for backward compatibility and migration, but they are not part of the main api + scraper + streamlit runtime path:
GOOGLE_SHEET_IDSERVICE_ACCOUNT_JSONSHEET_NAMEEMAIL_*SMTP_*MAX_PAGESREQUEST_DELAY_*LOG_FILEAGENCIES_CSV_PATHMYSQL_ENABLEDTELEGRAM_*
- Stores scraped data
- Initializes schema on app startup
- Uses named Docker volume
mysql_data
- Stores worker runtime state
- Used by health checks and operational visibility
- Uses named Docker volume
redis_data
- Exposes REST endpoints
- Can trigger background scrape jobs
- Bootstraps schema on startup
- Runs scheduled scraping loop
- Writes run status into Redis
- Writes listings to MySQL
- Displays recent leads and statistics
- Requires valid user definitions in
app/ui/users.yaml
Returns service health summary.
Returns recent leads.
Returns recent agencies if present in storage.
Queues a background scrape task in the API process.
Install dependencies:
poetry install --with devRun API:
poetry run uvicorn app.api.main:app --reload --host 0.0.0.0 --port 8000Run worker:
poetry run python -m app.scraper_workerRun UI:
poetry run streamlit run app/ui/streamlit_app.pyStatic checks:
make lint
make format
make type-checkmake help
make up
make up-build
make down
make restart
make logs
make logs-api
make logs-scraper
make logs-ui
make psBackup:
make db-backupRestore:
make db-restore FILE=backups/backup_YYYYMMDD_HHMMSS.sqlReset:
make db-resetAPI health:
make api-healthTrigger scrape:
make trigger-scrapeContainer shells:
make shell-api
make shell-scraper
make shell-mysql
make shell-redisBy default the platform uses structured logging.
Recommended production setup:
LOG_FORMAT=jsonLOG_LEVEL=INFOLOG_TO_FILE=falseunless you explicitly need file logs- configure
SENTRY_DSNfor error aggregation
Useful runtime signals:
- API health via
/health - Streamlit health via
/_stcore/health - Redis keys:
scrape:worker_statusscrape:last_statusscrape:last_total_scrapedscrape:last_writtenscrape:last_error
- Replace every placeholder secret in
.env - Use a dedicated database user instead of
root - Restrict externally exposed ports
- Put the stack behind reverse proxy / VPN if needed
- Enable backups for MySQL volume
- Set up Sentry or central log shipping
- Restrict
SCRAPER_SITESto validated sources - Verify one manual scrape before enabling long-running worker mode
docker compose up -d --build- persistent Docker volumes for MySQL and Redis
- external monitoring for container health
- periodic DB backups
- CI pipeline for lint, type-check, and image build
- Some sites expose masked or bot-protected contact data.
alo.bgmay hide phone numbers behind anti-bot flow; in such cases the system should keepphoneempty instead of storing fake data.- Source markup changes can break extraction logic, so each enabled source should be validated after deployment.
Check:
docker compose ps
docker compose logs --tail=100 api
docker compose logs --tail=100 streamlit_uiCheck:
docker compose logs -f scraper
docker compose exec redis redis-cli MGET scrape:last_status scrape:last_total_scraped scrape:last_writtenIf HTTP 200 exists but last_total_scraped=0, the source likely changed markup and needs parser updates.
Make sure .env values match the initialized volume.
If credentials changed after first startup, either:
- create the app user manually in existing MySQL
- or recreate volumes for a clean bootstrap
Another local MySQL instance is already bound to the host.
Options:
- stop local MySQL
- or change published port in
docker-compose.yml
docker compose down -v
docker compose up -d --build- Never commit
.env - Never keep real service-account JSON in the repository
- Rotate compromised secrets immediately
- Use hashed passwords only in
app/ui/users.yaml - Prefer non-root DB access for runtime services
- Keep Docker images and base packages updated
See LICENSE.