A working photographer pays for three subscriptions.
One for the portfolio site, because the client has to see the work. One for client galleries, because sending 400 photos over WeTransfer is not a delivery experience. One for invoicing, because the money has to arrive.
None of the three knows about the other two. The same client exists three times, under three slightly different spellings. The invoice has no idea which gallery it is for. The booking that started all of it lives in a fourth place, usually a phone's calendar app.
profotograaf is those three things in one place, on the photographer's own domain. It is a platform I am actively building out — not a weekend project I put on a shelf.
The bet
The bet is not that photographers want fewer tools in the abstract. Everyone says that and then buys another tool.
The bet is that the joins are where the value is. A booking that generates the invoice. A gallery that belongs to a client rather than to a link. A portfolio and a client area on the same domain, so a client who lands on one can find the other. None of that is possible while the three live in different companies' databases.
The initial wedge is photographers, which is a specific choice: tech-savvy enough to want a real site, unwilling to pay for the enterprise tooling, and vain enough — in the good way — to care what the gallery looks like when a client posts it to Instagram. Adjacent professionals reuse most of the same surface, but generalising too early is how you build something for nobody.
Pages are blocks
The heart of the product is a block-based page builder. The photographer's public site, their about page, their contact page and their gallery share pages all come out of the same renderer.
Blocks matter here for an unglamorous reason: a photographer wants their site to look like theirs, and a template with three configurable colours does not achieve that. Nine themed template families ship out of the box — atelier, editorial, frame, grid, lumen, magazine, minimal, mono, studio — and the builder lets you take one apart.
Everything about that page is versioned. Draft and publish are separate states, so the site does not change while you are mid-edit. Page versions mean a rollback is a click, not a support ticket. And AI-suggested copy arrives as a proposal you accept or reject, never as an edit that silently happened to your live site — the AI drafts, the photographer decides.
Galleries are the growth loop
The gallery slice is by far the largest part of the codebase, and that is not an accident. It is the part clients see.
Curated galleries and folder-backed galleries, presigned uploads with async variant processing, a photo library with folders, watermarking, share pages with passwords and expiry, proofing rounds where a client marks selects and leaves comments, downloads, per-gallery analytics.
The public pages are server-rendered Go templates rather than a single-page app, and that is a distribution decision rather than a technical preference. A gallery link gets pasted into WhatsApp, into Slack, into an Instagram story. It needs to render its own preview card without waiting for JavaScript, and it needs to be crawlable. Shareability is the growth loop, so the shareable surface gets the server-rendered treatment.
The app never touches the bytes
Photos go browser to object storage directly, via a presigned PUT. The application issues the signature and then gets out of the way.
This is the single decision I would most strongly recommend to anyone building something similar. A photographer uploads a 4 GB wedding in one sitting. If those bytes flow through your application you are now running a file transfer service that happens to have a web UI attached, and every upload competes with every page render for the same memory. Hand out a signature instead, let the client talk to the bucket, and process variants asynchronously afterwards.
One binary, three modes — but that is not the story
For completeness, since I have written about this pattern before: it is a single Go binary that dispatches on a mode variable into a writer that owns migrations and all writes, N read-only readers, and a controller that reconciles custom domains. Postgres in every deployed environment, SQLite locally. Vertical feature slices under internal/features/, so a change to galleries touches the gallery directory and stops there.
That is the architecture, and it is deliberate — one operator, no ops team, so every choice is biased toward fewer moving parts to run. But I want to be careful not to sell the plumbing as the product. The one-binary trick is the interesting part of BugBarn and FunnelBarn, which are self-hosted tools where "you can run this yourself on a small VPS" is the entire pitch.
profotograaf is not that. Nobody self-hosts their photography business. The customer never sees the binary; they see whether their gallery loads on a phone at a wedding venue with two bars of signal.
Where it is going
Custom domains are live, so a photographer's site is at their address rather than a subdomain of mine. Billing is in. The client portal — overview, galleries, proofing, invoices, messages — is server-rendered at /{slug}/client/, with stateless signed-cookie sessions rather than a session table.
What is next is more of the business layer, and more of the joins: the booking that knows about the questionnaire, the questionnaire that becomes the contract, the contract that becomes the invoice. That is the whole thesis, and it is the part no combination of three separate subscriptions will ever give you.