I built an internal comms tool at 17. Beekeeper was the second one.
Looking for something else in my Drive, I found a 28 KB zip from 1999. Inside: 823 lines of Java, two UDP ports and six text files, written to stop phone messages getting lost in my father's five-person office in Mexico City. I didn't remember writing most of it. The data model is the one I'd spend the next fourteen years of my life on.
PciCheck.java 05.08.1999 · Server.java 14.11.1999 · Pci.java 19.02.2000
The problem was the handoff
My father ran a business in Mexico City. Five people: him, Tere, Rita, Gaby and Mary. The phone rang all day, and the person it rang for was usually not at their desk — in the warehouse, with a client, out.
So somebody took the call, wrote it on paper, and walked it over. Sometimes. The note got lost, or it landed on a desk nobody came back to that afternoon, or two people took the same call and neither knew.
I was seventeen and I had just learned sockets. So I built the thing that was in front of me.
The product was never really about messages. It was about a handoff: somebody has information, and the person who needs it is somewhere else.
What it looked like
One window. Pci.java lays it out with setLayout(null) and positions every single control by hand with setBounds(x, y, w, h) — so this reconstruction isn't from memory or a screenshot. It's drawn from the coordinates in the source.
Pci.java · JFrame “PAG” · 550 × 700 px · rebuilt from the setBounds() calls
The notification. Fired by an applet, not by a refresh.
Server.bat · the whole backend, printing to a DOS window
The password label reads “Contaseña”. It has been wrong for twenty-six years and it is reproduced exactly.
Not decoration. It's a Java applet embedded in the window, holding a socket open, waiting to be told something arrived.
passwords[] = {"","","","",""}. Five empty strings. The auth flow was built. The passwords were never set.
How it worked
No database. No framework. No HTTP. A server holds one UDP port open; a second port exists purely so the recipient finds out without looking.
Eight bytes of header, by hand
No JSON. No XML. No serialisation. The entire message is one string inside a 256-byte datagram, and fixed character positions are the contract.
// Pci.java — the line that builds the packet
msg = codeArray[codeTo] + codeArray[userIndex] + tipo + messageToSend;
sendProto.sendMsg(msg);
// WriteThread.java — the line that takes it apart, across the wire
msg = data.substring(8);
codeTo = data.substring(0,3);
codeFrom = data.substring(3,6);
typCode = data.substring(6,8);
The nine call types are a portrait of the business
This is the part I'd forgotten, and it's the part that matters. These categories aren't generic. Every line was a thing that actually happened in that office, often enough to deserve its own radio button.
| Code | Type | What it was |
|---|---|---|
| 00 | Mensaje interno | Desk to desk |
| 01 | Llam. Personal | Family, things from home |
| 02 | Llam. CNLT | One account important enough to get its own category |
| 03 | Llam. Proveedores | Suppliers |
| 04 | Llam. Filatelia | Stamps. The business itself, on its own line |
| 05 | Llam. Ventas | Sales |
| 06 | Llam. Cliente | Customers |
| 07 | Llam. Admon | Admin |
| 08 | Otros | Everything else |
| 09 | No especificado | Not in the interface — the server's fallback when the code arrives broken |
What I got wrong
A lot, and it's worth being specific, because the bugs are more interesting than the parts that worked.
UDP. Fire and forget. If a datagram was dropped the message simply never existed, and nobody found out.
Anything past the buffer was truncated with no warning to the sender.
i and k are instance fields, and the lookup loops start wherever they were left. It only worked because every message got a fresh WriteThread.
WriteThread and notify each declare their own ipArray — and they don't match. Whoever it notified, it wasn't always who it filed the message under.
To get the next message number it reopens the file and parses substring(4,8) of the first line. The database was the formatting.
Four wrong passwords called System.exit(0). All five real passwords were the empty string.
Version 0.1
In 2011 I co-founded Beekeeper. The pitch, cleaned up for investors, was a communication platform for the people who don't sit at a desk — factory floors, hotels, hospitals, retail. It ended up serving 1,500+ customers in 130+ countries before the exit.
The data model on the whiteboard in 2012 was: recipient, sender, type, timestamp, body, a mailbox per person, an archive, an audit log of everything, and a notification that reaches someone who is not looking at the app.
That is a description of WriteThread.java, written in 1999 by someone who had never heard the phrase “frontline worker”.
I'm not claiming foresight. I'm claiming the opposite: I didn't invent that problem, and neither did the market. It was already sitting in a five-person office, obvious enough that a teenager with a socket tutorial could see it. The reason it took another twelve years and a company is that seeing the problem was never the hard part.
In 1999 I was competing with a paper note walked across an office. Fifteen years of Beekeeper and fifteen hundred customers later, I was still competing with the paper note.
The other thing I take from the zip is less flattering and more useful. The 1999 version has no acknowledgements, no retries and a silent truncation at 256 bytes — and it ran in that office for months. Nobody complained, because the alternative was a paper note. Shipping something honest and narrow beat shipping nothing, and it beat waiting until I understood TCP.
That's still how I work. It's rule one on the workbench: if it hasn't run in production against real life, it's an opinion.
The companion piece to this one is about the other thing I found in that Drive folder — the Jini specification, dated December 1998 — and what happened when I finally built the house it described: Jini, 1999 → the house, 2026.