About this app
A bulk email tool built for the Communications Team at Wycombe Abbey International School Bangkok. It writes to families and staff from the school's own contact records, sends every message individually, and keeps an account of exactly who received what. This page explains how it was built, by whom, in what time, and what it replaced.
A member of the Communications Team writes a newsletter out of blocks — a banner, text, story cards, buttons, an events list — picks who it goes to, sends a test to themselves, and presses Send. Every recipient gets their own separate email, addressed only to them.
Underneath that, four things are happening that a general-purpose email tool does not do. The audience is a live query against iSAMS, the school's contact database, so a pupil who enrolled this morning is already in the Year 7 parent list this afternoon. The message is compiled into email HTML that survives Outlook, Gmail and iOS Mail rather than into a web page that happens to be emailed. It sends through the school's own Microsoft 365 tenancy, so the message comes from communications@wycombeabbey.ac.th and never leaves the school's systems. And afterwards it reads the bounce reports back out of that mailbox, works out which address failed and why, and tells you which parent to ring.
Monkey Mail was Rolly Alfonso-Maiquez's idea. He is the school's Head of IT, and it came from working alongside the Marketing and Communications team rather than from a brief handed down to them.
The pain points were ones he had heard about, watched and run into himself: the work of getting a single message reliably to every parent, and to external contacts, and to all staff, using tools that were never designed for a school's contact records or a school's obligations. Knowing which of those frictions were worth building around — and which were just how email is — is the part that could not have been specified from outside.
He also came to it with practice. By September 2026 he had built more than twenty KEYSTONE apps for the school, so the first conversation started from a realistic idea of what a working first version needed to do and what could wait.
And he brought a method. BUILD is a five-step design-thinking framework for turning a school problem into something that works — Begin with a real problem, Understand the need, Iterate with confidence, Launch a useful first version, Develop it with others. This build followed it closely enough to be a fair demonstration of it: it began with one frustration, it was clarified with the people who would use it, it was corrected many times in the space of a day, it launched as a working link that had sent to real families, and it is now being developed with the team that sends from it.
The BUILD model: rolly-folsea-2026.ateneo.workers.dev — Dr Rolly Alfonso-Maiquez, presented at FOLSEA 2026, Jakarta.
This was built in the open-ended, conversational way people now call "vibe coding" — describing what was needed in plain language, watching it get built, using it immediately, and correcting course. One person working with Claude Code, in one continuous conversation.
It opened at 10:59 in the morning of 18 September 2026, and not with a request for code. The first message set out what the app was for, who would use it, and — the part that mattered most — how it should be structured: a separate Worker rather than another module bolted onto App-25 Pupil View, because only the Communications Team would use it and Pupil View should not have to carry it. The audience would come from Pupil View's existing iSAMS binding instead of a second connection to the school's records. And the editor had to be, in his own capitals, ÜBER GOOD — good enough to take an exact paste from Word with the images intact. That message ends with two words: "Q&A ME".
The first commit came four and a half hours later, at 15:25. Those four and a half hours were spent deciding things, and every structural decision taken in them is still standing: the separate Worker, the service binding, the single-recipient send. About forty-five minutes after that first commit the first test email arrived in a real inbox. That evening a real campaign went to two people. The following morning it went to fourteen Nursery families, and just after eleven it went to three hundred and forty-four.
Nothing in that sequence was a rewrite. The shape of the app changed constantly — story cards grew multiple buttons, campaign types acquired their own unsubscribe rules, the preview split into phone and desktop, the whole type system was re-derived from a single base size after a long argument about what a newsletter should look like on a phone — but the send path written in the first hour is the send path that delivered all 360 emails.
The work was not linear and it was not passive. A large share of it was the product owner using the thing, disliking what he saw, and saying so. "REALLY?????????" is a real message in the transcript, about a reading-time estimate that claimed five words took a minute to read. "waste of space!" is another, about a settings panel. "it's like another language ???" is what produced the plain-English notice that now sits above the font controls. The app is noticeably better in the places where the feedback was bluntest.
| When (Bangkok) | What happened | Evidence |
|---|---|---|
| 18 Sept, 10:59 | Ground zero — the first message: what to build, and how it should be structured | conversation log |
| 18 Sept, 15:25 | First commit — auth, audience binding, send queue | git history |
| 18 Sept, ~16:10 | First test email to a real inbox | audit log |
| 18 Sept, 18:01 | First real send — 2 recipients, 2 delivered, 4 seconds | campaign record |
| 19 Sept, 11:07 | Nursery send — 14 of 14 delivered in 10 seconds | campaign record |
| 19 Sept, 11:31 | Whole school — 344 of 344 delivered, 0 failures, 16m 54s | campaign record |
| 19 Sept, 12:30 | Latest commit — resend-to-failures and pre-send link checking | git history |
Twenty hours and twenty minutes from the first line of code to the whole parent body; twenty-four hours and fifty minutes from the first message. Not that many hours of work — the clock includes a night's sleep — but that much calendar. All times are Bangkok, which is where every one of them happened.
The calendar span flatters the build, because it contains a night. What the conversation log records is 6,138 timestamped events across six sittings, and those sittings can be measured rather than estimated.
| Sitting (Bangkok) | Length | What it was |
|---|---|---|
| 18 Sept, 10:59 – 11:32 | 0h 33m | The opening Q&A — what to build, and how to structure it |
| 18 Sept, 13:43 – 16:44 | 3h 01m | Scaffolding, the audience binding, the send queue, the first test email |
| 18 Sept, 17:31 – 20:40 | 3h 09m | The editor and the compiler; the first real send, to two people |
| 18 Sept, 21:25 – 23:02 | 1h 37m | Evening |
| 19 Sept, 00:16 – 00:55 | 0h 39m | Past midnight |
| 19 Sept, 06:06 – 13:32 | 7h 26m | Both bulk sends, the reporting work, and the writing of this page |
About 12 hours of active work, or 16½ hours counting whole sittings end to end, inside a 26½-hour calendar span. The 12-hour figure excludes any gap longer than ten minutes, the ordinary convention for active time; the 16½-hour figure measures each sitting from its first event to its last. Both come from timestamps rather than recollection. An earlier version of this page estimated 25 to 30 hours — that was a guess, and it was roughly double.
The shape of that table says more than the total. One sitting ends at 00:55 and the next begins at 06:06, and two of the six fall squarely outside a working day — which is the concrete version of the point made further down about why none of these hours carry a price.
The build figures above are regenerated from git every time this page is deployed — last updated 19 September 2026 at 19:14 Bangkok — so they describe the app as it stands rather than as it stood when this was written. The production figures below were read from the live database on 19 September 2026 and are refreshed by hand. None of it is an estimate unless the page says so.
The product owner has a way of describing how a build actually goes, and it is worth setting the record against it, because this build followed it almost to the letter.
"it takes just a little time to get from your idea (zero level) to 80% and you've basically got a working app. then the next 10% takes longer since this is your big edits, big tweaks. then the last 9% is further tweaking. i feel i never really reach 100% since there's always something to tweak so i don't aim for perfection and finality — just to a point that users are happy, functions are working, things are working well."
"once i reach 80/85% — i test with colleagues to figure out how people use them and see what can be improved, then i go back to vibe coding and then test and have others test again."
Seven hours after the first message, and about four hours of actual work into it, the editor existed, the compiler produced an email that survived Outlook, and a real campaign went to two real people. That is "you've basically got a working app", and it arrived roughly when his model says it does.
What follows 80% in his description is not more building, it is testing with colleagues — D, develop it with others. That is what the record shows: twenty-eight test sends, tests to colleagues and to demonstration parents, and a review by the Marketing and Communications colleague who would actually be doing the sending, run over LINE. It is also where six of the nine defects on this page were found. Not one of them was found by reading code.
The "big edits, big tweaks": the whole type scale re-derived from a single base size after a long argument about what a newsletter should look like on a phone, the preview defects, the confirmation gate, campaign types carrying their own unsubscribe rules. Then the thing it was built for — 344 of 344 delivered, no failures.
The "further tweaking", almost all of it prompted by what a real send revealed rather than by anything anyone planned: bounce classification that tells a full mailbox apart from a wrong address, human clicks separated from a link scanner's, a send report that names who to ring, a standing list of problem addresses carried between campaigns, resend-to-failures, and link checking before send.
By his own account there is no such point: "there's always something to tweak so i don't aim for perfection and finality — just to a point that users are happy, functions are working, things are working well." By that definition Monkey Mail was finished at 11:48 on 19 September, and has been improved every hour since.
The number that makes the model convincing is the split. The first 80% took about four hours of work. Everything after it has taken about eight more, and is still going. The last fifth of this app has cost twice what the first four fifths did — which is exactly the shape he describes, and the opposite of what "it was built in a day" suggests to most people.
Those percentages are a reading of the record against his own model, not a measurement — there is no scale that tells you an app is 90% done. The times and the hours behind them are measured; the labels are a judgement.
Quoted from rolly-folsea-2026.ateneo.workers.dev/about, where the wording is published in full alongside the question that prompted it.
Every so often the product owner stops and asks where the build actually is. The rule for answering is that nothing gets estimated if it can be counted — and both times so far, the counted answer has differed from the felt one.
The honest answer, that afternoon, was that the app could not say. sent meant
Microsoft had accepted the message and taken responsibility for it. Bounces supplied the
negative signal, but silence from a receiving server is weak evidence: a message quietly
dropped, or filed straight into a junk folder, looks exactly like one read over breakfast.
Checking that answer properly turned up two things. The first was a defect. Verifying the report against the database rather than reading it back showed the engagement figures were wrong — the report claimed 253 clicks by people and no machine activity, when the database held 45 human clicks and 210 by a link scanner, correctly classified all along. Every click query was missing the filter its sibling open queries had, and it had been naming a scanner in Hong Kong as the first person to read the newsletter, nought seconds after send.
The second was that the question had become answerable since the last time anyone checked. Microsoft had released a message trace API on Graph, generally available since January 2026, supporting an application permission — which is exactly how this app authenticates. It was built that afternoon. The first real run appeared to fail: the browser gave up waiting, because the code was issuing one database write per recipient and a whole-school send meant 344 of them in sequence. The data had landed perfectly well. Only the patience had run out.
| Exchange status | Recipients | What it means |
|---|---|---|
| delivered | 338 | Reached the recipient's mail server |
| failed | 5 | Exchange could not deliver it — the same five as the bounces |
| pending | 1 | Still being retried; Exchange keeps trying for two days |
| filed as spam | 0 | Nothing was accepted and then quietly junked |
Read from Microsoft's own message trace at 16:42 Bangkok on 19 September 2026 — the authoritative record of delivery to each recipient's mail server, independent of anything this app recorded.
The five failures matched the five bounces exactly. Two records kept by different systems, for different reasons, agreeing to the address — which is a far stronger statement than either could make alone. And the nought filed as spam is a number that could not previously be reported at all, honestly or otherwise.
The pattern is worth naming: a question the app could not answer became a feature, and the act of checking the old answer found a defect on the way. Neither would have happened if the question had been answered from memory.
Two parts to the question, and the record disagreed with both.
On the hours. It felt like forty. It was not.
| Measure | Hours |
|---|---|
| Calendar span, first message to now | 31.9 |
| Whole sittings, end to end, across nine of them | 17.4 |
| Active work, gaps over ten minutes excluded | 13.1 |
Thirteen hours. It feels like forty because it crossed two days and a midnight, which is exactly why the question was worth asking rather than answering from memory.
On the 95%. The sending engine is there or close to it: 360 emails, no failures, two real sends, and Microsoft's own message trace independently confirming 338 delivered and nothing filed as spam. Bounces classified, households reachable, links checked, engagement honest about machines.
The product is not, and the gap turned out not to be code at all. The audit log answers the question nobody had asked — who has actually used this?
| Role | What the audit log shows |
|---|---|
| Admin (the product owner) | Everything — three campaigns, three sends, twenty-eight tests |
| Sender — the colleague whose job this is | Signed in once. Nothing since. |
| Editor | Has never signed in |
Both real sends were pressed by the person who built it, not the person whose job it is. The colleague who will do the sending reviewed and edited the September newsletter, over a chat app — but has never created a campaign in Monkey Mail, never run a test send, never seen the confirmation dialog, and never pressed Send.
The BUILD model above is explicit about what follows 80%: test with colleagues and watch how they use it. That happened for the newsletter's content and not for the app. And every defect listed on this page was found by a person looking at real output — not one by reading code. So the largest untested surface left is not a missing feature. It is a trained user operating the thing unaided.
Which puts the honest figures at roughly 99% for the engine and 90% for the product, with most of the remainder discoverable only by watching someone else send the October newsletter start to finish while the person who built it sits on their hands and writes down every hesitation. That is the next action, and it will produce more improvement in an hour than a day of code review would.
Pulse checks get added here as they happen, with whatever they turned up, including the parts that were uncomfortable. A check that only ever confirms the current belief is not a check.
"Vibe coding" invites the assumption that someone described a wish and a machine granted it. That is not what the record shows. The code was written by Claude; almost everything that made the code correct came from the product owner.
Product owner and lead designer is the accurate description of the role Rolly Alfonso-Maiquez held here, and both halves matter. As lead designer he decided what the thing should look like and how it should behave — the block model, the phone-first preview, the type scale, the wording of the warnings. As product owner he did the job that in a larger team nobody else is allowed to do: he set the scope, ranked what got built next, gathered and weighed feedback from the colleagues who would use it, and accepted or rejected the work. Someone has to decide that a thing is good enough to send to 344 families. That decision was his, and he made it only after watching fourteen arrive.
Two rules were stated at the outset, in capitals, and never relaxed: never send as CC, because families must not see each other's addresses; and never send to anyone without being told to. Both are now enforced in code and guarded by tests. The send function takes exactly one address and has no code path that can attach a second. Those rules did not emerge from the build — they constrained it from the first hour, which is why they hold.
Given a working app and 344 waiting families, the decision was to send to fourteen Nursery families first and look at the result. That was his call, not a suggestion he accepted. It surfaced a case nobody had modelled: two parents with one child in Nursery and another in a different year group, who would have received the same newsletter twice. The app now detects the overlap before every send and offers to skip the people who already have it. A single-shot send to 344 would have found that bug by annoying two families.
Nine defects survived into something a parent could have received, or were only findable in real data. Six of the nine were caught by the product owner, looking at output.
| Defect | Why it mattered | Found by |
|---|---|---|
| "Desktop" preview showed the phone layout | The desktop preview had never once been correct | "do i actually get a preview of laptop view?" |
| Dozens of phantom dividers in the preview | App styling was bleeding into the email preview | "why bazillions of horiz dividers?" |
| Preview restyled itself on hover | Same cause, different symptom | "before i hover and after i hover. WHY?" |
| Editor misreported its own font sizes | A 15px block rendered larger than a 16px one | "TEXT 1 = 16px and STORY CARD = 15px but looks larger?????" |
| Every timestamp was seven hours out | UTC shown unlabelled as if it were local time | "it's 10:01 AM in Bangkok" |
| 42 of 44 clicks were a link scanner | Engagement figures were largely fictional | "clicked data is good — but from where and when?" |
| Subject never reached the email banner | Affected every campaign ever created | investigation prompted by a question |
| Preheader text was never actually sent | Same root cause, found in the same pass | investigation prompted by a question |
| An empty gold button shipped in the newsletter | Standalone buttons were never validated | found while repairing the live campaign |
Scroll the table sideways to see every column.
Four further defects were introduced by Claude during the build and corrected the same day, three of them after the product owner pushed back on a confident claim that turned out to be wrong. One of those was the assertion that email clicks cannot be faked. They can. Forty-two of them were.
Twenty-eight test sends before the first bulk send. Tests to himself, to colleagues, and to demonstration parents. Approval ran through a LINE chat with the Marketing and Communications colleague who does the sending, who reviewed and edited the September newsletter before it went out. Before pressing Send on 344 he asked whether the window had to stay open, whether the laptop had to stay awake, whether both parents would each get their own copy, and whether the comma-separated list on the confirmation screen meant the addresses would be visible to each other. Every one of those is the right question. The last one is the question that would have caught a CC bug if there had been one.
Told that user roles had never been configured, he replied "huh?" and produced the table showing all three roles configured since 18 September. He was right. Shown sample data for a report, he spotted instantly that it had borrowed a real family's name and invented a spouse and a phone number to go with it, and said so at once. Fixing that exposed a real bug underneath: the household lookup was grouping contacts by failed address rather than by family. A fabricated example had been hiding a real defect.
The honest summary. The technical judgement in this build is Claude's. The product judgement, the risk judgement and the quality bar are his. He knew what the output had to look like to a parent on a phone, he refused to accept anything that looked wrong even when told the code was correct, and he would not let 344 emails go until he had watched fourteen arrive. That is the difference between a demo and a system that has sent to every family in the school without a single failure.
There is a pattern in the nine defects and it is worth naming, because it predicts where the next one will be.
Almost every defect lived at a seam between two components that were each individually correct. The editor knew the subject line; the compiler knew how to draw a banner; the subject never travelled between them. The preview knew it was 600 pixels wide; the email knew it switched to phone layout below 620; nobody had compared the two numbers. The app had a stylesheet; the email had a stylesheet; injecting one into the other drew dividers nobody asked for. The tracker knew how to rewrite links; the archive URL was a token that could not be resolved at click time.
Meanwhile the part everyone was frightened of — the send path, the code that could email the wrong thing to 344 families — has never produced a production defect. It got the tests, the review, the staged rollout and the paranoia, and it has a perfect record. The bugs went where the attention wasn't.
The practical lesson for anything built this way: test the joins, not the parts. A component you can describe in one sentence is usually right. A sentence that needs the word "and" to connect two components is where to look.
If the school had gone to a development firm and asked for an email platform that does what Monkey Mail does — the live iSAMS audience binding, the block editor, the email compiler, individual personalised sending through the school's own mail system, bounce classification and the reporting — this is roughly the shape of that project.
| Phase | What it covers | Roles involved | Typical time |
|---|---|---|---|
| 1. Discovery & requirements | Interviewing Communications and Marketing, defining audiences, campaign types and roles | PM, Tech Lead, Designer | ≈2 weeks |
| 2. Architecture & contact integration | Data model, Entra sign-in, the live iSAMS audience binding, consent and suppression model | Tech Lead, 2 Backend, DevOps | ≈4 weeks |
| 3. The email compiler | Blocks to HTML that survives Outlook, Gmail and iOS Mail; phone layout; brand templates | 2 Frontend, Designer, QA | ≈5 weeks |
| 4. The editor | Block editing, rich text, images, live phone and desktop preview | 2 Frontend, Designer | ≈5 weeks |
| 5. Sending infrastructure | Microsoft Graph sending, pacing, resumption, per-recipient personalisation, suppression enforcement | 2 Backend, DevOps | ≈4 weeks |
| 6. Deliverability & reporting | Bounce ingestion and classification, open and click tracking, machine filtering, send reports | Backend, Data, QA | ≈4 weeks |
| 7. QA, security & data protection | Access-control audit, PDPA review, deliverability testing across mail clients | QA, Security, DPO, all | ≈3 weeks |
| 8. Deployment & training | Rollout and training the Communications Team | PM, Tech Lead | ≈1 week |
Scroll the table sideways to see every column.
Run in sequence that is 28 weeks. With the usual overlap between phases, a team of about eight would ship a comparable version one in roughly six to seven months. Every one of those phases happened here too — compressed into two days and folded into one conversation instead of eight scheduled handoffs.
Three ways the school could have arrived at a bulk email tool. The third column is Mailchimp Standard, the tier that realistically matches this team's needs, included because a cost comparison that leaves out the option most schools actually take is not a comparison at all.
| Metric | This build | Commissioned team | Mailchimp Standard |
|---|---|---|---|
| Team size | 1 person, working with Claude Code | ≈8 people across 7 roles | None — you buy it |
| Calendar time to first real send | ≈20 hours | ≈6–7 months | A few days |
| Developer time | ≈25–30 hours | ≈4,700 person-hours | ≈20–40 hours to set up, then list maintenance for ever |
| Tooling cost to build it | ≈$2–3 of subscription time — see the breakdown below | Included in the labour figure | $0 |
| Cost of the developer's time | Deliberately not priced — see the note below | ≈$580,000 in fully-loaded labour | None |
| Cost to run, per year | ≈$0–60 of Cloudflare; sending is included in the school's Microsoft 365 | Same hosting, plus a support retainer | ≈$480 at list price, rising past 500 contacts |
| Five years of running cost | ≈$300 of hosting | ≈$300 of hosting, plus five years of support retainer | ≈$2,400 and upward as the roll grows |
| Cost to change something | Same day, in conversation | A change request and a quote | A roadmap request, or not at all |
| Audience stays correct by itself | Yes — a live query against iSAMS | Yes, if specified up front | No — a list someone maintains |
| Operational email with no unsubscribe | Yes, by campaign type | Yes, if specified up front | Not permitted |
| Where parent addresses live | Only in the school's own systems | Only in the school's own systems | Uploaded to Mailchimp |
Scroll the table sideways to see every column.
Why this page puts no price on the developer's time. Monkey Mail was built by the school's Head of IT, and the hours split into two kinds. The ones inside the working day are work, and they are accounted for as work. The rest — evenings and weekends — are something else: continuing learning, and the satisfaction of helping his own team get more accurate and more efficient at something they do every week.
Putting a rate on either would mean publishing a salary, which is nobody's business. Putting a rate on the second would also simply be wrong: hours given because someone wanted to keep learning are not hours the school bought.
So the comparison above is made in hours, not currency: roughly twenty-five to thirty of them against a modelled four thousand seven hundred. That is the honest unit, and it shows the difference perfectly well on its own. The commissioned-team column keeps a dollar figure because it prices a hypothetical team at published market rates — it is nobody's actual pay, least of all his.
Read that table honestly and Mailchimp wins the top half of it. It is far cheaper than commissioning a build, enormously faster than either alternative, needs no developer, and ≈$480 a year is not a number that troubles a school budget. Anyone arguing this project on cost alone is arguing a weak case.
The decision is made in the bottom four rows, and none of them are about money. Mailchimp is a fast, cheap way to get a bulk email tool. It is not a way to get this one — a tool where the audience cannot drift because nobody maintains it, where a fee notice cannot carry an unsubscribe link, and where no parent's address ever leaves the school. The build was not undertaken to save $480. It was undertaken because the $480 product cannot do those three things, and they are the things a school needs most.
None of these figures are invoices. This build was never billed by the hour, and the commissioned-team column is a modelled benchmark — a hypothetical staffing plan priced with real published salary data, not a quote from an actual firm. The person-hours and phase durations are this page's own assumption. The labour cost uses a blended fully-loaded rate of about $124 per hour, derived from 2026 Bay Area role averages loaded by the 1.4× multiplier commonly cited from US Small Business Administration guidance. The "this build" column is an honest approximation of unlogged activity, and its hours are the only figure in it worth leaning on. Both columns exist to make the comparison legible, not to the dollar.
The build ran on Claude Code, on a Team Premium seat. Anthropic prices a Team standard seat at $20 per user per month billed annually, or $25 billed monthly; a Premium seat is $100 annually or $125 monthly, and carries five times the usage of a standard seat. The seat costs five times as much and gives five times the room.
That seat is not a Monkey Mail expense. It is a standing subscription the school already pays for, shared across more than twenty KEYSTONE apps, and it would have cost the same this month if Monkey Mail had never been built. Charging the whole $100 to this one app would be dishonest. What can be attributed is the share of it this build actually used.
| Basis | Share of one month | At $100/mo annual | At $125/mo monthly |
|---|---|---|---|
| Usage allowance consumed | ≈1.8% | ≈$1.85 | ≈$2.30 |
| Elapsed time (under 30 hours of a 730-hour month) | ≈4.1% | ≈$4.10 | ≈$5.15 |
Scroll the table sideways to see every column.
Two ways of prorating the same seat. The usage line is the meaningful one — a subscription is consumed by how much of its allowance you draw, not by how long the laptop is open — and the time line is included because it is the figure people reach for first. They land in much the same place, which is reassuring rather than surprising. Call it two to three dollars of subscription time.
The usage figure rests on one measured number and one estimate, and it is worth being exact about which is which. The second day of the build — both bulk sends, roughly half the commits, the reporting work and this page — was read directly from the account and came to 4% of that week's allowance. The first day fell inside a weekly window that has since reset and can no longer be read, so it is assumed to be of the same order, giving roughly 8% of one week, or about 1.8% of a month.
The number that matters is not the two dollars. It is the 4%. Building an entire email platform, sending it to every family in the school, and writing this page consumed a single-digit percentage of one week's allowance on one seat. Capacity was never the constraint on this project, and no amount of extra subscription would have made it faster. The constraint was how quickly one product owner could look at something, decide it was wrong, and say why.
Role-rate sources: Indeed — Software Engineer, Levels.fyi — Software Engineer, Indeed — Product Manager, Built In — Engineering Manager, Indeed — UX Designer, Glassdoor — Software QA Engineer, Indeed — DevOps Engineer, and the 1.4× fully-loaded-cost multiplier commonly cited from SBA guidance. Hosting: Cloudflare Workers pricing. Claude Team seat pricing and the 5× Premium usage multiplier: claude.com/pricing, checked 19 September 2026; the 4% weekly-allowance figure was read from the account the same day. All figures pulled September 2026; salary data moves, so treat them as directional.
It was written in the last sitting on the table above, on the afternoon of the day the newsletter went to every family — which makes it the only part of this build documented while the thing it describes was still warm.
That was deliberate. Most of what is on this page could not have been reconstructed a month later: which question found which defect, what the confusion actually was before it was resolved, how long a sitting ran, the fact that one ended at 00:55 and the next began at 06:06. Written in arrears it would have become the tidy version, in which everything was foreseen and nothing was wrong for long. The defect table in particular only exists because it was written down while it was still slightly embarrassing.
So this is a log as much as an explanation, and it is kept rather than finished. The build figures above — commits, files, lines, tests — are regenerated from the repository every single time this page is deployed, so they describe the app as it stands today and not as it stood when the first version of this page was written. The production figures are refreshed by hand and carry the date they were read. Anything estimated says that it is estimated.
Which is the same idea as the section above, applied to the page rather than the app. If the app is never at 100%, an account of it cannot be finished either. This one is expected to keep changing, and every deployment updates it.
A single Cloudflare Worker, with SQLite for the campaign records, object storage for images, and a cron trigger that drains the send queue a batch at a time and sweeps the mailbox for bounces every five minutes. Sending is Microsoft Graph against the school's own tenancy. The audience comes from App-25 Pupil View over a direct service binding — an in-process call with no URL, unreachable from the internet, which cannot write to Pupil View's data. Monkey Mail holds no copy of the pupil database and has no iSAMS credentials of its own.
No IP address is ever stored. Open and click tracking records the country, city and connecting network that Cloudflare reports, and nothing that identifies a device. That was a deliberate choice, made before the tracking was built.
A school sends a great deal of email to parents, and almost none of it fits what commercial email marketing software is built for. The three options available before Monkey Mail each failed in a different way.
iSAMS holds every parent and staff address the school has. It is the single source of truth for contacts, and it is correct, because it is the same record the school runs on. It can also send email. But sending a newsletter through it is cumbersome, and it cannot format one. There are no blocks, no story cards, no banner, no phone layout, no brand. A newsletter sent from iSAMS looks like a database sent it.
The obvious workaround is to maintain groups in Outlook and send to those. This means building a second contact database alongside the real one and keeping it in step by hand. It starts drifting the moment it is created. A pupil enrols and nobody adds the parents. A family leaves and keeps getting fee notices. Somebody maintains it carefully for a term and then goes on leave. It is not sustainable, and it is not sustainable in the specific way that matters: the failure is silent, and the people affected are parents.
Mailchimp does the formatting beautifully. It costs money, it holds the school's parent data on its own servers, and — the part that turned out to matter most — it imposes its process on the school rather than supporting the school's. The next section covers that in detail, because the cost is the argument people expect and it is not actually the strongest one.
The design decision that follows from all three. An audience in Monkey Mail is not a list. It is a question, asked live against iSAMS at the moment you send: "who are the Year 7 parents?" Nobody maintains it, nobody can forget to update it, and it cannot drift, because there is nothing to drift from. The school's single source of truth stays the single source of truth.
Mailchimp is a good product. It is a good product for a business sending marketing email to people who chose to receive it. A school sends two quite different kinds of email, and only one of them is that.
| Capability | Monkey Mail | Mailchimp Essentials | Mailchimp Standard | Mailchimp Premium |
|---|---|---|---|---|
| Where the contact list comes from | A live query against iSAMS | A list you import and maintain | A list you import and maintain | A list you import and maintain |
| Keeping it current when a pupil enrols | Automatic — nothing to do | Manual, or a paid integration | Manual, or a paid integration | Manual, or a paid integration |
| Where parent addresses live | Only in the school's own systems | Uploaded to Mailchimp | Uploaded to Mailchimp | Uploaded to Mailchimp |
| Operational email with no unsubscribe | Yes — required by campaign type | Not permitted | Not permitted | Not permitted |
| Who the message is from | The school's own Microsoft 365 mailbox | Mailchimp's servers, on your behalf | Mailchimp's servers, on your behalf | Mailchimp's servers, on your behalf |
| People who can use it | Unlimited, no per-seat fee | 3 seats | 5 seats | Unlimited |
| Roles | 3, built for this team: admin, sender, editor | 2 | 4 | 5 |
| Bounce follow-through | A standing list across campaigns, with the household's other contacts and phone numbers | Per-campaign bounce list | Per-campaign bounce list | Per-campaign bounce list |
| Bot and scanner filtering in the figures | Human and machine activity reported separately | Some filtering, not surfaced as a split | Some filtering, not surfaced as a split | Some filtering, not surfaced as a split |
| Custom-coded templates | The whole editor is purpose-built for this school | Can create, must upgrade to send | Yes | Yes |
| Time to change a structural workflow | Same day, in conversation | A vendor roadmap request | A vendor roadmap request | A vendor roadmap request |
Scroll the table sideways to see every column.
The row that decides it is the unsubscribe row. Mailchimp requires an unsubscribe link in every campaign and does not allow an outside unsubscribe process; if the merge tag is missing it adds a footer to the email itself. That is the correct policy for marketing email and it is the right thing for a bulk sender to enforce. It is also incompatible with a school sending a fee reminder, a closure notice or a safeguarding message — email a parent cannot opt out of, because the school has a duty to reach them.
Monkey Mail makes this a property of the campaign type. Marketing campaigns carry an unsubscribe link. Operational campaigns do not, and cannot. The type is chosen before the content is written and the rule is enforced by the app, not by remembering. That single distinction is the clearest example of the general problem: with Mailchimp the school follows the software's process, and here the software follows the school's.
The school has roughly 500 contacts — 344 parent addresses plus staff, growing each intake. Volume is not the constraint; one to four newsletters a month sits comfortably inside any tier's sending allowance. What drives the price is the number of contacts and the number of seats.
| Option | Year one | Ongoing, per year | What you get for it |
|---|---|---|---|
| Monkey Mail | No development invoice | ≈$0–60 of Cloudflare; sending is already paid for inside Microsoft 365 | Everything on this page, and the source code |
| Mailchimp Essentials | ≈$156 promotional | ≈$312 at list price, rising with contact count | 3 seats, 2 roles, cannot send a custom-coded template |
| Mailchimp Standard | ≈$240 promotional | ≈$480 at list price, rising with contact count | 5 seats, 4 roles — the realistic tier for this team |
| Mailchimp Premium | ≈$4,200 promotional | ≈$8,400 at list price | Unlimited seats, phone support |
Scroll the table sideways to see every column.
Mailchimp prices are the published rates for the 0–500 contact tier, checked on 19 September 2026, and rise once the school passes 500 contacts. The first-year figures reflect the introductory 50% discount; the ongoing figures are list price. Mailchimp offers a 15% discount to nonprofits and charities on request, which is not applied in this table. Overage charges apply if the contact or send limit is exceeded.
The money is real but it is not the decisive argument. Standard at roughly $480 a year is not what stops a school using Mailchimp. What stops it is that the parent contact list would have to be copied out of iSAMS and maintained somewhere else, parent data would sit on a third party's servers, and the school could not send an email that a parent is required to receive.
Sources: Mailchimp — official marketing pricing page, Mailchimp — Why We Require an Unsubscribe Link, and Mailchimp — Add an Unsubscribe Link (the unsubscribe footer is added automatically if the merge tag is absent). Checked 19 September 2026; vendor pricing and tier gating change without much notice, so re-confirm before relying on any single cell.
One. Never CC. Never BCC. One message, one recipient. Families must never see each other's email addresses. Every campaign sends a separate message to each person, addressed only to them, personalised to them. This is not a setting and there is no way to turn it off — the function that sends a message accepts exactly one address and has no parameter for a second. A test guards it. When the confirmation screen lists recipients separated by commas, that is a list for the sender to read, not the To line.
Two. Nothing is ever sent unless a person presses Send. No automatic sends, no schedules that fire on their own, no "helpful" retries to real families. Test sends go only to staff addresses. Every real send shows a final, unambiguous confirmation naming the exact number of recipients and stating that it cannot be cancelled once started — at any size, including fourteen.
Those two sentences shaped the architecture more than any technical decision did. They are also the reason the send path has never failed: it was the most constrained part of the system from the first hour, and constraints are what get tested.
A recurring theme in this build was refusing to report a number that sounds better than the truth.
Stated plainly, because a page like this is worth less if it only lists strengths.