Tunrooms free architecture

AI search widget on tunrooms.com. Infrastructure runs on free plans. The client pays OpenAI on its own account. Blue shows the request path. Orange shows the data path.

Tunrooms free architecture The visitor's widget calls a Cloudflare Worker. The Worker checks a SQLite Durable Object, calls OpenAI for intent and query embedding, searches the snapshot in memory and returns cards. GitHub Actions runs a daily catalog crawl and embedding job that publishes snapshots to R2. A Worker Cron refreshes calendars every minute into calendars.bin in R2. A future client read API will replace scraping. tunrooms.com page Cloudflare · Free plan Free Visitor on a tunrooms.com page Visitor browser Widget script served by the Worker Widget tunrooms-ai-live.js chat box + result cards Cloudflare Worker (Free plan, 10 ms CPU per request) Worker ≤10 ms CPU POST /api/chat GET /api/status GET /tunrooms-ai-live.js In-memory search snapshot cached in Worker memory filters → quotes → cosine → top 12 calendar age ≤3 h: observed 3–24 h: stale · >24 h: unknown 4 SQLite Durable Object: one call per chat for both checks Durable Object SQLite per IP: 10/min · 60/day app AI budget: min / day 1 call per chat R2 bucket tunrooms-search (private) R2 · tunrooms-search private snapshots/<rev>/ immutable · keep 10 manifest.json catalog.json vectors.f32 summaries.json current.json health.json calendars.bin row i of every file = listing i Worker Cron Trigger: calendar refresh inside Cloudflare Worker Cron every minute ~45 listings per run ≤4 concurrent requests all ~660 every ~15 min no GitHub minutes OpenAI, billed to the client's account OpenAI Client pays gpt-4o-mini intent text-embedding-3-small 1536 dims · queries + listings GitHub Actions, free org TunroomsAI: heavy daily job only GitHub Actions Free org TunroomsAI · daily sync 1. crawl catalog 37 pages × 3.5 MB 2. embed changed listings 3. validate + guards 4. upload snapshots/<rev>/ 5. swap current.json conditional write (If-Match) calendars: Worker Cron tunrooms.com: the data source today (scraping) calendar endpoint per listing catalog pages 37 pages × 3.5 MB tunrooms.com · source scraping now Client read API: future source, replaces scraping Client read API later · when the client delivers it GET /v1/export GET /v1/changes POST /v1/availability/check replaces scraping; live check for shown cards 1 6 2 3 intent + query embedding 5 live check later · ≤12 ids · 1 s A daily crawl B embed changed C publish snapshot, swap current.json D calendars If-Match write → calendars.bin E R2 binding → memory pointer check ≤1 / 60 s

Legend

  • Request path · numbered circles
  • Data path · lettered squares
  • Dashed: later, after the client API exists
  • Shaded area: owner / plan

What is free / who pays

ServiceCost
Cloudflare Worker + CronFree
Durable Object (SQLite)Free
R2 bucketFree
GitHub Actions, org TunroomsAIFree
OpenAI (intent + embeddings)Client pays

Search pipeline: CPU budget per chat

Runs inside the Worker on every chat (request steps 2–4). Bars use one 0–3 ms scale. The light part shows the top of a range. Network wait is not CPU.

  1. 1
    Body + state
    state.ts validation
    0.5 ms
  2. 2
    Admission
    one DO call · network
    ~0 ms
  3. 3
    Intent
    OpenAI fetch + validation
    1 ms
  4. 4
    Snapshot
    cold load, single-flight
    2–3 ms cold · 0 warm
  5. 5
    Hard filters
    city, type, guests, blocked days
    0.5 ms
  6. 6
    Quotes + budget
    weekend rule, overrides
    1–2 ms
  7. 7
    Embed + cosine
    619 × 1536
    1.5 ms
  8. 8
    Top 12 + JSON
    templates
    0.5 ms
Total target < 8 msp99, cold and warm, 90-night searches
0 8 ms target 10 ms free limit

Request path

  1. 1Widget sends POST /api/chat to the Worker.
  2. 2One Durable Object call checks the per-IP limit and the app AI budget.
  3. 3OpenAI returns the intent and the query embedding.
  4. 4The Worker searches the snapshot in memory: filters, quotes, cosine, top 12.
  5. 5Later: live check of the shown cards on /v1/availability/check.
  6. 6Cards go back to the widget.

Data path

  1. ADaily GitHub Actions job crawls the catalog (37 pages × 3.5 MB).
  2. BIt embeds only the changed listings with OpenAI.
  3. CIt writes immutable snapshots/<rev>/, then swaps current.json.
  4. DWorker Cron, every minute: ~45 calendars, ≤4 at a time, then an If-Match write of calendars.bin. All ~660 listings refresh about every 15 min.
  5. EThe Worker loads the snapshot from R2 into memory.