STR Optimization Manager — Claude Code Requirements Specification
1. Project Overview
A self-hosted, Dockerized web application for managing and optimizing a short-term rental (STR) property listed on multiple platforms (initially Airbnb and VRBO). The system automates daily performance data collection via browser automation, stores historical metrics in a local database, enables bulk pricing management across platforms, tracks pricing experiments (A/B style), and syncs reservation data for long-term record keeping.
Design philosophy: Modular by platform and by property. Every platform integration is an isolated adapter. Adding a new platform or a second property should require only a new adapter and config entry, not architectural changes.
2. Goals & Non-Goals
Goals
- Automated daily (and on-demand) scraping of performance metrics from Airbnb and VRBO
- Local time-series database of all collected metrics
- Dashboard with filterable, date-ranged charts for performance analysis
- Bulk pricing management across platforms (with preview/diff before commit)
- Pricing change log with experiment tagging and correlation to booking outcomes
- Full reservation sync and local storage
- Weekly performance summary email report
- Docker Compose deployment, runs on Mac (dev) and Debian (prod)
- Responsive UI: desktop and mobile
Non-Goals (explicitly out of scope for v1)
- Multi-property support (architecture should allow it later, but not built now)
- Cleaning/turnover scheduling
- Guest messaging
- Expense tracking / P&L
- Tax reporting
- Public platform APIs (all data collection is via authenticated browser sessions)
3. Architecture Overview
Services
| Service |
Image |
Purpose |
frontend |
Node (build) → nginx |
React SPA served via nginx |
api |
Node 20 Alpine |
Fastify REST API + cron scheduler |
scraper |
Node 20 + Playwright |
Browser automation workers |
db |
PostgreSQL 16 Alpine |
Primary data store |
4. Tech Stack
| Layer |
Technology |
| Backend API |
Node.js 20 + TypeScript + Fastify |
| Browser Automation |
Playwright (headless Chromium) |
| Database |
PostgreSQL 16 |
| ORM / Migrations |
Drizzle ORM + drizzle-kit |
| Job Scheduling |
node-cron |
| Frontend |
React 18 + TypeScript + Vite |
| Charting |
Recharts |
| UI Components |
shadcn/ui + Tailwind CSS |
| Email |
Nodemailer (SMTP) |
| Auth (app login) |
Simple single-user session auth with bcrypt + JWT stored in httpOnly cookie |
| Container |
Docker + Docker Compose v2 |
| Secrets |
.env file (never committed), credentials encrypted at rest in DB using AES-256 |
5. Platform Adapter Interface
Every platform integration implements this TypeScript interface. This is the core abstraction that keeps the system extensible.
Initial Adapters
AirbnbAdapter — targets Airbnb Host dashboard
VrboAdapter — targets VRBO Owner dashboard
Adapter File Structure
Critical pattern: All CSS selectors and XPaths live in *.selectors.ts files only. When a platform updates their UI, only that file needs updating — never the business logic.
6. Database Schema
platforms
| Column |
Type |
Notes |
| id |
varchar PK |
e.g. 'airbnb' |
| display_name |
varchar |
|
| credentials_encrypted |
text |
AES-256 encrypted JSON |
| session_data_encrypted |
text |
Stored browser session |
| last_scrape_at |
timestamptz |
|
| is_active |
boolean |
|
performance_snapshots
| Column |
Type |
Notes |
| id |
uuid PK |
|
| platform_id |
varchar FK |
|
| captured_at |
timestamptz |
When this row was written |
| period_label |
varchar |
e.g. 'last_30_days' |
| views_search |
integer |
Times appeared in search |
| views_listing |
integer |
Times listing was clicked |
| conversion_rate |
numeric |
views_listing / views_search |
| bookings_count |
integer |
|
| occupancy_rate |
numeric |
% of available days booked |
| avg_daily_rate |
numeric |
|
| revenue_total |
numeric |
|
| raw_json |
jsonb |
Full raw payload for future parsing |
daily_prices
| Column |
Type |
Notes |
| id |
uuid PK |
|
| platform_id |
varchar FK |
|
| date |
date |
The night in question |
| price |
numeric |
|
| is_available |
boolean |
|
| min_stay_nights |
integer |
|
| synced_at |
timestamptz |
|
price_changes
| Column |
Type |
Notes |
| id |
uuid PK |
|
| platform_id |
varchar FK |
|
| date |
date |
Night being changed |
| price_before |
numeric |
|
| price_after |
numeric |
|
| changed_at |
timestamptz |
|
| changed_by |
varchar |
'scheduled' or 'manual' |
| note |
text |
User-provided reason |
| experiment_id |
uuid FK nullable |
|
experiments
| Column |
Type |
Notes |
| id |
uuid PK |
|
| name |
varchar |
e.g. "Lower weekend rate Jan test" |
| hypothesis |
text |
What you expect to happen |
| start_date |
date |
|
| end_date |
date |
|
| status |
varchar |
'active' | 'completed' | 'cancelled' |
| created_at |
timestamptz |
|
| conclusion |
text |
Notes written at end |
reservations
| Column |
Type |
Notes |
| id |
uuid PK |
|
| platform_id |
varchar FK |
|
| platform_reservation_id |
varchar |
Native ID from platform |
| guest_name |
varchar |
|
| check_in |
date |
|
| check_out |
date |
|
| nights |
integer |
|
| guests_count |
integer |
|
| nightly_rate |
numeric |
|
| cleaning_fee |
numeric |
|
| platform_fee |
numeric |
|
| total_payout |
numeric |
|
| status |
varchar |
'confirmed' | 'cancelled' | 'completed' |
| booked_at |
timestamptz |
|
| synced_at |
timestamptz |
|
| raw_json |
jsonb |
|
scrape_jobs
| Column |
Type |
Notes |
| id |
uuid PK |
|
| platform_id |
varchar FK |
|
| job_type |
varchar |
'performance' | 'pricing' | 'reservations' |
| triggered_by |
varchar |
'schedule' | 'manual' |
| status |
varchar |
'pending' | 'running' | 'success' | 'failed' |
| started_at |
timestamptz |
|
| completed_at |
timestamptz |
|
| error_message |
text |
|
| rows_collected |
integer |
|
7. API Endpoints
All endpoints are prefixed /api/v1. Auth required on all except /api/v1/auth/login.
Auth
| Method |
Path |
Description |
| POST |
/auth/login |
App login (single user) |
| POST |
/auth/logout |
Clear session |
| GET |
/auth/me |
Current session info |
Platforms
| Method |
Path |
Description |
| GET |
/platforms |
List platforms and status |
| PUT |
/platforms/:id/credentials |
Update stored credentials |
| POST |
/platforms/:id/test |
Test login + adapter health |
| POST |
/platforms/:id/scrape |
Trigger on-demand scrape (all types) |
| GET |
/platforms/:id/scrape-jobs |
Recent job history |
Performance
| Method |
Path |
Description |
| GET |
/performance/snapshots |
Query snapshots, supports ?platform=&from=&to= |
| GET |
/performance/summary |
Aggregated summary across platforms |
| GET |
/performance/trends |
Time-series data for charts |
Pricing
| Method |
Path |
Description |
| GET |
/pricing/calendar |
All daily prices ?platform=&from=&to= |
| POST |
/pricing/preview |
Dry-run bulk price changes, returns diff |
| POST |
/pricing/apply |
Apply previewed changes after confirmation |
| GET |
/pricing/changes |
Price change log ?platform=&from=&to=&experiment_id= |
Experiments
| Method |
Path |
Description |
| GET |
/experiments |
List all experiments |
| POST |
/experiments |
Create new experiment |
| PUT |
/experiments/:id |
Update (add conclusion, change status) |
| GET |
/experiments/:id/analysis |
Correlation: price changes → bookings/views |
Reservations
| Method |
Path |
Description |
| GET |
/reservations |
List reservations ?platform=&status=&from=&to= |
| GET |
/reservations/summary |
Occupancy, revenue totals by month/year |
Reports
| Method |
Path |
Description |
| POST |
/reports/weekly/send |
Manually trigger weekly email report |
| GET |
/reports/weekly/preview |
Preview this week's report as JSON |
8. Frontend — Pages & Views
Navigation Structure
Dashboard (/)
- KPI cards: occupancy rate, avg daily rate, total revenue MTD, search views (last 30d)
- Side-by-side platform comparison for all KPIs
- Booking trend sparkline (last 90 days)
- Recent reservations list (last 5)
- Scraper job status indicators (last run time per platform, success/fail badge)
- "Run Scrape Now" button per platform
Performance (/performance)
- Date range picker (presets: 7d, 30d, 90d, YTD, custom)
- Platform filter toggle (All / Airbnb / VRBO)
- Charts (all use Recharts):
- Search views over time (line)
- Listing click-through rate over time (line)
- Bookings per week (bar)
- Occupancy rate over time (area)
- Avg daily rate over time (line, overlaid with booking events)
- Data table below charts: raw snapshot history, exportable to CSV
Pricing (/pricing)
- Calendar grid view: each day shows price per platform, color-coded by deviation from base rate
- Sidebar panel: select date range + enter new price → generates preview
- Preview modal: shows diff table (date | platform | old price | new price) before any changes go live. Requires explicit "Confirm & Apply" button.
- Pricing change log table: filterable by platform, date range, experiment
- "Link to Experiment" action on any change or group of changes
Experiments (/experiments)
- List view: all experiments with status badge, date range, linked price changes count
- Create experiment modal: name, hypothesis, date range, initial notes
- Experiment detail page:
- Linked price changes table
- Performance chart for the experiment date range (views, bookings, occupancy)
- Before/after comparison: avg metrics N days before vs during experiment
- Conclusion text field (editable when status = completed)
Reservations (/reservations)
- Table: all reservations, sortable, filterable by platform/status/date range
- Monthly occupancy heatmap calendar
- Revenue by month bar chart
- YoY comparison once data spans 12+ months
Settings (/settings)
- Platform credentials (masked, update form per platform)
- App login password change
- Scrape schedule configuration (time of day for daily run)
- SMTP configuration for weekly report email
- Adapter health check panel: "Test Connection" per platform with live output log
9. Scraper Worker — Detailed Behavior
Session Management
- On first run, performs full login flow (email → password → handle any MFA prompt interactively via a special "needs attention" UI state)
- After successful login, saves browser storage state (cookies + localStorage) encrypted to DB
- On subsequent runs, restores session state and verifies validity before scraping
- If session invalid, re-triggers login flow and flags for user attention if MFA required
Scrape Job Flow
Error Handling
- Retry up to 3 times on transient failures (network, selector not found)
- On persistent failure: mark job as failed, store error message, surface in UI
- Never silently swallow errors — all failures logged to scrape_jobs table
- Screenshot on failure: save to
/data/screenshots/ volume for debugging
Anti-Detection Considerations (document in spec, implement in adapters)
- Randomized delays between actions (200–800ms)
- Human-like mouse movement patterns via Playwright's
mouse.move()
- Persist and reuse sessions to minimize login frequency
- Run during off-peak hours by default (configurable)
- User-agent set to current stable Chrome
10. Pricing Change Flow (Safety-First)
This flow must never apply changes without explicit user confirmation.
11. Weekly Report Email
Sent every Monday at 8am (configurable). Contains:
- This week vs last week: views, clicks, CTR, bookings
- MTD vs same period last month: revenue, occupancy
- Upcoming 30 days: occupancy %, revenue booked
- Active experiments: name, days running, early metric movement
- Pricing changes this week: count, avg delta
- Any scraper failures from the past 7 days
Format: HTML email with inline styles (no external CSS). Plain-text fallback included.
12. Docker Compose — Full Stack Definition
.env.example
13. Monorepo Structure
14. UI Design Direction
Aesthetic: Clean, data-dense, utilitarian dashboard. Think Vercel analytics meets a Bloomberg terminal — dark mode default, monospaced numbers, high contrast data visualizations. Not a generic SaaS template.
Color palette:
- Background:
#0a0a0a (near-black)
- Surface:
#141414
- Border:
#262626
- Primary accent:
#22c55e (green — for positive metrics, occupancy, revenue)
- Warning:
#f59e0b
- Danger:
#ef4444
- Text primary:
#fafafa
- Text muted:
#737373
- Chart colors: distinct accessible palette (no red/green only for accessibility)
Typography:
- Numbers/data:
JetBrains Mono or IBM Plex Mono — monospaced for alignment
- UI labels:
Geist or DM Sans — clean, modern, not generic
Interaction patterns:
- All data tables have column sorting
- All date range pickers have keyboard support
- Loading states: skeleton screens (not spinners)
- Real-time job status via WebSocket (SSE acceptable as simpler alternative)
- Mobile: bottom tab navigation, cards stack vertically, charts scroll horizontally
15. Key Implementation Notes for Claude Code
- Start with the database schema and Drizzle migrations — everything else derives from this
- Build the adapter interface and a mock adapter first — use the mock for all frontend/API development before real scrapers are needed
- The Airbnb adapter is higher priority than VRBO — build and test it first
- All selector strings must be constants — never inline a CSS selector in logic code
- Preview-before-apply is non-negotiable — the
/pricing/apply endpoint must reject requests without a valid preview token
- Session encryption is day-one, not a later hardening step — credentials never touch disk unencrypted
- The weekly email must work with any standard SMTP provider — no vendor lock-in (no SendGrid dependency)
- Write a
docker-compose.dev.yml that mounts source volumes and enables hot reload for both api and frontend
- Include a
/api/v1/health endpoint that checks DB connectivity and returns scraper worker status
- The README must include first-run setup steps, how to update platform credentials, how to add a new platform adapter, and how to run outside Docker for local development
16. Acceptance Criteria
The following must all be true for v1 to be considered complete: