AI Practice

As a freelancer I'm increasingly finding that clients arrive with something they've already made with coding agents. It might be a prototype of something they want to build or even something that's up and running in their own business which they now want to expand.

They may also use it as a way to communicate. A working prototype shows the design, layout and features they want far better than a written description can.

The sections below each look at what the client brings, what it's lacking and some advice for clients who want to take it further themselves.

What clients bring

There are three kinds of thing that turn up:

  • A prototype of something they want built.
  • A demonstration of the design, the layout and how the features should work.
  • Something running in their own business, where they've either hit a wall or want to turn it into a product to sell to similar businesses.

The third has the most in it. On the plus side a lot of business and operational logic is already baked into it, and it's tried and tested in their own business.

However, selling it to other businesses brings a lot of considerations above and beyond what you need for your own business. There are also traps that will need dealing with even if it only ever runs in-house.

The code underneath

Coding agents are now very reliable when it comes to building features that are fairly bug free. But there may still be a lot of repetition in the code and an overall lack of organisation. This doesn't tend to bite early on, and you can get a long way without even considering it. But at some point the code will need rationalising and consolidating.

These are invisible issues that eventually become visible through their consequences.

Bugs that come back

Repeated code and a lack of high-level organisation eventually mean you fix a bug in one place and think it's fixed, but it still appears elsewhere because it needed fixing in several places. Bugs you thought you'd fixed keep popping up.

Rising agent costs

Without some architectural consolidation along the way, a growing code base has a larger and larger surface area. Every new feature has more places it needs hooking into. Either your costs go up as the agents find all of those places, or some get missed and a feature works in one context but not another.

The interfaces between parts of the code also grow more complex as you go.

The order you build things in

Agents can build features in almost any order, but the order still matters. You want to build the architectural pieces first and consider carefully how they'll be used, so that features are built on top of them rather than each bringing its own copy.

If you're taking it further yourself: when a fixed bug comes back, look for other copies of that code and have the agent consolidate them into one. Every so often stop adding features and ask what's been repeated. I've written more about keeping code to a shape you can check in Too much code to read.

The user interface

Something you've made for your own use can be very different from something other people will use. You've grown the user interface piece by piece and you understand what everything does because you made it. The danger is that it grows quite complex and not that logical for anyone else, while you know it fully. User interface design has a long history of established best practices which you are now having to learn to build something that people will find easy to use.

Consolidation applies to the user interface as well as to the code: making it consistent and not having many variants of the same component or several different ways of doing the same thing. And on top of this you have error handling, progress indicators where necessary and consistent terminology and messaging to deal with.

As the visible part of the application, this is the easiest part for a client to work through themselves. They may realise the interface is becoming a sprawling mess and work on consolidating it. They may also realise that what they're building is becoming too complex to manage, and that planning some of it in advance would have been better than piling features on top of features. It's clearest in the user interface but the same thing is happening in the code.

Build out, consolidate

What works is a cycle: build out for a while, then consolidate, then build out again.

Build out, then consolidate, then build out again
Build out, then consolidate, then build out again

Consolidating might mean simplifying the user interface, and it applies equally to the code and potentially the database. Otherwise the surface area eventually becomes too much for you and your AI agents to deal with. Bugs you thought you had dealt with keep reappearing, and adding to one area of the application can break seemingly unrelated areas that happen to share some code.

Security

AI agents are happy to create API endpoints when you're working with a server and a front end. But again there may be repetition, and security and authentication across the whole API surface need constant watching in any application.

That needs an understanding of the security issues that can actually arise, from vulnerabilities in the underlying language and its libraries to structural vulnerabilities in your own code. The consequences are data leaks (or worse, deliberate vandalism or deletion) or corrupted data.

If you're taking it further yourself: have someone experienced audit the system before it holds anyone else's data, and make sure every endpoint goes through the same checks in one place (see Mind your boundaries below).

Hosting

AI agents generally choose simple hosting designed for hobbyists, so you may find yourself on Supabase, Vercel or similar providers. These work for small applications but they're basically toys designed to get something off the ground quickly, not solutions for professional development. They also have their own ongoing security issues that I hope you're on top of.

At some point you'll be choosing between the real options:

  • Running servers directly with Linode, Hetzner or Amazon, for example. This is simpler to set up and run but more complex when it comes to security and scaling.
  • Cloud solutions such as Amazon Web Services or Microsoft Azure. These are often the right solution for medium to large projects that need to cover a range of scales automatically, but they're much more complicated to set up and get right.

There's no magic wand here. Whichever you choose, be prepared to learn a lot about databases and hosting and potentially never really know whether your product is truly secure. The big risk isn't necessarily a hacker or a competitor targeting you. It's the constant large-scale automated targeting of everything online.

Then there are viruses and other compromises. Especially if you work with freelancers, you may not even realise your code is infected: many freelancers have viruses without knowing it, and these spread through the code of the projects they work on.

The risks here are financial. If your code is compromised, personal details including payment details may be being sent to third parties. The first you'll hear of it is the day lots of your customers email to say unauthorised payments have been taken from their accounts. That will not be a good day.

From one user to many

Even within a single organisation, something one person uses - for example the business owner - is potentially a very different proposition from something several people in the organisation use. That's true even as a purely internal tool.

Permissions

You don't always want everyone in the organisation to have access to all of the information. So you need a system where every piece of information records not only who created it but who is allowed to access it. There may also be information that some people are allowed to see but not to change.

This takes some thought and some experience of how it can be organised. You may end up rediscovering a lot of it yourself, but you'll start needing to put users into groups and give each group its own set of permissions.

Every API endpoint has to check those permissions, and this is where it comes back to the architecture. If who can access what is checked all over the code it's very difficult to keep track of, and even an AI agent won't be able to follow it through to show that only the right people can see the data. You need a layered approach, and the best places to secure it are the API and the low-level database access functions.

Auditing

If a piece of information changes, do you have a log of who changed it? Can you trace it back? And can you revert anything that was a mistake?

Think about what happens when somebody presses the wrong button and deletes or changes something in a way that can't be undone. The answer is to keep records rather than overwrite them, which is covered under Records that don't change below.

Managing users

You'll need to be able to add and remove users. And when a team member is replaced you need to reassign everything they owned to other users, so the work carries on and no data gets orphaned.

Data protection

If you hold personal data about your staff or customers, there are rules about how you look after it. Taking the UK as an example, the UK GDPR applies to anyone who holds personal data, and the Information Commissioner's Office (ICO) enforces it. In short:

  • You must keep personal data secure. Encryption isn't strictly required by law, but the ICO recommends it, and personal data should be encrypted at rest using industry-standard methods.
  • People can ask for a copy of all the information you hold about them, and you have one month to provide it (the right of access).
  • People can ask for their information to be removed (the right to erasure), which you also have one month to answer.
  • Most organisations that hold personal data need to pay the ICO a data protection fee.

For the application that means personal data encrypted at rest, a way to gather up everything you hold about one person, and a way to anonymise it on request without breaking the records around it.

Depending on the size of your organisation you'll also need an audit log of everyone who has read or written that information.

Other businesses

When other businesses start using your application, managing users becomes particularly important. Each business needs to be able to set up its own users and groups, so what each group can access can't be hard coded.

You'll need something on top of the system that's effectively metadata about every page in the interface and every feature on each page. There must be an easy way for a business to set up its users, see all of the permissions at a glance and audit everything anyone has done. Managing users and groups needs permissions of its own, and so does the auditing.

If you're selling to small businesses, or everyone in a business can see the same information, you can get away without some of this. But as you approach corporates as customers you'll definitely need it.

Harder to change

Once other businesses are using it, some things end up set in stone or at least a lot harder to change, including the user interface and the internal data formats.

You're much more restricted in what you can change in the user interface because people have learnt it and rely on it. You'll need to provide training and potentially publish update schedules in advance so your customers can prepare.

The API is harder to change too. With a web-based application you can update the front end at the same time as the API endpoints, but you still have to consider what happens if an endpoint changes underneath a page that's been open for a long time. At this point you need versioning on your APIs.

That's another thing that needs skill. You need to keep a certain amount of backwards compatibility, and be consistent everywhere about checking the version and handling a back end that has moved on when the front end hasn't. The answer can be as simple as asking the user to reload the page. But they won't want to lose anything they've entered, especially in a large form or anywhere they can build up complex data before pressing submit:

const API_VERSION = 7;

async function api(path, body) {
    const response = await fetch(`/api/${path}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', 'X-Api-Version': API_VERSION },
        body: JSON.stringify(body),
    });
    if (response.status === 409) {
        //  The back end has moved on: keep what's been entered, then ask for a reload
        saveDraft();
        showReloadMessage();
        throw new Error('API version has changed');
    }
    return response.json();
}
Every call goes through one function, which sends its version and handles the back end having moved on

Here saveDraft keeps whatever is on the page in the browser's local storage, so it can be put back after the reload. The important part is that every call goes through the same function, so the check can't be forgotten anywhere.

One copy per customer

Other businesses will start having requirements of their own, and you'll have to build some of them. It's very tempting to make a copy of the application for each customer, and this will work for a while. But as you get more customers you'll have many copies to manage, with the same problem as repeated code: you fix a bug for one client and then have to fix it for all the others. You'll also find that clients want similar features.

At some point the surface area your coding agent deals with becomes a surface area of features and bugs that you have to keep track of.

The answer is a common core with the right hooks for adding features for specific clients, and modules added to the core that a set of clients share. I've written about how my own framework does this in Modular monoliths in PHP and Maintaining a PHP framework for 25 years.

Corporate customers

Corporates go further still. You'll probably have to provide a service level agreement for uptime, and everything under Harder to change applies even more strongly: changes to the interface come with training and notice.

If their management isn't centralised, the permissions and auditing need subdividing so team leaders can manage and audit their own teams.

User management is usually centralised, so you'll have to support single sign-on with whichever system they use: Microsoft Entra ID (formerly Azure Active Directory), Google Workspace, Okta, OneLogin or Ping Identity, among others. Most speak SAML or OpenID Connect for signing in and SCIM for adding and removing users automatically, but each has its own requirements and its own mapping from their groups and roles onto what your system supports.

You may also have to fit in with their processes and show that you meet the standards they hold their suppliers to. For information security that's usually ISO/IEC 27001, sometimes with Cyber Essentials in the UK or SOC 2 for American companies. Some also ask for ISO 9001, which is about quality management.

Architecture

Most of the points above come back to the architecture. An application has a front end, a back end and a database, and the back end has a data access layer between its logic and the database:

The layers of an application, with a boundary at the API and another at the data access layer
The layers of an application, with a boundary at the API and another at the data access layer

Mind your boundaries

The API is one place where you need a security layer: every request is checked for who's making it and whether they're allowed to. If you're being really strict you need a separate but equal security layer on the database access as well, so a mistake in one is caught by the other.

For that to work, all of the data access has to be kept in specific places where it can be audited, not spread all over the code. If any part of the code can query the database, there's no single place to enforce the permissions.

There's a type safety boundary too. Data arriving at the API should be checked for shape and type before anything uses it, and potentially at other layers too. I've written about that in Type safety at the boundaries.

Records that don't change

Generally the system should be immutable as far as possible: existing records don't get changed. Instead the database is laid out so information builds up, tagged with who changed it, when and what the change was.

The status of an order is a good example. The obvious way is a status field on the order, which gets overwritten each time it changes:

CREATE TABLE orders (
    id       BIGINT NOT NULL AUTO_INCREMENT,
    customer BIGINT NOT NULL,
    total    INT NOT NULL,
    status   VARCHAR(20) NOT NULL,
    PRIMARY KEY (id)
);

UPDATE orders SET status = 'shipped' WHERE id = 1042;
A status that gets overwritten

Once it says shipped there's no record of when it was packed or who packed it, and if someone sets the wrong status by mistake the old one is gone. Instead the order can have a separate list of statuses, each with its own date, where the very last one wins:

An order with its own list of statuses, where the latest one wins
An order with its own list of statuses, where the latest one wins

Changing the status is now an insert, never an update. The history is the audit log, and putting a mistake right is just adding the right status again.

The same goes for deleting. Soft deletes mark things as deleted rather than actually deleting them, so nothing is lost when someone presses the wrong button.

Backwards compatibility

As the application grows, its data storage has to stay backwards compatible or have an upgrade path, so nothing already in the system is invalidated. That includes any files people have downloaded, if the application lets them export and import.

That means designing the database to grow in both the amount of data and the number of features it supports, and migrating the existing data each time it changes.

Designs and new features

AI is very good for mocking up designs and new features, and a client may use it to show what they want next. The expectation can be that a feature made this way is ready to go straight into the application once it's handed to the developers.

But the developers are usually dealing with how all of the issues on this page meet: the permissions, the audit log, the API versions, the data that has to stay compatible, the customers who need notice. It's going to be very difficult to set up a pipeline where what you make goes straight into the application.

At best it's a mock-up showing how the design might look or how a feature might work in a basic way. That's still very useful and much clearer than a written description, but don't expect a finished feature.

What you can get away with

A lot of this comes down to the fact that without the experience you're going to set yourself traps. Some aren't too bad and are easily put right later, but some will come back and bite you.

TrapFine whileBites when
Repeated codeit's small and changing fastfixed bugs keep coming back, and agent costs climb
A sprawling interfaceonly you use itsomeone else has to learn it
No permissionsone person uses ita second person logs in
Overwriting recordsmistakes are cheapsomeone deletes something that matters
No upgrade path for the datathere's no data worth keepinga change breaks what's already there
No API versionspages are reloaded oftena page left open talks to a newer back end
One copy per customerthere are one or two customersevery fix has to be made over and over
No single sign-onthe customers are small businessesthe first corporate customer
Hobbyist hostingthe application is smallit has to scale, or the platform's security issues become yours
Unchecked API endpointsnever, once it's on the internetsomeone finds one
Unprotected personal datanever, once you hold anythere's a breach, or a request you can't answer

Where experience still counts

Amongst all of this there are things that need human judgement, and not always technical experience. Some need judgement in a product manager role. Some need experience of having dealt with this before, even as a product manager: knowing for instance that users, groups and auditing will be needed.

Some need technical experience:

  • How to make the product secure.
  • How to design databases that allow for growth in both data and features without breaking existing data, and how to migrate that data as it grows.
  • How to architect a product so there's no code repetition and its complexity grows in a controlled way, only where new features are added, rather than without limit.
  • How to scale from a single user to many users to many businesses on one application, keeping a common core with the right hooks for each client's features.

And some need experience of having done it the wrong way. Taking an application down this road you'll discover a lot of these things, but probably as the issues hit you rather than by preparing for them in advance.

Building it yourself

If you're going to build it yourself you may make a lot of progress at first. But eventually you'll run into many of the things that people with development experience will (hopefully) know how to avoid by structuring the code and the user interface appropriately from the start.

Be prepared to learn a lot along the way. And be mindful that there may be a lot of things you don't know that you don't know. In the worst case they'll come back and bite you; otherwise they can slow progress to a crawl as you get stuck in a loop of fixing things.

See also