stay automation
keeps a pool villa's booking channels in sync through their admin pages, plus guest texts and fee checks
- when
- May 2026 – now
- for
- a small pool villa business in korea
- role
- solo: design, build, deploy, operate
- stack
- TypeScript, Next.js, Playwright, Prisma, PostgreSQL, Docker, Cloudflare Workers, SOLAPI
- size
- 460 commits, 44k lines, 17 prisma models, 40 test files
- client work, through neoworks
also in 한국어
what it is
a small pool villa with four room types sells the same rooms on Naver SmartPlace, Yeogi Eottae and Yanolja. the code can also pull Airbnb and Agoda bookings over iCal. this is the back office: every 5 minutes a cron container reads bookings from every channel, closes the room on the other channels when one sells, and opens it again on cancellation. on top sit guest texts, extra fee settlement against bank deposit emails, a Naver Works calendar, and a korean admin dashboard.
it's three containers on one server: the Next.js app with Playwright inside, Postgres, and a tiny alpine cron. bank emails reach the app through a Cloudflare email worker.
the hard parts
there are no partner APIs here. everything runs on the owner's own admin logins, through the pages or the internal APIs behind them, and each channel fails differently.
naver locked the account, and my first diagnosis was wrong. on september 13 naver changed its login button markup. my code had one selector for it, so every login timed out. the naver path also had no backoff, so the 5 minute cron kept firing fresh logins: about 85 an hour, 138 failures in a row over 3 days. naver answered with a captcha on the account. i told the owner to pass the check in his own browser. that couldn't work: the captcha is tied to the session that triggered it. and the trigger was the headless fingerprint. same IP, same account, ten minutes apart: headless got a captcha, headful chromium under Xvfb with the automation flag off got none. now only the password-typing path runs headful. the 5 minute polling stays headless on saved cookies. even a solved captcha isn't the end: naver sends you back to the login page, and you have to submit the password again on that same page.
tokens lie about their lifetime. yanolja's token says it lasts 60 minutes. in practice it gets rejected after about 15 or 16. its iat and exp are also in milliseconds instead of seconds, so read naively it expires in the year 58560. that was the owner's "it keeps logging out". now credentials refresh ahead of time from the shorter of the claimed and the measured lifetime, and the measured one only ever shrinks. one day yanolja also started parking logins on a "change your password" page. the fix clicks "change after 30 days", which turned out to be a div, not a button.
"done" that wasn't done. opening a room on naver takes two writes: a sale switch and a stock count. the code flipped the switch, never checked the count, and reported success, so the room stayed unsellable while the dashboard said open. now every write is checked. it only pushes stock back up for dates where the owner explicitly asked for an extra room. on every other date he lowers stock by hand on purpose, and the next cron cycle must not quietly undo that.
the same pattern kept showing up in other places:
- settlement marked a 30,000 won deposit as paid for a 90,000 won charge, because matching only required the sender's name. 2 of 76 paid rows were short. now a row is paid only when total deposits cover the charge.
- unit numbers were recomputed on every render, sorted by guest name, so a same-day booking could push an assigned guest into another unit. staff walk guests to rooms by those numbers. assignments are now stored, first one wins.
- the cron container stayed dead for 3 days.
unless-stoppeddoesn't restart a container docker counts as stopped, and the health check lived inside the cron it was supposed to watch, so everything stayed green. a watchdog now lives in the app process. it texts after 60 silent minutes and runs the sync itself, with one shared lock so guests can't get the same text twice. its test replays the real outage: 12 alerts in 72 hours, not 432.
the rest of the system
calendar sync went through three versions. first an ICS feed. then cancellation tombstones, because subscribed calendars don't drop events that just disappear from a feed. then direct writes to the Naver Works calendar API, because Works ignores STATUS:CANCELLED in subscribed feeds. the cutover wrote 205 bookings and checked them by reading back. a later backfill added 539 past events.
for the same business i also built a kakao channel chatbot that answers guests from a Google Sheet knowledge base and looks up bookings from this system's calendar feed, a windows tool that posts photos to Instagram, Naver blog and Naver cafe, and a naver place keyword tool.
where it is now
live and used daily. the latest fixes are from this week: naver's week header has a different format for the week that crosses new year, and room blocks for months naver hasn't opened yet now wait quietly instead of showing a red banner. most of the 460 commits follow the same loop: an owner report, a live measurement, a regression test.