What you'll build
Internal tools are the highest-return software a small team can build. In a week with Claude Code you can replace three spreadsheets and a Slack channel with one app that has SSO, a Postgres database, role-based access control, an audit log on every write, and a clean table-and-form UI. Built right, it pays for itself in recovered team time by the second week and runs without touching it for months.
What you're building
You're building an internal app that replaces a specific workflow your team currently runs on spreadsheets, email threads, and copy-paste. Pick one concrete workflow: customer onboarding, refund approvals, vendor invoices, content moderation, lead routing. The narrower the workflow, the better the tool. A one-table-one-form internal tool that nails a daily workflow beats a five-feature dashboard that does nothing well. Don't try to build the company's operating system in a week. Build the thing that ends one specific spreadsheet.
By Friday you have an app behind your company SSO, a few roles like viewer and editor and admin, a table view of records with filter and search, a detail view with edit and history, and an audit log of every change. Teammates are logged in and using it. That last part is the real test. Internal tools that nobody opens are worse than spreadsheets, because the existence of an unused tool creates ambiguity about where the source of truth lives.
Pick a workflow that hurts every day, not one that hurts every quarter. Daily pain creates the demand pull that makes a tool stick. Quarterly pain creates a tool everyone forgets how to use by the time the next quarter rolls around. The hardest part of an internal tool isn't the build, it's the adoption, and daily pain handles adoption for you.
What you need before you start
Comfort with TypeScript and a working knowledge of SQL. An understanding of who'll use the tool and what they need to do every day, written down. Access to whatever data source the tool reads from, whether that's a production database, a Stripe account, or a Google Sheet. A place to deploy that your team can reach, either a Vercel project on a custom domain or a self-hosted box behind a VPN. A short conversation with the future users before any code is written, even thirty minutes, will save you a day of guessing.
- Node 20 and pnpm
- Claude Code installed and authenticated
- Postgres via Supabase, Neon, or your existing database
- Drizzle ORM or Prisma for typed queries
- Clerk, WorkOS, or Supabase Auth for SSO
- A short doc with the workflow and roles
Day one: spec and skeleton
Write a SPEC.md before you write code. List the user roles, the entities, the actions each role can take, and the screens. A page each: list view, detail view, create form, settings. Keep the screen count under five for v1. Ask Claude Code to scaffold a Next.js App Router project with Tailwind, shadcn/ui, Drizzle, and your auth provider. Confirm sign-in works end to end before adding anything else. Auth bugs in an internal tool block every other person on the team, so they're worth getting right before any feature lands on top.
Add a simple users table that mirrors your auth provider with a role column. Seed it with two test users, one admin and one viewer, and confirm role-based redirects work. This is the foundation everything else sits on. Get it right on day one. The audit log table goes in on day one too, even though you won't write to it until day two. Tables added later always end up backfilled with NULL, which makes them useless for the question you wanted to answer.
Day two and three: data and CRUD
Write the schema in Drizzle. Two or three tables for v1. Run a migration with drizzle-kit. Build the list view first with a server component that queries the table, a TanStack Table or a simple HTML table with sort and filter, and pagination. Don't load every row at once. Paginate from day one or the page locks up the moment the table has ten thousand rows. Cursor-based pagination scales further than offset pagination, and Claude will write either if you ask, so pick cursor.
Add the detail page next, then the create form, then the edit form. Use shadcn forms with react-hook-form and zod for validation. Every form should write through a server action that checks the user's role, validates the payload, writes to the database, and writes an audit log entry. The audit log is non-negotiable on internal tools. Day-two-you adds it. Day-fourteen-you thanks day-two-you when a teammate asks who changed the row, and day-ninety-you thanks both of them during the security review.
Day four: roles and permissions
Internal tools live or die on the role model. Spend an hour on day four with the SPEC.md open and walk through every action the tool supports. For each action, write down which roles can perform it, on which records. The matrix that results is the contract between the UI and the permission layer. Without it, you'll improvise role checks at the point of use and end up with inconsistent rules that confuse users and auditors. Paste the matrix into SPEC.md so the contract stays visible to everyone.
Define three roles for v1: viewer, editor, admin. Viewer can read. Editor can create and update. Admin can delete and manage users. Build the permission helpers in lib/permissions.ts and call them from both the server actions and the UI components. The UI checks hide buttons. The server checks prevent abuse. Both are required. UI checks alone are security theatre, server checks alone are a confusing experience, and the combination is what production-quality internal tools look like.
Choices to make along the way
Clerk versus WorkOS versus Supabase Auth: Clerk is the fastest to set up for a typical SaaS-style internal tool and has great prebuilt components. WorkOS is the right choice if you need SAML SSO from day one because you're selling to enterprise. Supabase Auth is the cheapest and ties cleanly to a Supabase database. Pick by what your team's existing IDP is.
Drizzle versus Prisma: Drizzle is closer to raw SQL, gives you typed queries without a separate generation step, and is faster at runtime. Prisma has a friendlier query API but adds a build step and a heavier runtime. For an internal tool, Drizzle is the right default in 2026.
Day five: shipping
Before flipping the switch, do one production dress rehearsal end to end. Sign in as each role, exercise every action, watch the audit log fill up. If anything writes to a real third-party system on update, double-check the integration is pointed at the production endpoint with the production key. Internal tools that quietly write to the staging API for a week create the worst sort of confusion to unwind.
Deploy to Vercel on a subdomain like tools.yourcompany.com. Connect to the Postgres instance and confirm migrations run. Set every secret. Lock the subdomain behind a Vercel Access policy if you don't want it open to the web. Add a one-page README that tells teammates how to log in. Send a Slack message to the people who'll use it with screenshots and ask them to try it. Don't announce in a wide channel. Recruit two users by name first, then expand.
Sit with two of those teammates while they use the tool for ten minutes. Watch what confuses them. The things that confuse them are the things you fix on day six. The club at claudecodeclub.ai has a public internal-tools starter and a thread of teams running this exact playbook. Worth the nine dollars a month for the patterns alone, especially the role-design conventions.
How to test it
Write a TESTING.md with the five workflows that have to work: log in, list view with filters, create record, edit record, delete record as admin. Run the script in production before every deploy. Add Playwright tests for the two flows you broke at least once. Don't write a hundred tests up front. Write the ones you need after you need them.
Run a permissions matrix test once a week. Every role times every action should resolve to a single yes or no, and the test sweeps the table to confirm. The matrix catches the moment someone adds a new action and forgets to extend the permission helper. Without it, that gap shows up as a real incident months later.
Test with realistic data volume. Seed the database with at least ten thousand rows in the main table during development and confirm the list view stays under a second. Bugs that don't appear at ten rows but break at ten thousand are the most expensive to find in production, and they're cheap to find with a seed script.
How to extend it
Add CSV import and export, because internal tools without CSV are abandoned within a month. Add Slack notifications on state changes so the right people know without checking the tool. Add a saved-filter feature so users land on their own view. Add a small dashboard with two or three KPIs at the top. Add a job queue with Trigger.dev or Inngest if some actions take more than a few seconds. Each of those is a one-day add once the foundation is solid.
Add a command palette next, hit Cmd-K and search every record across every table. Power users live in the palette and stop opening the dashboard at all, which is the highest form of internal tool praise. cmdk plus a simple full-text search across the main tables is half a day of work and changes how the tool feels. Add bulk actions on the list view soon after, because a tool without checkboxes and a bulk-edit button forces users back to spreadsheets the moment they need to update twenty rows at once.
Common gotchas
Loading every row into the page on day three and forgetting to paginate. Skipping the audit log and regretting it on day fourteen. Sprinkling role checks across the codebase instead of centralizing them. Forgetting to add an index on the columns you filter and sort by, which makes the list view slow as the table grows. Forgetting to handle the deleted-by-someone-else case in the edit form, which throws a confusing error.
Optimistic UI updates that don't reconcile with the server response are the silent killer. The form looks like the save worked, the user moves on, and the change never landed. Use revalidatePath after every mutation, or pessimistic updates that wait for the server response, and don't trust optimistic UI on tools where correctness matters more than perceived speed.
The biggest gotcha is treating an internal tool like a SaaS product. It's not. Visual polish matters less than getting the workflow right. A homely tool that nails the workflow gets opened daily. A beautiful tool with a clunky workflow gets opened once and abandoned. The right way to know which side of that line you're on is to ask the users, in person, what they wish was faster. Their answer is the next ticket.
How Claude Code handles the boilerplate that kills the week
The reason internal tools take two to four weeks without Claude Code is the boilerplate. The SSO setup, the Drizzle schema with migrations, the role-based middleware, the shadcn/ui data table with sorting and filtering, the server actions with zod validation - each one is a two- to three-hour standalone task. Claude Code collapses all of them. You describe the schema in plain English, it produces the Drizzle table definitions, the migration file, the TypeScript types, and the seed script. You describe the list view, it produces the TanStack Table component with the columns you specified, pagination wired to a server component, and a filter input that debounces client-side. A full day of boilerplate work becomes two hours of review and iteration.
The audit log is the component most developers skip because it feels tedious to build. Claude Code does it in one request: 'add an audit_logs table with user_id, action, entity_type, entity_id, old_values, new_values, and created_at fields, and a logAuditEvent helper that every server action calls before returning.' The helper is ten lines. The table is one migration. And the moment a teammate asks 'who approved this refund?', you have the answer in three seconds rather than reconstructing it from Slack history.
The permissions module is where Claude Code's knowledge of the codebase pays off across the whole week. Early on, you define the permission matrix in a comment - viewer can read, editor can create and update, admin can delete and manage users - and ask Claude Code to produce lib/permissions.ts with named functions for each action. Every server action and every UI component you add for the rest of the week calls those helpers. The cost of adding a new action is writing one permission function. The cost of changing a rule is editing one file. That consistency is hard to achieve when you're writing role checks by hand across a growing codebase.
What to do after the first users adopt the tool
The first two weeks after launch are the most important. Sit next to two users and watch them use the tool for fifteen minutes without helping. Write down every moment of confusion, every click that did not produce the expected result, every piece of data they went looking for and could not find. That list is the v1.1 backlog. Build the top three items before the end of week two. Tools that improve visibly in the first two weeks after launch become trusted. Tools that do not improve get quietly abandoned and replaced by the spreadsheet they were supposed to kill.
Add CSV export early. Every internal tool user, at some point, needs to pull the data into a spreadsheet for an analysis the tool does not support. If they can export, they stay in the tool for everything else and use the spreadsheet for the one thing. If they cannot export, they start doing everything in the spreadsheet. Add a download button to the list view that generates a CSV from the current filtered state. Claude Code writes the server action and the React download trigger in under ten minutes.
The club at claudecodeclub.ai has teams running internal tools built with this exact stack - Next.js, Drizzle, Clerk, shadcn/ui, Vercel - for customer onboarding, content moderation, refund management, and vendor invoice tracking. The $9/month membership includes the internal-tool starter template, the permission module pattern, and a thread of teams sharing what broke in production and how they fixed it. The patterns that show up repeatedly in that thread - missing pagination, skipped audit logs, permissions that drifted over time - are exactly the gotchas this guide covers.
Common questions
Which auth provider should I use?
Clerk is fastest for typical setups and has great prebuilt components. WorkOS is right when you need SAML SSO from day one for enterprise sales. Supabase Auth is the cheapest and pairs cleanly with a Supabase database. Pick the one that matches your team's identity provider.
Drizzle or Prisma?
Drizzle in 2026. It gives you typed queries without a code-generation step, runs faster, and stays closer to raw SQL when you need it. Prisma is friendlier at the query API level but heavier at runtime and at build time.
Where do permission checks belong?
In a single permissions.ts file with named functions like canEditOrder(user, order). Both UI components and server actions call those helpers. UI checks hide buttons, server checks prevent abuse, and changing a rule means editing one file.
Do I need an audit log?
Yes, from day two. Every write goes through a server action that records who did what and when. When a teammate asks who changed a row, you have an answer. Adding the log retroactively is much more work than building it in.
Should I write tests during the build?
Keep a TESTING.md with the five critical workflows and run the manual script before every deploy. Add Playwright tests after you've broken a flow once in production. Don't front-load a hundred tests for an internal tool with a small user base.
Why pagination matters from day one?
Without pagination, the list view loads every row in the table. The page is fast at a hundred rows and unusable at ten thousand. Adding pagination later means rewriting the list view, the filters, and the sort logic together.
More to build
Build it. Ship it. Get paid.
Step-by-step lessons for every one of these inside the club. Join Claude Code Club for $9/month.
Related: the library, guides, and comparisons.
