Build Your Internal Admin Tools
Internal Admin Tools for Your New SaaS
Goal: Stand up the internal tooling every solo founder secretly needs — customer impersonation, manual data fixes, support context lookups, refund actions, support-ticket-to-state inspection — without it ballooning into a side-project that competes with shipping the product. Build the right 8-12 admin actions deliberately, in one focused day, then resist the urge to keep adding.
Process: Follow this chat pattern with your AI coding tool such as Claude or v0.app. Pay attention to the notes in [brackets] and replace the bracketed text with your own content.
Timeframe: V1 admin panel live in 1 day. New actions added on demand (max 1 per week, with discipline). Quarterly admin-tooling audit to remove cruft.
Why Founders Get This Wrong
Three patterns hit indie founders the same way:
- Panic-built admin actions during incidents. A customer reports their data is corrupted. The founder SSH-es into prod and runs raw SQL at 2am. Three months later, the codebase has 40 one-off scripts and no one remembers what they do.
- Admin tools become a side project. The founder spends a weekend building a beautiful dashboard with charts and analytics. The product ships nothing for two weeks. The dashboard is used twice and forgotten.
- No admin tools at all. Every customer-data issue requires a database query and a manual SQL UPDATE. Risk of mistakes is high; founder time is consumed; support response time is bad.
The version that works is structural: 8-12 admin actions deliberately scoped to the high-frequency support and incident needs, accessible from a single /admin route, audit-logged. Not a dashboard. Not analytics — those live in PostHog. Just the actions you actually do, made repeatable.
This guide pairs with Customer Support (admin tools live next to the support inbox), Incident Response (admin actions are needed during incidents but built before them), and Data Trust (admin actions need audit logging because they touch customer data).
1. Audit What You Actually Do Manually
Before writing any code, list the things you currently do manually that would benefit from a button. Most teams over-build admin tools by guessing at needs; the right way is to extract them from real activity.
Audit my last 30 days of manual operational work. Examples to look for:
- "I manually updated [field] in the database for [reason]"
- "I ran a SQL query to find out [thing]"
- "I impersonated a customer to debug their issue"
- "I refunded a customer manually through Stripe dashboard"
- "I reset a password via the database"
- "I unlocked an account that was incorrectly flagged"
- "I migrated data from old format to new format"
- "I deleted spam accounts"
- "I extended someone's trial"
- "I changed someone's plan tier manually"
For each manual operation:
1. **What did I do?** (specific action)
2. **How often?** (1×, weekly, daily?)
3. **Why?** (the underlying reason — sometimes the right answer is to fix the product, not build admin)
4. **Risk level?** (low: reading data; medium: updating non-critical fields; high: touching billing or auth)
Sort by frequency × risk. Top 8-12 become the V1 admin actions.
Anti-patterns:
- "Build admin for everything I might need" — over-engineering. If I've never needed it manually, I don't need a button for it.
- "Build admin only for the most important actions" — under-engineering. The frequent low-risk actions (impersonate user, view recent activity) are 80% of usage.
Output: a ranked list of operations + the 8-12 V1 actions to build + the rest to defer until they prove necessary.
The discipline that prevents bloat: only build admin tools for things you've actually done manually at least 3 times. Speculative admin tools rot without ever being used.
2. Pick a Pattern (Three Real Options)
Three viable approaches to admin UI in 2026. The right pick depends on your stack and ops appetite:
Pick an admin-tools pattern.
**Pattern A: A `/admin` route in your main app**
- Lives in your existing Next.js / app codebase
- Gated by hard-coded admin emails or a `is_admin` flag in users table
- Lowest setup; uses your existing auth, deployment, monitoring
- Best for: solo founders, small teams, less than 30 admin actions
**Pattern B: Retool / Internal.io / similar internal-tools platform**
- Visual builder, drag-drop UI, connects to your DB and APIs
- $20-50/seat/month at indie scale
- Best for: teams with non-engineer admins (support, ops) who need to use the tools
- Faster to ship; harder to version-control; some lock-in
**Pattern C: Direct DB access + scripts repository**
- No UI; admins use a query tool (TablePlus, DataGrip, the Supabase / PlanetScale / Neon dashboard)
- Plus a `scripts/` directory with reviewable Node / Python files for repeated operations
- Best for: technical founders who genuinely don't need a UI
- Cheapest; requires SQL fluency
Recommend ONE pattern based on:
- Solo founder vs team
- Who needs to use the tools (engineer-only or support staff too)
- Risk tolerance for prod-direct DB access
- Stack (Next.js, RoR, etc.)
Default for solo TypeScript founders: Pattern A — a `/admin` route in the main app. Lowest friction, most version-controlled, easiest to evolve.
For each pattern, output the setup instructions and the cost trajectory at 1, 5, 20 admin users.
The pragmatic 2026 default: Pattern A. Until you have non-engineer admins who need the tools, a /admin route in your main app is the right answer — same auth, same deploy, same observability, no extra bill.
3. Build the V1 Admin Panel
The V1 should ship in one focused day. Resist the urge to make it pretty.
Build a `/admin` route in my Next.js App Router app with these characteristics:
1. **Auth gate**:
- Server-side check: is `session.user.email` in my hard-coded `ADMIN_EMAILS` list (env var)?
- OR: is `session.user.is_admin === true` (DB flag)?
- Anyone else gets a 404 (not 403 — don't reveal that /admin exists)
- Audit: log every /admin page load with timestamp + user email
2. **Layout**:
- Sidebar with the 8-12 V1 actions from Section 1
- NO charts, NO analytics, NO custom logos
- Mobile-unresponsive is fine — admin tools live on desktop
- Single column, 800px wide max
3. **The standard admin pages I should build first** (cover ~80% of real ops):
- **/admin/users** — search by email, see basic user info, click into detail
- **/admin/users/[id]** — user profile, action buttons (impersonate, extend trial, change plan, lock account, send password reset)
- **/admin/billing/[customerId]** — billing summary, action buttons (issue refund, change plan, void invoice)
- **/admin/incidents** — quick actions during incidents (revert deploy, flush cache, force-disable feature flag)
- **/admin/scripts** — list of named scripts with "Run" buttons (e.g., "Re-run failed email sequence for [user]", "Recalculate usage for [customer]")
- **/admin/audit-log** — read-only view of all admin actions taken, with filters
4. **Audit logging on every action**:
- Table: `admin_audit_log` with columns: id, timestamp, admin_user_id, action_name, target_user_id (nullable), target_resource_type, target_resource_id, parameters (JSON), result (success/failure), reason (free text from admin)
- Every action requires admin to fill in a "Reason" field before submitting (1-2 sentences)
- Critical: audit log is append-only; no delete or update on these rows
5. **Action wrapper** (every action goes through this pattern):
```ts
async function adminAction({
adminUser,
actionName,
targetUserId,
reason,
execute,
}: AdminActionInput) {
// 1. Verify admin permissions
if (!await isAdmin(adminUser)) throw new Error("Unauthorized");
// 2. Require reason (not empty)
if (!reason || reason.length < 10) throw new Error("Reason required");
// 3. Pre-execute audit entry
const auditId = await logAuditStart({ adminUser, actionName, targetUserId, reason });
try {
// 4. Execute the action
const result = await execute();
// 5. Log success
await logAuditComplete({ auditId, result, success: true });
return result;
} catch (error) {
// 6. Log failure
await logAuditComplete({ auditId, error, success: false });
throw error;
}
}
For each of the 8-12 V1 actions, output:
- The page or modal where it lives
- The action wrapper code
- The "Reason" prompt for the admin
- The DB query / API call it actually performs
- The success message + audit log entry
Don't ship without item 4 (audit log). Untracked admin actions are the root cause of "what happened to this customer's data?" mysteries.
The "reason" field is the discipline that scales. When a future you or a teammate looks at the audit log, "Refunded customer per their request" is useful; an unlogged action is a future incident in waiting.
---
## 4. Build the Customer-Impersonation Move
The single most-used admin action across most products: impersonate a customer to see what they're seeing. Build it deliberately and safely.
Build customer impersonation as a first-class admin action.
Requirements:
- From the admin panel: a "Login as [user]" button on /admin/users/[id]
- Click triggers:
- Audit log entry: "Admin [name] impersonated [user]"
- Browser navigates to /app while my session represents the target user
- A persistent banner at the top of every page: "Impersonating [user@email.com] — [Stop impersonating]"
- Cookie or session token marked as
is_impersonating: true
- While impersonating, restrict destructive actions:
- Cannot delete the user's account
- Cannot send emails as the user
- Cannot make billing changes
- Cannot accept terms / privacy updates
- These actions are blocked even though the impersonating admin technically has permission
- End impersonation: clicking "Stop impersonating" restores the admin's session
- Audit log captures: every page view + every action while impersonating, tagged as "impersonation" so it's distinguishable from real user activity in analytics
The banner is non-negotiable. Without it, admins forget they're impersonating and accidentally take actions they didn't mean to take. The persistent visual reminder prevents that.
Output:
- The session-mutation code that swaps identities
- The banner React component
- The action-restriction middleware
- The audit log integration
- The "stop impersonating" return path
Common bugs to test for:
- Impersonating, then opening a new tab — does the new tab also show the impersonation banner? It should.
- Clicking "Logout" while impersonating — should log out the admin, not the impersonated user
- Impersonation expiring — set a max duration (e.g., 1 hour) and force re-confirm if longer
- The impersonated user being logged in elsewhere — both sessions should work independently
The "destructive actions blocked even when impersonating" rule is the move that prevents catastrophe. Without it, an admin clicks the wrong button, deletes a customer's account, and recovery is hours of cleanup. With it, the worst-case action is a confused admin who can't do the bad thing they accidentally tried.
---
## 5. Build the Refund / Billing Override Action
Billing actions are the highest-stakes admin operations. Build them with explicit safeguards.
Build refund and billing-override admin actions with safeguards.
Refund action:
- /admin/billing/[customerId] page
- Shows recent invoices (last 12 months)
- "Refund" button next to each invoice
- Click triggers a confirmation modal:
- Pre-fills the invoice amount
- Allows partial refund (override amount)
- Requires "Reason" (1-2 sentences)
- Optional: "Cancel subscription" checkbox if it's a final refund
- On confirm:
- Calls Stripe / billing provider's refund API
- Logs to audit log
- Sends a confirmation email to the customer
- Updates internal billing records
Plan-change action:
- "Change plan" dropdown on user detail page
- Shows current plan + dropdown of all plans
- Confirmation modal asks:
- Effective date (now / next billing cycle)
- Prorate? (yes for upgrades, usually yes for downgrades)
- Reason (free text)
- On confirm:
- Calls Stripe / billing API
- Updates internal user record
- Sends notification email to customer
Trial extension action:
- "Extend trial by N days" button
- Modal asks for: number of days (max 30), reason
- Updates the user's trial expiration in the DB
- Audit logs
Critical safeguards:
- Daily limits per admin: max 5 refunds / 10 plan changes / 20 trial extensions per admin per day. Past that requires a second-admin approval (or for solo founders: a sleep-and-revisit-tomorrow check).
- Amount caps: refunds over $1,000 require a "Yes, I really mean to refund [amount]" confirmation
- Notification to customer: every billing change generates an email — both for customer transparency and for fraud detection (if a customer didn't request the action, they tell you immediately)
- Reverse-out plan: every billing action should be reversible if discovered to be wrong within 24 hours. Document the reversal procedure.
Output:
- The refund-action page and action wrapper
- The plan-change-action page and action wrapper
- The trial-extension page and action wrapper
- The daily-limit + amount-cap enforcement
- The customer-notification email templates
The daily-limit safeguard is the protection against compromised credentials. If your admin account is breached, the attacker can't drain $50,000 in refunds overnight — they hit the daily limit at $1,000 and you have time to detect.
---
## 6. Build a Scripts Library
Some admin operations are too rare to deserve a dedicated UI but too risky to do via raw SQL. The middle ground is a versioned scripts library with a "Run" button.
Build /admin/scripts — a versioned library of named one-off operations.
Structure:
- Each script lives in
scripts/admin/[script-name].tsin the repo - The file exports:
name: display namedescription: what it doesparameters: typed input schema (Zod) — what the admin needs to fill indryRun: function that returns "what would happen" without applyingexecute: function that actually does itrisk: "low" | "medium" | "high" — gates whether it requires explicit re-confirmation
The /admin/scripts page:
- Lists all available scripts
- Click → form with parameter inputs
- Click "Dry run" → shows expected impact (e.g., "Would update 4 user records")
- Click "Execute" → confirmation modal with the dry-run output, requires reason, runs
- Audit log captures everything
Common scripts to seed:
recalculate-user-usage: re-derives usage stats for a specific customer for a billing periodreset-onboarding-state: marks a user as "needs to redo onboarding" so they re-enter the flowbulk-spam-cleanup: deletes accounts matching specific spam signatures (with dry-run first)re-send-failed-emails: re-runs the email-sequence for a specific customermigrate-customer-to-new-plan-structure: when you change pricing, this is what migrates one specific customerforce-re-embed-customer-data: when you change embedding models, this re-embeds for one customer
Why scripts beat raw SQL:
- Version-controlled (PR review)
- Reusable
- Auditable (audit log captures invocation)
- Type-safe parameters (no manual SQL injection risk)
- Dry-run capability (preview before commit)
Why scripts beat dedicated UIs:
- Don't pollute the admin nav with rare operations
- Easier to ship (it's a 50-line file, not a new route + form + state management)
- Each script can have a different UX without UI consistency burden
Output:
- The scripts library structure
- The /admin/scripts UI
- 5 sample scripts implemented end-to-end
- The auth + audit-log integration
The dry-run pattern is the most-skipped feature. Without it, admins guess at impact and sometimes overshoot. With it, "would update 4 records" is the safety net before "actually update those 4 records."
---
## 7. Build the Audit Log Reader
The audit log is only useful if it's queryable. Build a simple reader.
Build /admin/audit-log — a read-only view of all admin actions.
Features:
- List view of recent actions (last 30 days), most recent first
- Columns: timestamp, admin (who), action name, target (customer / resource), reason
- Filters:
- By admin user
- By action type
- By target customer (search by email or ID)
- By date range
- Click a row → detail page showing full parameters + result + reason
Use cases this serves:
- "What changed for [customer]?" — when a customer asks about modifications, search by their ID
- "Did anyone refund [customer X] yet?" — search by customer + action_type=refund
- "Who's been doing the most refunds?" — group by admin, count
- "Did we have any admin activity during the [date] incident?" — date range filter
- Compliance / SOC 2 evidence — audit logs are a required artifact for SOC 2 Type 2 (per Data Trust)
Critical:
- Read-only. No editing or deleting audit log entries from the UI.
- Append-only at the DB level (revoke UPDATE / DELETE permissions on the table for the app role)
- Retention: at least 7 years for compliance (longer than most other data)
Output:
- The page implementation
- The query optimization (audit logs grow fast; index by timestamp, admin_user_id, target_user_id, action_name)
- The export-to-CSV action for compliance / forensic review
The CSV export is the move that makes the audit log usable in actual investigations. When a security review or customer dispute requires log review, "give me a CSV of all actions on [customer] in [date range]" should be one click, not a custom SQL query.
---
## 8. Avoid the Common Sprawl Patterns
Admin tools rot if not actively maintained. The discipline that keeps them small:
Build the operating discipline that keeps admin tools small.
Rules:
-
No new admin action without a real instance. If I haven't done it manually 3 times, I don't build it.
-
No analytics in admin tools. Analytics live in PostHog. Admin tools are for ACTIONS, not for understanding.
-
No charts. Same reason. If a chart is useful, it belongs in a dashboard tool.
-
Resist the "while we're in here" feature creep. "While building the refund button, let me also add a feature flag editor and a bulk email tool" produces unmaintained sprawl. Each new admin action is a separate, deliberately-scoped PR.
-
Quarterly audit: every quarter, scroll through the admin nav. Any action used <3 times in the quarter — consider removing. Any frequent action that takes too many clicks — consider streamlining.
-
Treat admin tools as production code. Same TypeScript discipline, same testing, same code review. Admin tools accessing production data are equally critical to user-facing features.
-
Document each action with one paragraph in /admin/help. Future-me will appreciate "What does the 'Recalculate usage' button actually do?" being answered without reading the code.
-
Don't build management hierarchies until you have managers. Permission systems with roles, role-based-access-control (RBAC), approval workflows — none of these belong in V1. Add when you have a second admin who needs different permissions.
Output: the recurring quarterly admin-tools audit + the policy doc that prevents sprawl.
The quarterly audit is the rule that prevents the slow-build-up of unused admin actions. Most successful admin panels lose 1-3 actions per quarter as the underlying problem gets fixed in product or the action becomes irrelevant. That pruning is the discipline.
---
## Common Failure Modes
**"My admin panel has 50 buttons and I forget what most of them do."** Sprawl. Run Section 8 quarterly audit; cut anything used <3 times in the quarter.
**"Customer reports their data was changed and I don't know who did it."** Missing audit log. Section 3 item 4. Retrofit immediately.
**"An admin accidentally deleted a real customer's data."** Insufficient destructive-action safeguards. Section 5's daily limits + Section 4's impersonation restrictions prevent most.
**"My /admin route is publicly accessible."** Auth gate broken. Verify Section 3 item 1 — server-side check on every page load, not just the layout.
**"We have admin tools but support has to come to engineering for everything."** Pattern A (in-app admin) doesn't scale to non-engineer admins. Migrate to Retool or similar (Pattern B) when you hire your first non-engineer support person.
**"The audit log doesn't capture WHY actions were taken."** No reason field. Section 3 item 4 — make Reason required, minimum 10 characters.
**"My admin actions sometimes succeed in the UI but fail to actually run."** Action wrapper doesn't capture failures. Section 3 item 5 — log audit completion BEFORE the success response, so failures are still recorded.
---
## Related Reading
- [Customer Support](customer-support-chat.md) — admin tools and the support inbox are co-located in the founder's workflow
- [Incident Response](incident-response-chat.md) — admin actions during incidents need to be ready, not built panic
- [Data Trust](data-trust-chat.md) — audit logging is required for SOC 2 and GDPR data-subject-access logs
- [PostHog Setup](posthog-setup-chat.md) — analytics live there, not in admin tools
- [Customer Community](community-building-chat.md) — admin actions can fire from community moderation work too
[⬅️ Growth Overview](README.md)