The brief was specific: keep the recruit-to-hire pipeline, drop everything else, and bring it down to a fixed build cost instead of a per-seat SaaS bill that climbs every quarter. This is the b(uild)log article for that project. It covers the process, not the code: how I used Claude as the implementation engine, how BPMN diagrams became the source of truth for the build and build prompts, why we kept a separate Obsidian vault as the project wiki, and how the whole thing ended up AI-native.
The setup: planning day, then build day
The cadence for the whole project was deliberate. Every dev log in the vault is preceded by a full day of planning. Not a quick scribble on a whiteboard, an actual planning day with diagrams, schema drafts, route maps, and written acceptance criteria. The build day that followed was almost mechanical by comparison.
This was on purpose. Claude is fast, but it is faster when you hand it a finished decision instead of asking it to make one. A planning day produces three things that a build day needs:
- A BPMN diagram of the user flow the feature implements
- An ERD schema that the diagram implies
- A written list of pages, routers, and procedures that need to exist by end of day
When I sit down for the build day, I am not asking Claude "how should we do this." I am asking it to execute against a plan I already wrote with it/him/her.
Why model-driven engineering works with AI
The architecture plan in the vault opens with this line: "Approach: Model-Driven Engineering — this plan is the foundation before BPMN, use case, ERD, and class diagrams are designed, then implementation begins."
That order matters. Model-driven engineering used to be a heavy methodology that produced beautiful UML diagrams and three months of slides. With an AI agent doing the implementation, it becomes the cheapest part of the project. You spend a day in Camunda modeler or in an IDE with an agent drawing and planning how a job requisition turns into a hired worker, and that diagram does double duty: it documents the business process for the client, and it becomes the input to the next prompt.
The recruit-to-hire BPMN is the clearest example. Three swimlanes — Partner, HR, Applicant — with message flows between them. Inside the HR lane, the kanban groups (To Be Screened, Interviewing, Hired) are drawn as BPMN groups, which means the diagram already encodes the application status enum. Making the diagram is the same act as defining the data model.
When I prompted Claude to build the app and its features, the prompt was effectively: "Here are the BPMN and all the plan outlined in my Obsidian vault. Read and reference it all then build them in this order." The model never had to guess what the feature was. It read the diagram and plans and implemented it.
The boilerplate question
I did not start from create-next-app. I started from a Next.js 15 + tRPC + Prisma + better-auth boilerplate I had already shaken down on a previous project. The first commit on day one is literally feat: Add Claude Code configuration and documentation, and the second is chore: Remove previous branding, clear public page content. Day one was 80% deletion.
This is the part of AI-assisted development that nobody talks about. Boilerplates are not lazy, they are leverage. The boilerplate gave us:
- Auth wired up with sessions, password reset, and role middleware
- A tRPC + React Query client/server pair that already type-checks end-to-end
- A Prisma client output path, migration scripts, and a seed runner
- Tailwind v4 with shadcn/ui already configured
- A Turborepo + Bun monorepo skeleton with shared config packages
Without that, day one would have been "set up the plumbing." With it, day one was "scaffold the entire ATS domain — schema, seed, two feature pages, sidebar, auth cleanup." End of day, two full feature areas (partners and requisitions) were running against real data. The dev log records 55 files changed, +4,762 / −8,260, in a single sprint.
The right mental model: Claude is good at writing code that looks like the code already there. Give it a healthy reference codebase and it stays in the lane. Give it an empty repo and it will guess at conventions and you will spend a week pulling those guesses back out.
The Obsidian vault as project wiki
Parallel to the code repo, I keep a separate Obsidian vault. It contains no source code. It contains the BPMN files, the architecture plan, the ERD, the UI/UX plan, the email communication flows, the WordPress integration docs, the security checklist, and a dated dev log for every build session.
The vault is symlinked into the actual code repo, so when Claude is working in the code repo it can read the vault and resolve cross-references like [[architecture/iOS-Safari-Notch-And-SafeArea]]. The reverse is also true: the vault has a CLAUDE.md at its root that tells Claude what the vault is and where the actual code lives.
A few things this enables that surprised me:
- The dev logs become a memory aid for the model. When I open a new session a week later, "read
dev-logs/Dev-Log-2026-04-30.mdand continue from there" is enough context to pick up exactly where we left off, including the iOS Safari rabbit holes and the architectural decisions we already made. - The client gets a real handover artifact. When the project ends, they do not get a zip of source code. They get a wiki that explains why the system is shaped the way it is, with diagrams, plans, and decision records.
- The plans are versioned. When we revised the AI integration plan from per-user keys to org-wide BYOK, the dev log captures the date of the change and the reason. Six months from now, the next person will not have to reverse-engineer that decision from a Prisma migration name.
I treat the vault as a first-class project deliverable. It is the difference between a codebase someone has to inherit and a system someone can maintain.
How BPMN becomes a build prompt
The mechanical part of the workflow looks like this:
- Open the BPMN file in Camunda Modeler. Walk through every task, gateway, and message flow. Confirm with the client that the flow matches reality.
- Write a markdown plan in the vault that translates the diagram into concrete deliverables: schema models, tRPC procedures, page routes, and UI states.
- Hand Claude the markdown plan plus the BPMN as context. The prompt is short — "build this" — because the plan is doing the heavy lifting.
- The build day produces commits, and at the end of the day, Claude writes the dev log against the commit history.
That last step is worth pausing on. The dev logs in the vault are written by Claude at end-of-session, not by me. Each one starts with a commit table, then "Work Done by Area," then "Bugs Fixed," then "What's Next." It is consistent because the model is consistent. I review and edit, but I do not author them from scratch.
The Recruit-to-Hire-Process.bpmn is the canonical example of this pipeline working end-to-end. The diagram defines the application status enum (NEW → SCREENING → INTERVIEW → OFFERED → CONTRACT → HIRED → REJECTED), the message flows define the email events (offer letter, rejection notice, application acknowledgment), and the partner swimlane defines the public REST endpoints that the WordPress site will hit. Three artifacts (schema, email service, public API) all fall out of the same diagram.
The stack, briefly
The technical surface is unsurprising on purpose. Boring tech is fast tech.
- Next.js 15 (App Router) on Vercel, with React 19 and TypeScript strict
- tRPC v11 for end-to-end type safety, React Query on the client
- PostgreSQL with Prisma, hosted on Supabase
- better-auth for sessions, with a
staffRolefield gating thehrProceduremiddleware - Bun + Turborepo monorepo (apps/web, packages/db, shared config packages)
- Resend for outbound mail and inbound webhook (two-way email threading with applicants and workers)
- UploadThing for CV uploads
- Tailwind v4 + shadcn/ui, with custom island/card layouts
- Vercel AI SDK +
@ai-sdk/anthropicfor the in-app assistant
The integrations were not afterthoughts. WordPress is the public-facing site, so jobs embed via an iframe at /embed/jobs and applications come back through CORS-whitelisted REST endpoints under /api/public. Resend's inbound stream lands at a webhook that verifies the HMAC signature, extracts a reply token from the recipient address (ats+<token>@replies.zuidman.nl), and threads the reply onto the right Application record. Both of those are documented in the vault before they were built.
The AI-native part
Every business operation in the system was already a typed, validated, role-gated procedure. Adding an AI assistant on top of that was not a feature, it was an exposure layer.
Phase 1 of the AI assistant (built on 2026-05-01) wraps each tRPC procedure as a tool in the Anthropic tool-call format
That is the entire architecture. The agent does not get raw Prisma access, it does not compose SQL, it does not get its own permission model. It gets the same procedures the UI calls, with the same Zod validation and the same hrProcedure role check. If the calling user cannot do something via the UI, the agent cannot do it either.
24 tools are exposed across partners, requisitions, jobs, applicants, applications, workers, and worker threads. Reads run unaudited; writes go through a defineTool wrapper that records every call to an AgentAction table — userId, toolName, argsJson, resultJson, error. PII fields like nationalId are stripped from tool results before they reach the model. Destructive operations (deletes, terminal status transitions, outbound email) are not exposed in Phase 1; they are queued for a Phase 2 confirmation-card UI.
The result is an HRM/ATS module where any natural-language instruction the HR team can phrase — "find me available welders in Rotterdam," "create a partner record for Jansen Bouw," "move all the screening-stage applicants for the Amsterdam scaffolding job to interview" — runs against the same business logic the UI runs. The chat is not a separate product. It is a different surface on the same system.
This is why I keep calling it "accidentally AI-native." We did not set out to build an AI-first product. We built a clean tRPC backend, and the AI layer fell out of the architecture for free. If your business logic is well-typed and centralized, exposing it to an LLM is a Tuesday afternoon.
What actually shipped
Reading back the post so far, I realised I never actually listed the features. Here is the surface the HR team logs into every morning.
The recruit-to-hire pipeline
- Partners — the external companies that send labour requests. Full CRUD, status tabs (Active / Prospect / Inactive), linked requisitions on the detail page.
- Requisitions — auto-numbered as
REQ-YYYY-NNN, linked to a partner, assigned to a staff member, with trade, head count, contract type, and work location. - Jobs — the postings that fulfil a requisition. Headcount tracking, published/draft state, slug-based URLs for the public WordPress embed.
- Applicants — phone is the unique identifier, not email (construction workers in this market do not all have stable email addresses). Skills, trade specialty, CV upload via UploadThing, GDPR-compliant anonymisation on hard delete.
- Pipeline kanban — full-page drag-and-drop board per job, columns mapped to the application status enum, "add applicant" works inline so the HR user does not have to bounce between pages.
- Workers — what an applicant becomes after they get hired. Employee number, assignment history, daily rate, status (Available / On Assignment / Inactive / Blacklisted).
Two-way email communication, threaded inside the app
This was one of the parts I am proudest of, because it works without any of the HR team learning what an MX record is.
- Outbound — when HR sends a message from an application or worker thread, the email goes out via Resend with a reply-token baked into the address (
ats+<token>@replies.zuidman.nlfor applicants,wkr+<token>@replies.zuidman.nlfor hired workers). The reply-to display name is configurable in the admin settings, so the recipient sees "Hiring Team" instead of a raw token address. - Inbound — applicants and workers reply from whatever mail client they like. Resend's inbound stream POSTs to a webhook that verifies the HMAC signature, extracts the token, and threads the reply onto the right
ApplicationMessageorWorkerMessagerow. The HR user sees a chronological conversation inside the app, no inbox-juggling. - Admin setup — a three-step stepper page (
/settings/admin/email) walks an admin through registering the sending domain, adding the MX record for the reply subdomain, and pasting the webhook signing secret. The secret is encrypted at rest with AES-256-GCM. After two env vars are set on deploy, the rest is self-serve. - Worker threads — post-hire conversations live in their own thread, separate from the applicant's original application thread. Same plumbing, different prefix on the reply token. No bleeding context between "we want to interview you" and "your timesheet for last week."


The AI assistant
Every HR/Recruiter/Admin staff user gets a /assistant page. It is not a chatbot stapled to the side, it is a full surface on the same data the UI works with.
- Natural-language CRUD — "create a partner record for Jansen Bouw," "find available welders in Rotterdam," "move all screening-stage applicants for the Amsterdam scaffolding job to interview." The assistant calls the same tRPC procedures the UI calls, with the same Zod validation and the same role check.
- 24 tools, one source of truth — partners, requisitions, jobs, applicants, applications, workers, and worker threads. Reads are free; writes are restricted to non-terminal status transitions in Phase 1.
- Audit log — every write the assistant makes lands in an
AgentActionrow with the user id, tool name, args, and result. If anything ever goes wrong, the trail is complete. - PII redaction — fields like
nationalIdare stripped from tool results before they reach the model. The data the LLM sees is the data the HR user could read on screen, minus the sensitive bits. - Org-wide BYOK — one Anthropic key, set by an admin in
/admin/settings/ai, encrypted at rest, never echoed back to the client. HR and recruiter users do not see it (not even masked), they just use the assistant. - Markdown output, persistent chat — replies render with proper tables, lists, and code blocks. Conversations persist across page refreshes per user.
- Strictly scoped — the system prompt refuses non-ATS questions with a single canned sentence, so the chat does not turn into a free-tier coding assistant for the HR team.



The boring necessities
- Auth — better-auth with email/password, role middleware (
ADMIN,HR,RECRUITER), session cookies, password change flow on the settings page. - Public REST API — CORS-whitelisted endpoints under
/api/publicso the WordPress site can list jobs and post applications without going through the authenticated tRPC layer. - WordPress embed —
/embed/jobsis iframe-safe, height-listener snippet provided in the admin settings, so the marketing site shows live jobs without any plugin install. - GDPR erase — applicant detail page has an "anonymise" action that wipes PII while preserving the application history rows for legal reasons.
- Mobile parity — pill-nav top header, horizontal-scroll filter strips, icon-only action buttons under
sm, full iOS Safari notch/safe-area handling.
What I will change post MVP
A few things, honestly:
- The localStorage-only chat history. Phase 1 stores conversations per-browser keyed by user id. It is fine for one HR user on one laptop, but it falls apart the moment you want cross-device history or audit trail. Server-side persistence is on the follow-up list.
- Phase 1 leans on the system prompt to refuse destructive operations. That works, but it is a soft guardrail. Phase 2 needs the confirmation-card UI so deletes and terminal transitions require an explicit human OK.
The one difficulty I faced and spent hours refining
As someone who came from a design/creative background, I am obsessed with pixel perfect UI designs. I always try to account for all kinds of devices on all my builds especially in terms of browser experience. I underestimated how much iOS Safari would eat. The dev log for 2026-04-30 has a section on the notch and safe-area, and the 2026-05-02 log has another on a keyboard-dismiss bug that left the body shifted into the notch. Total time spent on iOS layout quirks: maybe a full day. None of that was in the plan and honestly speaking Apple did make the lives of us designers harder after that one WWDC where they switched to the liquid glass effect.
Closing thought
The reason this project ran on time is unglamorous. It all comes down to scaffolding a solid build architecture and applying the correct techniques and best practices from the start. The Obsidian vault meant context never had to be reconstructed; every session started from a real document. The BPMN-first habit meant the model was implementing a decision, not making one. The boilerplate meant day one was already on rails.
The AI assistant on top is the bonus round. The real work was the planning days nobody sees in the demo. In essence, this project is a proof that vibe-coding goes better when you apply MDE techniques and best practices.
If you are building software with Claude and you are not seeing the speedup people promised, the issue is almost never the model. It is usually that the model is being asked to make decisions that should have been made the day before, in a diagram, in a vault, with the client in the room.
Interested in a demo? Head to the contact page and fill up the form, and I will be right with you!
