


- Role
- Solo build
- Year
- 2026
- Status
- Live, by invitation · v1
- Live site
- Demo on request
- Repo
- Private during v1
- Capabilities
- Back-end & data
- Front-end
- AI and LLM integration
- Infra and delivery
- Stack
- Claude Haiku
- agentic tool use
- FastAPI
- Psycopg 3
- Alembic
- Next.js 14
- TypeScript
- PostgreSQL 16
- JSONB i18n
- Cloudflare R2
- Caddy + on-demand TLS
- python-telegram-bot
- Docker Compose
- EN / HR
Signal path · 5 stages
How it works
- Photographer: in Telegram
- Bot: Claude Haiku · tool use
- API: FastAPI · tenant-scoped SQL · R2
- Website: Next.js
- Own domain: Caddy on-demand TLS
What it does
Each photographer gets their own domain and a full website, and manages all of it by chatting with a Telegram bot: uploads, captions, layout, copy, even DNS, all in plain language. I built it to find out what changes when an LLM is the only write path to a real product.
A photographer adds the bot in Telegram and has a working website at a subdomain of their choice within minutes. They send photos one at a time or in batches; the bot uploads them, generates captions, picks the right gallery and updates the site. They can ask it to “swap the homepage hero”, “make the about page warmer”, “translate everything to Croatian” or “use my own domain studiomariana.hr”, all in the same chat.
There is no admin panel, by design. Every operation a photographer might ever want exists as a tool the model can call: uploading photos, setting captions, reordering galleries, changing the theme colour, binding a custom domain, adding new sections. The chat is the product, not a layer on top of one: the bot is the CMS.
How it’s built
Three services, one boundary
There are three services and one boundary worth pointing at: the bot writes, the website reads, and they never share a process. The bot is the only write path; the website is read-only output.
The bot runs the agentic loop with Claude Haiku and calls tools that hit the FastAPI service. FastAPI owns the database and R2. The Next.js site reads from FastAPI and is served by Caddy with on-demand TLS, which is what lets each photographer point any domain they own at the same IP and have a certificate provisioned for it within seconds.
The components
- Bot (
bot.py, on python-telegram-bot): takes text and photos from the photographer’s Telegram client and runs the Claude Haiku agentic loop, which writes alt text, categorises photos and translates copy. - API: a FastAPI service on Psycopg 3 and raw SQL, with pytest tests and Alembic migrations.
- Database: PostgreSQL 16, with
photographer_idon everything, and translations (English and Croatian) and theme settings in JSONB. - Storage: Cloudflare R2 (S3-compatible), with UUID keys and original, web and thumbnail versions of each photo.
- Website: Next.js 14 and TypeScript on the App Router, built as standalone output and themed with CSS variables.
- Edge: Caddy, issuing Let’s Encrypt certificates on demand and serving the site to visitors on any custom domain.
The hard parts
Tenant isolation in every query
Every photographer’s data is scoped behind a tenant-id filter on every read and every write. There’s no ORM, so the filter is visible at every call site and a tenant leak shows up in code review rather than at runtime. A tiny helper wraps the cursor to keep the ergonomics close to an ORM, without the “we’ll inject the filter for you, trust us” magic.
A leak in a multi-tenant query is a privacy incident, not a bug. The cure is to make the filter loud, not invisible.
Bring your own domain
Photographers don’t know what an A record is, and they don’t need to. The bot walks them through it in plain Croatian, and the flow goes like this:
- The photographer says “I bought studiomariana.hr, can we use it?”
- The bot replies “Great. Go into your registrar’s dashboard, find the record that looks like this, and paste this number.”
- The bot polls the domain every few seconds.
- The moment propagation completes, the bot says “Ready, I just turned it on.”
TLS issued on first request, gated
Caddy issues a Let’s Encrypt certificate the first time it sees traffic for a new domain, but only if the back end confirms, through a registration-check endpoint (/photographers/exists), that the domain belongs to a registered photographer. That gate is what stops random domains pointed at the IP from burning through Let’s Encrypt rate limits. Cheap, boring, very effective.
Status
A small group of photographers use it for their working portfolios. The website side is read-only HTML served through Caddy, with no JavaScript for content; for now, Next.js is mostly a build-time templating layer. At this scale, the cost per tenant is dominated by R2 storage and a few cents a month of Haiku tokens, well below what any hosted-CMS subscription would charge. The bot runs on real tokens, and demo access is by email.
On the roadmap:
- a screenshot test suite that diffs the rendered site against the last known good version after every tool call, so the bot can roll itself back when it accidentally turns the whole homepage pink
- an eval harness for the agentic loop itself
In numbers
- admin panels, by design
- 0
- from adding the bot to a working site
- Minutes
- to TLS on a photographer’s own domain
- Seconds
- of Haiku tokens per tenant a month
- A few cents