# lazyflat Combined deployment of **flat-alert** (reliable scraper) and **flat-apply** (experimental auto‑applier) behind a single authenticated web UI. ## Architecture Three isolated containers on one internal Docker network: ``` ┌─────────┐ POST /internal/flats ┌─────────┐ POST /apply ┌─────────┐ │ alert │ ────────────────────────► │ web │ ────────────────► │ apply │ │ scraper │ │ UI+DB │ │ browser │ └─────────┘ └─────────┘ └─────────┘ ▲ │ HTTPS (Coolify / Traefik) │ user (auth) ``` * **`alert/`** — scrapes inberlinwohnen.de (unchanged logic) and posts each discovered flat to `web`. Zero external dependencies on `apply`, so `apply` crashes can never bring the alerter down. * **`apply/`** — FastAPI wrapper around the experimental Playwright applier. Only accepts requests with a shared `X-Internal-Api-Key`. Not exposed publicly. * **`web/`** — FastAPI + Jinja + HTMX dashboard. The only public service. Owns the SQLite database, auth, and orchestration. ## Safety / isolation Because `apply/` is still experimental, the system is hardened around it: | Control | Behavior | |-----------------------|-----------------------------------------------------------------------| | Separate container | A crashed `apply` does not take `alert` or `web` with it. | | Internal-only network | `apply` is not reachable from the internet; requires internal API key.| | Default mode `manual` | New flats are just shown in the UI; apply runs **only** on click. | | Circuit breaker | N consecutive apply failures auto-disable further apply calls. | | Kill switch | One-click button in the UI that blocks all apply activity. | | `SUBMIT_FORMS=False` | Default for `apply` — runs the full flow **without** final submit. | | Audit log | Every auth event, mode change, and apply is recorded. | ## Web security * Argon2id password hashes (`argon2-cffi`), constant-time compare. * Session cookie: signed with `itsdangerous`, `HttpOnly`, `Secure`, `SameSite=Strict`. * CSRF: synchronizer token bound to the session on every state-changing form. * Login rate limit (in-memory, per-IP). * Strict `Content-Security-Policy`, `X-Frame-Options: DENY`, `noindex`. * `/internal/*` endpoints gated by a shared `INTERNAL_API_KEY` and never exposed by Coolify. ## Deployment on Coolify 1. **Create repo**: push this monorepo to `ssh://git@git.moritz.run:2222/moritz/lazyflat.git`. 2. **New Coolify resource** → *Docker Compose* → point it at this repo. Coolify will read `docker-compose.yml` and deploy all three services on one network. 3. **Domain**: set `flat.lab.moritz.run` on the `web` service only. Coolify (Traefik) handles TLS. Do **not** set a domain on `alert` or `apply`. 4. **Secrets**: paste the environment variables from `.env.example` into Coolify's env UI. At minimum you need: * `AUTH_USERNAME`, `AUTH_PASSWORD_HASH` * `SESSION_SECRET`, `INTERNAL_API_KEY` * `GMAPS_API_KEY`, `BERLIN_WOHNEN_USERNAME`, `BERLIN_WOHNEN_PASSWORD` * personal info + WBS for `apply` 5. **Generate the password hash**: ```bash python -c "from argon2 import PasswordHasher; print(PasswordHasher().hash(''))" ``` 6. **Generate secrets**: ```bash python -c "import secrets; print(secrets.token_urlsafe(48))" # SESSION_SECRET python -c "import secrets; print(secrets.token_urlsafe(48))" # INTERNAL_API_KEY ``` 7. First apply launch should stay in **manual** mode with `SUBMIT_FORMS=False` until each provider is verified end-to-end. ## Local development ```bash cp .env.example .env # fill in AUTH_PASSWORD_HASH, SESSION_SECRET, INTERNAL_API_KEY, creds docker compose up -d --build # open http://localhost:8000 (set COOKIE_SECURE=false for plain http!) ``` To also expose the web port locally, add `ports: ["8000:8000"]` under `web:` in `docker-compose.yml` (the Coolify production compose doesn't publish host ports).