← Projects

Project write-up · wilssens.com

Building wilssens.com

A family name, turned into a platform: how a search box became mailboxes, websites, AI apps and a command line, and the rules that kept it safe.

Steven Wilssens · October 2026 · 15 min read

3
family mailboxes moved off Microsoft 365
5
websites live on the family domain
16
questions to the help assistant this month
$1.26
AI help cost this month, of a $25 cap

Today www.wilssens.com runs my family’s email and websites: mailboxes moved off Microsoft 365 to one automated Zoho organization, five websites live, and a help assistant that answers relatives’ questions before they reach me.

01A domain with a family attached

wilssens.com is our family domain. Anyone named Wilssens, and their household, can claim a name on it: [email protected] for email, jan.wilssens.com for a website, or both.

It started on 29 September 2026 as a demand test. There was no sign-in and no payment, just a search box and a Request button, to answer one question: will Wilssens actually want this? Two weeks later the same site sets up real mailboxes, hosts real websites, runs server code with its own databases, lets AI apps publish sites directly, and answers questions in three languages.

The home page. Type a first name and the site answers as you type; here, both addresses for “emma” are free.

This is how it was designed, what is under the hood, and the best practices behind it.

02Rules before features

The most important design work was not code. It was a short list of rules about names, written down early and enforced in the database, so no future feature could quietly break them.

  • A name belongs to the person it is named after, not to whoever pays. A parent managing a household can stop paying for a child’s address, but can never reassign it or read its mail. The person can always take over.
  • Your name is yours for as long as you want it, and stays protected for 24 months after you give it up. When someone deletes their last service, the name goes into quarantine: mail to it bounces and only the original owner can bring it back. After two years someone else may claim it, but never with the old mail or settings. A database trigger refuses any other outcome, whatever the application code tries.
  • No public list of names or addresses. The site shows totals only: members, people waiting, countries. A list of family email addresses is a gift to spammers, and households include children. The one exception is an opt-in directory of websites, each one switched on by its own adult owner.
  • Prices live in exactly one file. Pages, the calculator, emails and the Stripe catalogue all read from it.
A name returns to the pool only after 24 months of quarantineFreeanyone may askRequestedbackups queueOwnedby the personQuarantinemail bouncesrequestapproveor payall servicesdeletedonly the owner can set it up againafter 24 months, and never with the old mail or settings
The database itself refuses to hand a name to anyone else while it is owned or in quarantine.

These rules shaped everything after. Ownership checks, quarantine, and “start clean” when a name changes hands appear again in every feature below.

03Small parts, chosen on purpose

The whole platform runs on Cloudflare plus two outside services, Zoho for mail and Stripe for payments. There are no servers to patch.

Two front doors share one set of servicesFamily membersAI apps over MCPCommand lines, GitHubSite visitorswww.wilssens.comAstro pages in English, Flemish and FrenchOne Pages Function: search, requests, sign-in, accounts, admin, MCPname.wilssens.comEvery family sitestatic + server codeD1 databaserequests, namesR2 storagesite versionsSite Workersown code + DBZoho Mailmailboxes, mailStripepayments (off)
People, AI apps and command lines all come in through www; visitors of a family site come in through its own subdomain. Both reach the same storage and services.
PartWhat it doesWhy this one
Astro, static pagesEvery public page in English, Flemish and FrenchFast, cheap, nothing to run; plain scripts, no framework
One Cloudflare Pages FunctionThe single API: search, requests, sign-in, accounts, adminOne door to guard instead of many
Cloudflare D1 (SQLite)Requests, names, sites, sessions, usageBound directly to the Function, so there is no public database endpoint to lock down
Cloudflare R2Hosted websites and screenshotsOne object per site version keeps every page view to two storage calls
A second Pages projectServes every name.wilssens.comOwners’ sites never run on the main site’s address
Workers for PlatformsEach site’s own server code and databaseHard isolation per family member
Zoho Mail APIMailboxes, forwarding, outgoing mailOne organization for the whole domain, DKIM-signed
Turnstile, PostHogSpam protection, privacy-friendly analyticsNo cookies for analytics, no visible captcha for most people

Two choices did most of the work. The database is reachable only through the Function, so there is no API key to leak and no row-level security to get wrong. And every hosted site is stored as a single packed file per version, which made publishing and serving cheap and predictable at any size.

Tests run against a real copy of the schema, with every migration and trigger applied, on Node’s built-in SQLite. Every push runs tests, type checks and a build; a push to main also migrates the database, deploys, and checks that the family’s existing sites still answer.

04One click from request to working mailbox

A visitor searches a name and presses Request; no account needed. The search answers as you type: available, already requested (join the waiting list), taken, or in quarantine until a given month. The request is saved, and confirmation emails go out in the requester’s own language.

Then one function does everything: approveRequest(). Today I trigger it with an Approve button in the admin. Once payments are switched on, a completed Stripe checkout calls exactly the same function, so the paid path and the free path can never drift apart. It:

  1. Retires the name, so the database will refuse it to anyone else.
  2. Creates the address at Zoho: a forwarding group for email forwarding, or a full mailbox with a temporary password.
  3. Attaches name.wilssens.com at Cloudflare as a redirect or an empty hosted site, and waits for the certificate.
  4. Emails the owner when each part is live, in their language.

Each step can fail without losing the request: emails are sent in the background and logged on failure, and a “Check now” button retries anything still queued. The temporary mailbox password is never emailed; the email carries a one-time link that shows it once.

Owners then manage everything on their account page: switch forwarding to a mailbox, a redirect to a hosted site, add a service, or delete their profile. Deleting goes through the same quarantine rule as everything else.

05Why Zoho, and why automate it

The biggest win is not a feature. It is that family mail became easy to support and cheaper to run.

Before, our addresses lived on Microsoft 365 through GoDaddy: a separate paid plan per person, extra security add-ons, and every mailbox set up by hand. By 12 October every mailbox had moved to a single Zoho organization for the whole domain, for much less than we paid before.

Mail moved from per-person plans to one automated organizationBefore: GoDaddy, Microsoft 365A paid plan per person, plus add-onsEach mailbox set up by handEvery question came to meNo single view of what worksNow: Zoho, automatedOne organization for the whole domainApprove, and a mailbox is ready in minutesThe help assistant answers firstThe admin checks every address
Family mail, before and after: Microsoft 365 through GoDaddy to one automated Zoho organization.

With Zoho’s API behind the Approve button, the operator’s work shrank to almost nothing:

  • A new mailbox takes minutes. I press Approve; the address, the password link and the welcome email follow on their own, in the person’s language.
  • Help starts without me. Relatives ask the help assistant first, which knows our guides for iPhone, Outlook, Windows and Zoho’s own settings. Only what it cannot solve reaches me.
  • I can see whether things work. The admin checks every address against Zoho, visits every website address, and flags anything that does not match, before a relative notices.
  • Mail apps set themselves up. Autoconfig, autodiscover and an iPhone profile fill in the server settings, so most people never type them.

For one person supporting a whole family, that is the difference between a hobby and a help desk.

06Five ways to build a website, one door to publish

The family ranges from teenagers to grandparents, so there could not be one way to make a site. There are five, and all of them end in the same function, publishSite(), which applies the same checks every time.

The website help centre. Its header animation is the whole idea in one picture: every way in produces a new version, and that version goes live.
  • A page maker for people who never want to see code. Pages are stored as data, not HTML: blocks for text, photos, galleries, video, maps, timelines and more, eight themes, three languages, and up to ten pages. Photos are re-encoded in the browser, so location data never reaches the public page.
  • Editing a live site in place. If a site was built some other way, the editor loads the real site, running, and lets you click on any text or photo and change it. Sections can be moved, hidden or duplicated, and styled per screen size.
  • Upload a file, a folder or a zip.
  • Your own AI app. The site speaks MCP, the protocol AI assistants use to connect to tools. Connect Claude or ChatGPT once with a standard OAuth sign-in, then say “add a photo gallery to my site” and the assistant reads the files, edits them and publishes a private draft for you to preview. Each site gets its own connector address, so an app connected to one site can never touch another.
  • A code editor or GitHub. The wilssens command line or a starter project gives you the site as a folder, with instructions for AI coding assistants already inside, a GitHub Action that publishes on every push, and deploy keys you can revoke.

Every version is kept and immutable, so rolling back is one click. Drafts can be previewed on the real address with a one-time link, and only in the owner’s browser.

The gate in front of all five is the same. Files with API keys are refused. A page with a sign-in form naming a bank or a mail provider is held for a human to review, so a family website can never become a phishing page on a trusted domain. Every page carries a small “Report this page” line.

Five ways in, one gate, three outcomesPage makerEdit in placeUploadAI app over MCPCode editor, GitHubpublishSite()the same checks,every timeRefusedan API or private key in a fileHeld for a person to reviewa sign-in form naming a bank ormail provider, a form to another sitePrivate draft or livea new version, kept for good;going back is one click
Whichever way a site is built, it meets the same checks, so a new way of building never opens a new hole.

For the curious, a site can also run server code with its own database. Each site gets its own isolated Worker and SQLite database, its own secrets, an hourly call for scheduled work, and monthly limits with warnings at 90%. Over a limit, only the site’s API pauses; the pages keep working. It is the same Cloudflare stack a company would use, sized for one family member’s hobby.

Owners can then choose to list their site in the family directory or ask to be featured on the home page. Screenshots of each site, desktop and phone, are taken automatically twice a day.

The family directory at /sites, filtered. Every card is there because its owner switched it on; the screenshots are taken automatically.

07Three ways in: the website, AI apps and the command line

Everything an owner or an admin can do exists three times: on a web page, as tools for AI apps over MCP, and in a command line. It is built only once, on the server, and all three call it.

Three ways in, one set of tools, for owners and for adminsWebsite owners/account in the browserAI apps at /mcpwilssens command lineOwner tools, built once on the serverthe same checks, wording and confirmation everywhereevery publish goes through publishSite()Admins/admin in the browserAI apps at /adminmcpwilssens-admin command lineAdmin tools, built once on the servera fresh Zoho two-step sign-in to approve each connectiona typed confirmation and one audit log for every callThe only gaps are deliberate: new deploy keys and secret values never pass through an AI chat
Owners and admins each get three ways in to one set of tools on the server.

The rule behind it is simple: a new action is written as a server handler first, then exposed as a tool, then given a page, a command and a test, with the same checks, confirmation and wording everywhere. Tests check that the same call through MCP and through the command line reaches the same handler and gives the same answer. That means an AI app can never do more than the website allows, and a feature is never “web only” by accident.

For website owners

  • AI apps connect to www.wilssens.com/mcp, or to a per-site address, with a normal OAuth sign-in. They can read files, publish drafts, preview them, put a version live, read server-code logs, query the site’s database, and see or end the owner’s own sign-ins.
  • The wilssens command line uses the very same tools. init downloads the live site as a project folder, dev runs it on localhost with live reload (with /api/ going to the real server code), and site deploy --draft publishes a private draft with a preview link.
  • Two things deliberately never pass through an AI chat: making a new deploy key and typing a server-code secret. Those exist only on /account and in the command line, where secrets are typed hidden.
The owners’ command line. Commands that change something are marked with a flag, and every list can also come out as JSON, YAML or CSV.

For admins

The same idea, with tighter locks, because these tools can approve requests, delete mailboxes and switch things off.

  • An admin MCP server at /adminmcp gives an AI app everything /admin can do. Approving a connection needs a fresh Zoho sign-in with two-step verification, for that very approval; an existing session is never enough. Access tokens last 15 minutes and every connection ends after 30 days.
  • Anything that cannot be undone (deleting, declining, rejecting, switching off, ending sign-ins) needs the target typed back. Database queries are read-only, one SELECT at a time, and cannot touch sign-in, key or payment tables.
  • The wilssens-admin command line ships as a branded Windows installer and runs every admin tool from the terminal, plus an AI analyst: Claude with the admin tools, which reads the data itself and writes a briefing on demand, traffic, sites and problems. Requesters’ emails are masked before anything reaches the model, and it can propose actions but needs a typed yes.
  • One audit log records every call, from AI apps and the command line alike, each under its own name.
The admin command line: incident switches, the AI analyst, and the same tools the admin MCP server offers.

A command line built like the good ones

The command lines borrow their habits from gh, kubectl, git and the Stripe CLI:

  • Commands are declared as data, so help pages, argument checks and shell completion are generated and can never drift apart.
  • Commands return data and never print it themselves: every list can come out as a table, JSON, YAML, CSV or NDJSON, filtered with --fields or a small jq-like --query.
  • Typos get “Did you mean …?”, errors say what to do next, and exit codes are documented.
  • A person gets pickers and y/N prompts; a script never gets asked anything and must pass --yes. Every action supports --dry-run, and actions are never retried, so nothing is approved twice.
  • doctor checks the version, the sign-in, the server and, for admins, whether the family’s own sites still answer.

Coding agents included

Tell a coding agent “set up wilssens for my website” and it can do it alone, following the AGENTS.md that ships with the command line. The only step that needs the person is signing in: the command prints a link and opens the browser, and the person approves it on wilssens.com. No password, token or code ever passes through the agent.

08An AI helper that is restricted by design

The only AI the site runs itself is a help assistant on every page, built on Claude. It answers “how do I add my address to Outlook on my iPhone?” in English, Flemish or French, streams its answer, and links to the guide topics it used.

The help assistant docks beside the page. Visitors without an account pass a Turnstile check first; the footer reminds people never to share a password.

It knows only what we send it: the email and website guides, the FAQ, the privacy text and the prices, about 12,000 tokens, cached for an hour. It may also use general knowledge of the apps people actually use, because “Outlook says this is the primary account” is exactly the kind of question the guide never covers.

The interesting part is what it cannot do, enforced in code rather than in the prompt:

  • It offers; it never acts. It sees only the signed-in person’s own names, services and recent site versions. When something needs doing, it offers a card under its answer, and the person does the rest (below).
  • A hidden first line in every reply says whether the question was in scope. The server reads it and strips it; off-topic questions get our fixed answer, never the model’s.
  • Signed answers. The conversation lives in the visitor’s browser, so every answer carries a signature. A conversation containing an answer the server did not sign is refused, which closes the classic trick of feeding a model a fake earlier reply.
  • Abuse detection that acts on its own. Bursts, off-topic streaks or a cost spike first restrict the assistant to account holders, then switch it off, and email me once. Only a person can switch it back on.
  • A monthly budget of $25, counted from the API’s own token numbers.
Every answer passes checks the model cannot talk its way aroundQuestionfrom any page,3 languagesWho may askTurnstile, ratelimits, budgetSignaturesevery earlieranswer is oursClaudeour guides only,offers cards onlyScope linehidden, read bythe serverAnswerstreamed, withsources, signedTurned awayand counted by detectionFixed answernot the model’snonooff-topicAbuse detection: bursts or a cost spike limit it to account holders or switch it off
The model writes the answer, but the server decides whether it is shown.

Action cards: the assistant proposes, the person decides

The redesign gave the assistant a way to help with the next step, not just explain it. Ask “can I have a mailbox instead of forwarding?” and the answer comes with a card that does exactly that.

The assistant proposes, the person decides, and the site rules still applyCard offeredby Claude, underits answerServer checksallowed for thisperson? two at mostPerson fills inthe details,or cancelsConfirm windowwhat happens, theprice, any warningSite’s own APIthe same as thepage, own sign-inTurnstile, rate limits, ownership checks and the operator’s approval all still apply
Every card is checked by the server, confirmed by the person, and run through the same API as the website’s own pages.
  • What it can offer: check whether a name is free, request a name, sign in with an email link and code, add, switch or remove a service, change where a website address points, list a site in the directory, switch between names, put an earlier site version back, sign out, or delete a profile.
  • The server vets every card first. Owner actions are offered only to the name’s owner, a request only for a name that can be had, and never more than two cards per answer. Cards are signed with their answer, so a card the server did not offer cannot be slipped into a conversation.
  • Nothing runs on the first click. Every card opens the site’s standard confirmation window: what will happen, the price from the one pricing file, that it waits for approval, and any warning. Removing a service or deleting a profile is shown in red; what cannot be undone needs a tick, and deleting a profile needs the email typed back.
  • The card calls the same API the website’s own pages call, with the person’s own sign-in. Turnstile, consent, rate limits, ownership checks and my approval of service changes all still apply, because nothing about them is new.
  • Some things stay off the chat entirely. Publishing, deploy keys, connected apps and server-code secrets are links to /account, never cards.

How each card ended (done, failed or cancelled) goes back with the next question, so the assistant knows whether its suggestion worked. What is kept is minimal: only questions the guide could not answer, or answers someone marked unhelpful, with email addresses, keys and passwords redacted, for 90 days. Those are the to-do list for the guide.

09How we keep people safe

A family domain is a trusted brand in miniature: if wilssens.com sends you a link, you click it. That made security a feature, not an afterthought, and the platform went through a full audit on 10 October.

Four kinds of protection hold up one promiseA family domain people can trust with their nameChildrenadults-only directoryhidden contact listno photo locationsPhishingrisky forms heldReport this pagesigned mailAccountsno emailed passwordssingle-use linksper-site AI accessPrivacytotals, never listscookieless analyticsredacted help logsFoundation: written rules, enforced in the database, tested on every push
The platform-wide rules come first, then what they mean for children, phishing, accounts and privacy.
  • Signing in is not owning. People sign in with Zoho, or with a one-time email link or code. Every action on a name then checks ownership separately, so a relative who requested a name for someone else sees only their own request.
  • Owners’ code never runs on the main site. Every personal site lives on its own subdomain, and drafts are previewed there too, never on www, where a script could act with the owner’s account.
  • A name changing hands starts clean. Deploy keys, AI app connections, sessions and old versions of the previous owner are removed or hidden.
  • No credentials in plain text. Secrets stored in the database are encrypted; sign-in tokens and deploy keys are stored only as hashes and shown once.

Children and households

Some members are children, so the defaults assume a child could be on the other end.

  • The family directory is for adults. A child’s site is listed only after a parent or guardian agrees, and only I can switch that on.
  • The Zoho organization’s contact list is hidden from every user, admins included. Zoho cannot hide one person, so nobody sees anyone.
  • A parent can pay for a child’s address but cannot read its mail or give the name away.
  • Photos added in the page maker are re-encoded in the browser, which strips location data before anything is published.

Phishing and impersonation

The domain carries the family’s name, which is exactly what a scammer would want to borrow.

  • Pages with a sign-in form naming a bank or mail provider, or a form sending data to another site, wait for a person to approve them. In the admin they are shown as text, never rendered, so a bad page never runs with admin rights.
  • Every hosted page carries a small “Report this page” line that reaches a real inbox.
  • Mail is signed and checked (DKIM, SPF, DMARC), so others cannot easily send as @wilssens.com.
  • The main site cannot be framed by other sites, which blocks click-jacking tricks.

Accounts and passwords

  • A new mailbox’s temporary password is never emailed. The email holds a one-time link that shows it once, within 14 days.
  • Sign-in links open a page with a button rather than signing in directly, because mail scanners click every link they see. Links last 15 minutes and work once.
  • Sign-in codes, requests and searches are rate-limited, and a wrong guess uses up a try before it is checked.
  • Every credential expires: browser sessions after 30 days, AI app and command-line connections after a year (30 days for admins), deploy keys after 30 to 365 days. Owners can end one or all with “Sign out everywhere”.
  • In an incident I can end every sign-in for one site, one person or everyone, and switch off AI apps, the command lines or deploy keys separately, in seconds. Websites, email and /admin keep working.

Privacy

  • The only public numbers are totals. There is no list of names or addresses anywhere public, and analytics use no cookies.
  • The help assistant keeps only questions it could not answer well, redacted, for 90 days, with no name, email or IP address.
  • Name searches are tied to a visitor code that changes daily and are deleted after 100 days.

None of these are exotic. The work was in applying them consistently across every endpoint, and writing them down so future changes keep them.

10Running it: one person, one dashboard

The operator is one person, so the admin is built to answer “what needs me?” first. Its overview shows headline numbers and a Needs you list: new requests, service changes, versions held for review, sites asking to be featured. A bell in the site header shows the same counts wherever I am.

  • Every name, with its owner, email and website, plus a live check that visits each subdomain and a cross-check against Zoho for accounts nothing explains.
  • Traffic per hostname, from Cloudflare’s analytics, saved daily so history outlives Cloudflare’s 31-day window.
  • Storage: site files, database size, and each mailbox.
  • Server code capacity: what every site uses this month against the plan’s included amounts, and what it would cost if every site used its full limit.
  • The help assistant: questions, cost per day against the budget, answer speed, and the questions the guide could not answer.
  • Demand: searches, requests, referrals and the funnel between them.

Cost discipline is built into the code, not just watched. A hosted page costs two storage calls whatever the site’s size, analytics queries are cached because each takes 10 to 25 seconds, and every AI and server-code feature has a hard limit.

11Experiments, measured with PostHog

wilssens.com began as an experiment, and the habit stuck: every feature ships with the events that will show whether anyone uses it.

Every feature ships switched off, gets measured, then gets decidedShip it darkbehind a switch, orfor a few sites firstName the eventskinds and counts,never text or namesRead it in /adminHogQL funnels,cached in D1Decideswitch on, change,or take it outthe next idea starts the loop again
The experiment loop: ship it switched off, name the events, read them in the admin, decide.
  • Ship it dark. Payments, live mailbox creation, server code and the help assistant each have a switch. Server code is allowed site by site, and the help assistant can be open to everyone, to account holders only, or off.
  • Name the events up front. The name search is a funnel: search, available or taken, plans viewed, request started, request sent, shared, referred. The page maker records which of the five ways people build with, which blocks and templates they pick, and how often they ask their AI app for a change. Every event carries the page’s language.
  • Count what matters where ad blockers can’t hide it. A sent request and a request from a share link are recorded by the server, so the key numbers stay honest.
  • Read the results where decisions are made. The admin queries PostHog with HogQL, one part at a time, saves the results in the database and refreshes them in the background, so the page opens instantly.
  • Measure without watching people. No cookies, no autocapture, no session recordings, no surveys. Events hold kinds and counts, never names, text or addresses, and my own browsers are never counted.

The result is a site where “should we keep this?” has a number behind it. PostHog’s feature flags and A/B tests are the natural next step once there are enough visitors for a test to mean something.

12Best practices

  1. Write the rules down, then make the database enforce them. “A name is never given away during quarantine” is a trigger, not a code review comment. Rules that live only in people’s heads do not survive the fiftieth feature.
  2. One function per outcome. Approving and paying both call approveRequest(); five ways of building all call publishSite(). Every safety check lives in exactly one place.
  3. Design for both ends of the family. The same site serves a grandparent who wants a mailbox that just works and a teenager deploying from GitHub. Each gets their own path, and the paths meet at the same rules.
  4. Restrict AI by architecture, not by instructions. A prompt can be talked around. A model that can only offer cards, with signed answers and a server that has the last word, cannot do much harm.
  5. Keep a living record of decisions. The project has one document that states every settled decision, with its date and its reason. I built this with Claude Code, Anthropic’s AI coding tool, and that document is what kept 179 commits consistent: every session starts by reading it, and no settled decision gets re-argued.
  6. Small, boring infrastructure scales down beautifully. No servers, no containers, a database that is a file. Nothing to patch, and almost nothing to pay while it is quiet.

13What’s next

For now, wilssens.com serves my immediate family, and every name on it is free. That is the right size for it today.

The platform is ready to grow if the need ever scales beyond us. Payments through Stripe are already built and switched off; turning them on means live keys, tax and terms, not new code. Pricing, waiting lists and referrals are in place too. If more Wilssens find their way to the site and ask for a name, the platform can welcome them, and pay its own way, without a rebuild.

If your name is Wilssens, search for yours.