
I Asked Codex to Add a Real Feature to Vuesion
TL;DR: I asked Codex to add workspace ownership transfer to Vuesion with a four-sentence prompt. I didn't tell it which files to change, how ownership should work, which edge cases to handle or how to secure the API. It implemented the complete feature without asking a single implementation question. More interestingly, it found several security and domain implications I hadn't mentioned. The result wasn't exactly how I would have written it, but the differences looked much more like a normal code review than a failed AI implementation.
A few days ago, I gave Codex a Figma design and almost no implementation instructions.
The result surprised me.
But that experiment had an important limitation.
Codex was building something new.
This time, I wanted to know what would happen when it had to change something that already existed.
Not an isolated component.
Not a demo.
A real feature that Vuesion was actually missing.
The feature
Vuesion already has workspace management.
Workspace owners can invite people, members can have different roles, admins can manage other members and access control is enforced throughout the application.
But there was one thing you couldn't do.
Transfer ownership of a workspace.
The existing member management allowed me to switch users between MEMBER and ADMIN, or remove them entirely.
The OWNER role was different.
There was no way to give it to someone else.
This was something Vuesion actually needed, so instead of inventing another AI demo, I decided to let Codex implement it.
The prompt
I deliberately kept the prompt short:
Implement workspace ownership transfer.
A workspace owner should be able to transfer ownership of the workspace to another workspace member.
Follow the existing Vuesion architecture, patterns, conventions, and design system. Make sure the feature is safe and properly authorized.
Implement everything necessary to make the feature production-ready.
That was it.
I didn't tell Codex where the feature should live in the UI.
I didn't tell it what should happen to the previous owner.
I didn't tell it how the API should look.
I didn't mention transactions.
I didn't explain which operations should no longer be allowed once ownership became transferable.
I didn't tell it what should happen with personal workspaces.
And I didn't give it a list of tests.
Those were the things I wanted it to figure out.
It didn't ask me how to build it
Codex worked through the existing workspace implementation and built the feature without asking me a single implementation question.
The only two times I had to interact with it were to give it permission to execute scripts.
I didn't correct its approach.
I didn't point it toward particular services.
I didn't tell it about an edge case it had missed.
The initial prompt produced the implementation I reviewed afterwards.
That matters because I wasn't interested in whether I could eventually guide Codex toward a good implementation.
With enough instructions and corrections, that isn't a particularly useful benchmark.
I wanted to know what the existing codebase could tell it on its own.
The obvious part was easy
The visible part of the feature was straightforward.
Codex extended the existing workspace member table with a transfer action.
Selecting it opens a confirmation dialog explaining what the transfer means.
After confirming, the selected member becomes the new owner and the previous owner becomes an admin.
Codex also refreshes the workspace details, workspace list and member list afterwards so the UI immediately reflects the new state.
That part worked.
But it wasn't the part I found most interesting.
Codex understood that ownership isn't just another role
The simplest implementation would have treated ownership like the existing roles.
Change one membership from ADMIN to OWNER.
Change the other from OWNER to ADMIN.
Done.
Codex didn't do that.
It introduced a dedicated ownership-transfer operation:
POST /api/workspaces/:id/transfer-ownership
with a deliberately small request:
export type WorkspaceOwnershipTransfer = {
workspaceMemberId: string;
};
The client only specifies which existing workspace membership should receive ownership.
It doesn't send the current owner.
It doesn't send the new role.
It doesn't decide what happens to the previous owner.
Those decisions remain on the server.
That's exactly what I would want from an operation like this.
The UI wasn't treated as a security boundary
This is where the implementation became more interesting.
Before this feature existed, the UI already prevented users from doing certain things with the workspace owner.
You couldn't simply select OWNER from the role controls.
And the owner didn't get the same remove action as normal members.
For normal application use, that worked.
But a UI restriction isn't a security boundary.
An API can be called without using the UI.
Codex recognized that.
While implementing ownership transfer, it changed the existing workspace-member update endpoint so the owner's role can no longer be changed through the generic member operation:
if (currentWorkspaceMember.role === 'OWNER') {
throw BadRequestError('The workspace owner role can only be changed by transferring ownership.');
}
It also explicitly restricted normal role changes to ADMIN and MEMBER:
if (data.role !== 'ADMIN' && data.role !== 'MEMBER') {
throw BadRequestError('Workspace member role must be ADMIN or MEMBER.');
}
And it protected the delete endpoint:
if (currentWorkspaceMember.role === 'OWNER') {
throw BadRequestError('The workspace owner cannot be removed. Transfer ownership first.');
}
I hadn't asked for any of this.
Codex had effectively inferred a new invariant:
Workspace ownership may only change through the dedicated ownership-transfer operation.
Once it had established that rule, it started looking for other ways the application could violate it.
It found another path I hadn't thought about
Workspace invitations also contain a role.
That creates another possible way of introducing an OWNER.
Again, the normal UI didn't make this a problem.
But a manipulated request could.
Codex added validation to both sending and accepting workspace invitations so an invitation can no longer be used to assign the OWNER role.
This was probably my favorite part of the implementation.
Not because the code itself is complicated.
It isn't.
But because I never mentioned invitations.
Codex had to understand that workspace ownership was a privileged domain state, find another part of the application capable of affecting workspace roles, and protect that path too.
It wasn't just implementing the new happy path.
It was looking for ways around it.
The actual transfer is atomic
Changing ownership touches more than one record.
The workspace itself has an owner.
The previous owner's membership has a role.
The new owner's membership has a role.
Leaving those operations independent could result in an inconsistent workspace if one update succeeds and another fails.
Codex wrapped the complete operation in a database transaction.
Inside it, the new owner must already be a member of the workspace and cannot be the current owner.
Personal workspaces are explicitly rejected.
Then the workspace owner is changed, the previous owner becomes ADMIN, and the new owner becomes OWNER.
There was another detail I liked:
const ownershipUpdate = await tx.workspace.updateMany({
where: {
id: workspaceId,
ownerId: currentOwnerId,
},
data: {
ownerId: newOwnerMembership.userId,
},
});
if (ownershipUpdate.count === 0) {
throw ForbiddenError();
}
The update doesn't merely target the workspace.
It only succeeds if the authenticated user is still its owner at the moment the write happens.
That protects the operation against the ownership changing between the earlier authorization check and the actual database update.
Again, I didn't ask for that.
Authorization stayed on the server
Codex also added a dedicated policy:
export const canTransferWorkspaceOwnership = (resource: WorkspaceDetailView) => {
return resource.currentUserRole === 'OWNER';
};
The important part isn't the complexity of that policy.
It's where it gets enforced.
The new controller retrieves the authenticated session, loads the workspace in the context of that user and applies the policy server-side before calling the transfer operation.
The current owner's ID comes from the authenticated session.
It never comes from the request body.
So there are multiple layers involved.
The UI only shows the operation where appropriate.
The API independently checks authorization.
And the database write still verifies that the authenticated owner hasn't changed before committing the transfer.
That's a reasonable level of defense for what initially sounds like a very small feature.
Would I merge it exactly like this?
Not quite.
And I think that's the useful part of doing the review after the experiment instead of correcting Codex while it was working.
The implementation worked.
The important security and domain behavior was there.
But there are a few things I would change before merging it.
I would respect the service boundaries more strictly
This is the biggest architectural difference.
The ownership transfer lives in the workspace service, but inside that operation Codex accesses workspaceMember directly through Prisma:
const newOwnerMembership = await tx.workspaceMember.findFirst(...);
await tx.workspaceMember.update(...);
await tx.workspaceMember.update(...);
Vuesion already has a workspace member service.
I would use that existing service for workspace-member operations instead of letting the workspace service reach directly into the underlying Prisma model.
Codex understood the domain relationship correctly.
It just didn't follow the service boundary as strictly as I would have.
That's an important distinction.
The code isn't broken.
But in a codebase with established boundaries, correct behavior isn't the only thing I care about.
I also want future changes to have an obvious place to live.
I wouldn't load the workspace twice
The controller already retrieves the workspace before checking authorization:
const workspace = await getWorkspaceDetails(workspaceId, session.user.id);
assertHasAccess(canTransferWorkspaceOwnership(workspace));
The ownership-transfer service then queries the same workspace again to check whether it exists and whether it is personal.
I would pass the workspace that has already been loaded into the operation instead.
That removes a redundant database lookup and avoids checking the same existence condition twice.
I would still keep the conditional ownership update.
The earlier read and the actual write serve different purposes.
The first provides the domain data and authorization context.
The second ensures that the user is still the owner when the transfer is committed.
I would use an action menu in the member table
The UI Codex initially produced rendered the new ownership-transfer action next to the existing remove action as another icon button.
It worked.
But I wouldn't keep adding icon buttons as more member operations appear.
I would replace those controls with an action dropdown containing explicit actions such as:
- Transfer ownership
- Remove member
That's cleaner, easier to understand and scales better if member management gains another operation later.
This is a small UI decision.
It's also exactly the kind of thing I would comment on in a normal pull request.
What I changed before merging
None of those review comments required rewriting the feature.
I gave Codex the feedback and let it refactor its implementation.
For the backend, I asked it to reuse the existing workspace member service instead of accessing workspaceMember directly through Prisma.
There was one important constraint: the ownership transfer still had to remain atomic.
Codex handled that by making the workspace member service participate in the existing Prisma transaction.
The workspace service could then use the normal member-service operations while the workspace ownership update and both membership-role changes still committed together.
It also removed the redundant workspace lookup.
The controller now passes the workspace it already loaded and authorized directly into the ownership-transfer operation.
The conditional database update remained in place, because that check protects against ownership changing between the earlier read and the actual write.
For the UI, Codex replaced the two icon buttons with the existing Vuesion dropdown component.
The ownership-transfer flow itself didn't need to change.
The confirmation dialog, authorization, API operation and state updates all stayed intact.
That was the version I ended up keeping.
The implementation wasn't perfect.
But fixing it didn't require another implementation.
It required a code review.
What surprised me
The mistakes weren't where I expected them.
Codex didn't forget server-side authorization.
It didn't trust the UI.
It didn't implement ownership as a generic role update.
It didn't forget that multiple database changes should be atomic.
It didn't allow ownership transfer for personal workspaces.
It didn't allow transfer to an arbitrary user.
It didn't let the client decide who the current owner was.
And it found existing API paths that could bypass the ownership-transfer rules without me pointing them out.
The things I changed were mostly architectural refinement and one UI decision.
That's a much better outcome than I would have expected from such a short prompt.
This wasn't a CRUD test
That's also why I prefer this experiment to asking an agent to build another isolated CRUD feature.
Workspace ownership transfer is small.
But it changes the meaning of an existing domain.
Once ownership can move, the application has to answer questions it didn't need to answer before.
Who is allowed to initiate the change?
Can an owner remove themselves?
Can ownership be assigned through another endpoint?
What happens to the previous owner?
Can personal workspace ownership change?
What happens if two transfers happen at almost the same time?
Which state changes have to succeed together?
The prompt didn't contain those questions.
The codebase did.
Codex had to find them.
Existing code is part of the prompt
This is the same lesson I keep coming back to with AI-assisted development.
The text you type into the agent isn't its only instruction.
The repository is an instruction.
The architecture is an instruction.
Existing controllers are instructions.
Policies are instructions.
Tests are instructions.
Naming conventions are instructions.
The design system is an instruction.
If those things consistently express how the application works, an agent can infer surprisingly much without being explicitly told.
That's what happened here.
Codex didn't invent workspace ownership from scratch.
It found an existing workspace domain with members, roles, authorization, personal workspaces, invitations, services, stores, controllers, UI components and tests.
Most of the concepts it needed were already present.
Its job was to understand how the new behavior fit between them.
Consistency doesn't eliminate review
I don't want to overstate the result.
I still reviewed the code.
And I found things I would change.
That's important.
A consistent codebase doesn't make an AI agent infallible.
It makes its mistakes smaller and easier to identify.
Instead of reviewing an implementation where every architectural decision might be arbitrary, I was reviewing a feature that mostly looked like it belonged in Vuesion.
The remaining discussion was familiar:
Should this service call another service instead of accessing the model directly?
Can we avoid this duplicate query?
Would an action menu be cleaner here?
Those are normal code-review questions.
And after that review, Codex was able to make those changes without undoing the parts of the implementation that were already good.
That's a much more useful place to be.
Good architecture gives agents less to invent
In the previous experiment, I wrote that good code should be the easiest code to generate.
This experiment reinforced that idea from another direction.
There was no Figma specification this time.
There wasn't even much of a feature specification.
But Codex still had something much more valuable than an empty repository.
It had constraints.
Workspace roles already meant something.
Authorization already had a pattern.
Domain services already had a structure.
UI actions already had conventions.
Tests already described expected behavior.
Personal workspaces already existed as a concept.
The agent didn't need to invent all of those decisions while implementing ownership transfer.
It could spend most of its effort figuring out how the new behavior affected decisions that had already been made.
That's what I want from a foundation.
Not more code for the sake of having more code.
Fewer decisions that every developer, or every agent, has to make again.
This is how I build Vuesion
These aren't just ideas I write about. They're principles I've been building into Vuesion for more than eight years.