Get in touchcorey@spiritdevs.com

Principal Engineer, Web and Mobile Platform Architecture · Corporate Interactive · Sydney, Australia

Theme
github.com/coreybainSnapshot 09 OCT 2026 · 00:08 UTC
Direct line

Start a conversation.

Send a note to corey@spiritdevs.com. Add a brief, job specification, or other context if it helps.

AttachmentsUp to 3 files · 4 MB combined

Submitting stores the message and nothing else — no queue in front of it, no autoresponder, no list to be added to.

01 / 06Writing

A CMS With No Admin Panel

Tue 6 Oct 20266 min readCorey Baines

This week I deleted my site's admin screens and its sign-in. Posts now start in a conversation with Claude Code or Codex, which call an MCP server that lives in the site's own repo.

  • MCP
  • AI agents
  • Architecture
“No admin. Just tools.” beside a crossed-out sign-in form. In front of it, a card shows a publish_post tool call with its expected revision numbers and an idempotency key, and ticks for scope, revision and receipt.

On this page

  1. 18,699 lines gone in one commit
  2. 37 tools behind one launcher
  3. What replaced the login screen
  4. A preview page that takes a code
  5. Where it's weaker than a login

Share

18,699 lines gone in one commit#

On 29 September I removed the browser admin from this site. It had screens for posts, projects, Labs, experience, Fun entries, the résumé, site settings, the contact inbox and access tokens, plus its own sign-in page. The commit touched 94 files, added 76 lines and deleted 18,699. The admin's stylesheet alone was 2,081 lines.

The plan from 15 September had three steps: build an MCP server, build a native iOS app, then remove the browser admin. The first version of the MCP server landed on 17 September. I skipped the iOS app. The last few posts hadn't gone through the admin anyway. An agent wrote them and published them from the terminal.

Clerk went 33 minutes after the admin. The only thing still using a sign-in was an image upload endpoint for the iOS app, and it had never worked in production because its allowlist was never configured. Removing it took away nothing that worked. The decision record now says the site "has no sign-in, middleware or session of any kind."

That left a gap. The MCP server had never been connected to the live site. For about an hour and three quarters that evening, the only way to change content was the Convex command line.

37 tools behind one launcher#

Claude Code and Codex both read their MCP config from the repo, and both files point at the same launcher:

{
  "mcpServers": {
    "home-management": {
      "command": "bun",
      "args": ["packages/mcp/src/local.ts"]
    }
  }
}

The launcher loads four values from a git-ignored .env and starts the server. Neither committed file holds a secret.

The server has 37 tools. Sixteen read content. Ten save work without touching the live site: create, update and discard drafts for posts, projects and Labs, and mark a review note as resolved. Ten need the publish scope: publish and unpublish for all three, schedule and unschedule for posts, and open or close the preview page. The last one, upload_media, runs on my laptop and puts an image on Uploadfile, then returns the URL and dimensions a draft needs.

No tool deletes anything. Fun entries, experience, the résumé and site settings can be read but not edited, so those still need the command line.

An animation comparing two ways to edit a post. Before: a browser goes through a Clerk sign-in, which is the only check, then an admin form, then Convex. The sign-in and the form are then crossed out and marked deleted on 29 September. After: a chat message goes to an agent, which makes an MCP tool call to Convex. Three calls go through, and each one is checked when it reaches Convex.

What replaced the login screen#

A sign-in screen answers one question once: is this me? The agent never signs in, so the backend does four things on every write instead.

The first is whether the token is valid. Each one has a name, a list of scopes and an expiry date, and the function that issues tokens refuses one without a future expiry. The token Claude Code and Codex share can read, write and publish content, and read profile data. It expires on 29 September 2027. It can't issue or revoke tokens; that needs deploy credentials.

The second is whether this exact change has already happened. Every write carries an idempotency key, and the backend keeps a receipt of the result for seven days. If the agent times out and sends the same call again, it gets the first result back instead of making a second change. The same key with different input is rejected.

The third is whether the agent read the latest version. Edits, schedules and publishes carry expectedRevision for the post and expectedDraftRevision for its staged draft. If either has moved since the agent read the post, the call fails with a conflict, and the tool description tells the agent to read the post again rather than retry with new numbers. So publish_post publishes the draft the agent read, not a newer one.

The fourth isn't a check. Each write adds a row to an audit log with the token, the operation, the record, the revision before and after, and the names of the fields that changed. It doesn't copy the content.

All of it runs inside one Convex mutation, so the checks, the change, the receipt and the audit row commit together or not at all:

const actor = await requireManagement(ctx, args, scope);
const receipt = await beginManagementWrite(ctx, actor, {
  idempotencyKey: args.input.idempotencyKey, operation: args.operation, input: args.input,
});
if (receipt.replayed) return receipt.result;
const { result, audit } = await applyManagementPostWrite(ctx, args);
return await completeManagementWrite(ctx, actor, receipt.receipt, result, audit);

An animation of three update_post_draft calls on one post, passing through four steps in one transaction: token and scope, receipt, revision, then the write and its audit row. The first call saves and moves the draft to revision 2. The second repeats the same idempotency key and gets the first result back. The third uses a new key but expects revision 1, so it's turned back with a conflict. The audit log ends with one row.

A preview page that takes a code#

I still want to read a post in the real layout before it goes out. When I ask, the agent calls create_preview_code and gives me a single-use code that works for ten minutes. I type it into the preview page, and that browser gets a session that lasts 30 days from its last use. revoke_preview_sessions signs every browser out at once.

The page shows each draft as it will look on the site. I can highlight a sentence and react to it or leave a note, and the agent reads those through the MCP server.

That page does have buttons that change the live site: publish, schedule, move back to draft, and publish or discard pending changes. So strictly, one small admin survived. It only covers blog posts, it has no text fields and no delete, and publish, unpublish and discard each ask for a second click. Its actions go into the same audit log.

An animation of the preview handoff. The agent calls create_preview_code and gets a single-use code that's good for ten minutes. I enter it once on the preview page and it's marked used. The browser gets a session that drains over 30 days and fills back up on each visit, until revoke_preview_sessions signs every browser out.

Where it's weaker than a login#

The token sits in a .env file on my laptop. Anyone with that file can do what the agent does until the token expires or I revoke it, and revoking it is a Convex command-line call.

Nothing reads the audit log yet. It can show what happened after something goes wrong, but it doesn't stop anything.

The iOS app lost its sign-in, its editing screens and its photo upload along with Clerk. Its source still contains the Clerk SDK.

The agent's token has the publish scope, so it could publish a post I haven't seen. It doesn't, because I've told it not to. That's an instruction, not a check. The preview page asks twice before publishing; the MCP server doesn't ask at all.

02Keep readingAll writing
A draft preview card with the phrase “edits the draft” tinted and underlined in wavy amber, and a question-mark badge at its end. Below it, a bubble offers a tick, a question mark, an angry face and a speech bubble, next to a resolved reply.
Older post

Notes That Stick to the Sentence

Fri 2 Oct 2026