portfolio email github linkedin

Side project · 2026v1 · live

Photo Portfolio Platform — a Telegram bot that ships websites.

A multi-tenant SaaS where each photographer gets their own domain and a full website that they manage entirely by chatting with a Telegram bot. Uploads, captions, layout, copy, even DNS — all in plain language. Built solo in 2026 as a way to find out what changes when an LLM is the only write path to a real product.

Claude Haiku agentic tool-use FastAPI psycopg3 (no ORM) Alembic Next.js 14 TypeScript Tailwind (CSS-var theme) Postgres 16 JSONB i18n Cloudflare R2 Caddy + on-demand TLS python-telegram-bot Docker Compose EN / HR

01What it does

A photographer adds the bot in Telegram and gets 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 live site. They can ask "swap the homepage hero", "make the about page warmer", "translate everything to Croatian", or "use my own domain studiomariana.hr" — all in plain language, all in the same chat.

There is no admin panel, by design. Every operation the photographer might ever want is a tool the model can call: upload_photo, set_caption, reorder_gallery, set_theme_color, bind_domain, add_section. The chat is the product, not a layer on top of one.

02Architecture

Photographer (Telegram client) text · photos bot.py Claude Haiku · agentic loop alt-text · categorise · translate tools data FastAPI service psycopg3 / raw SQL pytest · Alembic migrations Postgres 16 photographer_id everywhere JSONB i18n + theme Cloudflare R2 (S3) UUID keys · orig / web / thumb Next.js 14 App Router · standalone CSS-var themed Caddy on-demand TLS · LE certs /photographers/exists → any custom domain → visitors

Fig. 1 — the bot is the only write path; the website is read-only output.

Three services and one boundary worth pointing at: the bot writes, the website reads, and they never share a process. The bot runs the agentic loop with Claude Haiku and calls tools that hit FastAPI; 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 cert provisioned for it within seconds.

03Under the hood

Tenant isolation in every query

Every photographer's data is scoped behind WHERE photographer_id = $1 on every read and every write. There's no ORM: the filter is visible at every call site, so a tenant leak shows up in code review rather than at runtime. A tiny helper wraps the cursor — db.one("SELECT … WHERE photographer_id=$1", pid) — so the ergonomics stay 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 flow is: photographer says "I bought studiomariana.hr, can we use it"; bot says "great — go into your registrar's dashboard, find the record that looks like this, and paste this number"; bot polls the domain every few seconds; the moment propagation completes, bot says "ready, I just turned it on."

TLS issued on first request, gated

Caddy issues a Let's Encrypt cert the first time it sees traffic for a new domain — but only if FastAPI confirms via a /photographers/exists?domain=… endpoint 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.

04Status

v1 is live with a small group of photographers using it for their working portfolios. The website-side is read-only HTML served via Caddy, no JS for content (Next.js is mostly a build-time templating layer at the moment). Cost per tenant at this scale is dominated by R2 storage and a few cents per month of Haiku tokens — well below what any hosted-CMS subscription would charge.

On the roadmap: a screenshot test suite that diffs the rendered site against last-known-good after every tool call, so the bot can self-rollback when it accidentally turns the whole homepage pink. And an eval harness for the agentic loop itself.

Resources

Live demoby invitation — email for access (the bot is live and runs on real tokens)
App repoprivate during v1 — happy to walk through it
Infra repoprivate during v1