Category
Last night at ODUG I showed a CRM I'd built to replace a hosted one I felt was clumsy and overkill for my needs. The app runs on my phone; I can search my customers, tap a phone number to call, tap an email to write a message, read a dated history of every customer, and build a BCC email list for a filtered group of customers in a couple of taps. It's exactly what I needed.
I'm a Delphi developer with a full-time job, a handful of customers on cloud servers, and no appetite for another software subscription. This post is about how the thing got built, what the interesting technical parts were, and what I think it says about where software is heading.
Why Bother?
The need was small and specific. I bill my customers through an online accounting system. I keep notes about customer logins, direct cell phone numbers, when and why they call for support, and so on in a couple of different places, eventually moving most of that to a web-based CRM tool but found was cumbersome to use. I finally just stopped and asked myself what I really needed on top of the accounting system to be productive with information at my fingertips.
I came up with three things:
- Notes and history for each customer.
- Instant lookup of an email or phone number.
- A way to produce a list of email addresses to send a bulk message out.
If I was going to build that myself, where would I put it and what would it look like?
My first instinct was to extend the "RDWeb" pages that are supplied when you install Remote Desktop Services on a Windows server. I'd already customized these for cloud customers and added some administration bits for myself so thought this would be another good extension. My second was to build a mobile app and either tie it in to the RD services or add a RAD Server module but that sounded like more work.
I put both options in front of Claude Code and asked a deliberately open question: what are the best ideas out of this?
That framing mattered. I didn't ask for code; I asked for recommendations.
The Evolving Design
Several decisions were made during the ensuing discussion:
1. Who owns which data? The first answer was the best idea of the whole project: let the accounting system stay the source of truth for what it already knows (name, email, phone, address), and keep in my own database only what it doesn't know: customer type, status, notes, extra contacts, a user count, and a log. The two are joined on the accounting system's customer ID. I never type a phone number twice and I can't create a second, conflicting set of customers.
2. Server, not web pages. The RDWeb pages are inline C# and XSLT with no data layer; the accounting API uses a token that needs to be saved somewhere - best to put that on a server. So this pointed heavily toward putting the logic and a database on RAD Server.
3. Database selection. I'd planned on using InterBase because my RAD Server already used it. However, RAD Server encrypts the database for its use so I would need a second license. This simple app was just for myself so that would be an unnecessary expense. I didn't want to use SQLite (not on a server) or Microsoft SQL Server (too much overhead) or several other options. Firebird became the obvious and simple choice because I already have experience with it and it's a solid, low-maintenance, and completely free database engine. With the database selected and a clean schema written, the back end was settled.
4. Everything pointed to a Mobile App. While RAD Server can certainly serve web pages, the conversation got me thinking more about how I wanted to use this custom CRM and the more I thought about it, the more I wanted an app on my phone instead of using a web interface. Delphi builds great cross-platform applications (I should know - after all, I wrote a book about it!) and this was a perfect use case!
5. Status and Type for Flexibility. I started with a single "status" with three values (current ABS Cloud, former ABS Cloud, not ABS). Then I decided the app should replace my CRM entirely, which meant it also had to handle my other customers. "Type" (ABS, ABS Cloud, Retail Pro) and "Status" (Active, Inactive) are separate things, and both became editable lookup tables. The email-blast query got better for free: type is now just a parameter.
6. A log that remembers the past. I wanted a history of every change, written automatically. Then I realized I also had years of notes (e.g. "customer signed up 04-29-2020") that I'd want to type in. So the log has two dates: when something happened (editable, so I can back-date) and when the row was created. Automatic entries come from database triggers, so a change is logged whether it came from a data change or was manually entered.
I also removed things a typical CRM would have such as no prospect tracking (every customer is a billable customer from day one) and marketing campaigns. Finally, I made a hard rule that the app never writes to or deletes anything in the accounting system; additionally, a customer who is removed from there gets flagged in my database, not automatically deleted.
These design decisions were specific to my needs and to what I wanted in a custom app. This is an important thing to remember when considering the Bigger Trend (see below).
The Three Parts
So, the resulting three major pieces of this project are:
1. Read the accounting system's API (read-only)
The accounting system exposes GraphQL (SQL in JSON format for APIs). A small client fetches customers a page at a time. A console tool, ccCRMSync, upserts them into the database. It only ever writes the columns the accounting system owns, so my own columns (type, status, notes) survive every sync.
The "upsert" (update or insert) is a single Firebird statement:
UPDATE OR INSERT INTO CUSTOMER (WAVE_ID, NAME, EMAIL, PHONE, ...)
VALUES (:WAVE_ID, :NAME, :EMAIL, :PHONE, ...)
MATCHING (WAVE_ID)
Two safeguards I'm glad I had: an empty response from the API aborts the sync (it's far more likely a bad request than "all my customers left"), and every customer not seen in a run is flagged, never removed.
2. Firebird plus RAD Server
The data module is the heart of the thing and is used by both the sync tool and by the RAD Server package; RAD Server gives me small REST resources: customers, contacts, a customer log, lookups, an email list, and "sync now" (POST only).
The automatic log is a tiny stored procedure plus a trigger that calls it for each field that changed:
IF (OLD.EMAIL IS DISTINCT FROM NEW.EMAIL) THEN
EXECUTE PROCEDURE LOG_CHANGE(NEW.WAVE_ID, 'Email', OLD.EMAIL, NEW.EMAIL);
That one line becomes a log entry reading Email: old -> new. Adding another tracked column is two lines.
3. The phone app
The mobile app is FMX built on TFrameStand: the customer list is a permanent base page, and every other screen (detail, edit, log, email list, settings) is a frame stood on top of it and closed with the Back button. The app never touches the accounting system or the database; it talks to RAD Server, and every call goes through a small helper that runs the request on a background thread and delivers the answer on the UI thread, and quietly drops the answer if the screen has been closed in the meantime:
TAsync.Run<TArray<TCustomerRow>>(FAlive,
function: TArray<TCustomerRow>
begin
Result := Crm.Customers(Query, TypeId, StatusId);
end,
procedure(ARows: TArray<TCustomerRow>)
begin
FillList(ARows);
end,
procedure(AError: string)
begin
CountLabel.Text := AError;
end);
A Few Gotchas
The "AI built it" story is only useful if it includes the stumbles along the way.
- An unverified API address. The first real sync failed with a 404 from the accounting system. The endpoint in the code was simply wrong, and had never been checked. Fixed by probing the server without credentials and finding the right path.
- A wrong first diagnosis. When RAD Server threw a socket error, the first suspected cause was a port clash between InterBase and Firebird. It wasn't. When I described exactly what I saw (other packages fine, only the dev server failing), the cause turned out to be another service of mine already using port 8080.
- A blank field. RAD Server allows a blank application ID. The app insisted on one, so no data showed up.
- A startup race. On launch, the customer list began loading on a background thread before the settings were read, so the first request failed and the second worked. Fixed by loading settings once at program start.
- Overlapping labels and dropdowns. Once reported, the AI assistant measured the actual control positions at runtime and found the combo boxes were drawn 50 px tall while the layout reserved 36. In another case of overlapping labels, a layered frame was found to be transparent.
Every one of those was caught by me, the human, running the thing. Claude Code compiled everything with the real compiler and checked what it could on its own, but it couldn't access my accounting data, read my database, access my server, or see what it actually looked like when running. AI built it faster than I could have, but I alone could verify it was useful to me.
The Bigger Trend
At ODUG I mentioned that businesses are increasingly using AI to build custom software that replaces expensive subscriptions. The ODUG meeting page links a few articles making that case, including "$100 and 120 Hours: How One Business Owner Replaced Six Tools" and "Making sense of the SaaSpocalypse." I think the trend is real. My own project is a small example of it. Some Delphi vendors are feeling the pinch as well with sales dropping as developers use AI to develop custom solutions which previously needed component sets or special tools to accomplish.
Three things made it practical for me:
- The data was already mine. The accounting system has a good API. The "CRM" part was only the thin layer the hosted tools were charging me for.
- The requirements fit in one head. One user, a few dozen customers, no permissions model, no workflow engine. That is a very different problem from replacing a company-wide system.
- I can read what it wrote. I'm a developer. When something was wrong, I could tell, and I could say exactly what I wanted instead ("that list should be
alClient" or "that error indicates the AppID was not read").
I also don't want to oversell it. Here some strong considerations you should think about before doing the same:
- I am now the support department. No vendor patches this when a new version of Android comes out requiring an application update. Backing up the data is completely my responsibility.
- Security is mine to get right. The app currently authenticates with a RAD Server application ID, which identifies the app but isn't a login. Per-user login and locking down the server's resources are things I need to decide on and implement.
- It wouldn't have happened without effort. As the ODUG write-up says, with AI doing a lot of the heavy lifting this took a couple of weekends; without it, it might never have been done at all. That's the real shift. Not "AI replaces developers," but projects that were just fleeting ideas can now be a reality.
- This doesn't generalize to everything. I replaced a tool and a collection of notes with a highly customized and narrowly scoped program on data I already controlled. I would not rip out a payroll or large inventory system this way.
Remember: commercial software has to be built for everybody; a tool you build for yourself only has to fit one person, and that is a different, much easier problem. The craft isn't going away; it's moving toward knowing what to build and recognizing when what you got is right.
Suggestions for Others
- Start with the question, not the solution. My best result came from asking for ideas, not code.
- Decide who owns each piece of data before you write anything.
- Write down your rules (coding style, never write to the source system, etc.) and make the AI assistant follow them.
- Make it prove things. Compile, run, measure. Be suspicious of anything that hasn't been run on your own hardware.
- Keep the scope small and goals clear. Searchable customer list, dated log, email list.
There is nothing secret about the code; it's in a public repository on Github, including a summary of the conversation with Claude Code. If you're in Oregon, come to an ODUG meeting where we share stories of what we're building and discuss how AI is shaping our lives.
Add new comment