Every SaaS company eventually builds internal tools. Customer support needs to look up a user's account state and reset their password. Finance needs to issue manual refunds and adjust subscriptions. Engineering needs to inspect a customer's data when debugging an integration issue. Sales needs to grant trial extensions and apply discount codes. The first version of each of these tools is a SQL query run by an engineer at a terminal. The second version is a small admin endpoint that wraps the SQL query in a web form. By the third version the company has accidentally built a second product, complete with its own UI, its own permissions system, its own bugs, and its own backlog of feature requests.
The internal-tool problem is real and it eats engineering time at every growing company. We've kept ours small at DocuMint, CronPing, FlagBit, and WebhookVault by following a few principles that keep the tools useful without letting them become a second product. This post lays out those principles.
Principle 1: customer impersonation should produce an audit trail by default
The single most common admin tool is customer impersonation — the ability for a support engineer to view the application as a specific customer would, in order to reproduce a problem the customer is reporting. The naive implementation logs the engineer in as the customer and lets them click around. This is a security disaster: the engineer's actions are indistinguishable from the customer's, the engineer can accidentally change the customer's data, and there is no record that the impersonation happened.
The right implementation never gives the engineer the customer's credentials. Instead, the engineer authenticates as themselves, requests an impersonation session, and the application creates a session with both the customer's identity (for what data and views to render) and the engineer's identity (for audit-trail purposes). Every database write that occurs during the impersonation session is logged with both identities. Every page renders a visible banner showing "Impersonating: Customer X (as engineer Y)" so the engineer cannot forget what mode they are in. Some classes of write — payment changes, account deletion, password changes — are blocked entirely while in impersonation mode, requiring the engineer to leave the impersonation session and perform the action through a different audit-trailed admin tool.
Principle 2: every admin action is a first-class business event
Manual database edits are the single largest source of data corruption in SaaS systems. The engineer who runs UPDATE subscriptions SET status = 'active' WHERE id = 12345 to fix a customer's broken subscription has produced a row with no audit trail, no synchronization with Stripe, no email to the customer, and no record of why the change was made. Three months later when the customer asks why their subscription works the way it does, the answer is unrecoverable.
The right pattern is to expose the admin operations as first-class business events with full handlers. "Reactivate subscription" should not be an UPDATE statement; it should be a function that records the reason for reactivation, calls Stripe to ensure the subscription is in the right state, sends an email to the customer, writes an audit log entry, and updates the local database. The function is exposed both to the admin UI (for engineer-driven invocation) and to the support API (for support tickets that automate the resolution). The same function is used in both cases, so the behavior is identical regardless of who triggers it.
The discipline this imposes is that engineers are not allowed to fix problems by direct database edit. If a problem requires direct edit, the team's response is to build the admin operation that handles that case, then run it. This is more work in the short term but converts a one-off intervention into a permanent, audit-trailed capability that the team will use again the next time a similar problem occurs.
Principle 3: read-only is the default, write requires confirmation
Most admin actions are read-only — looking up a customer's account state, viewing their usage history, checking their payment history. Read-only actions should be the default and should be available to anyone with admin access. Write actions should require an explicit confirmation step that summarizes the action and its consequences, and should be restricted to a smaller subset of users.
The confirmation step is the place to surface the consequences of the action. "Cancel subscription for Customer X. This will: end their access at the end of the current billing period, send a cancellation email, and disable their API keys at midnight UTC on the cancellation date. Are you sure? Type the customer's email to confirm." The confirmation flow is annoying when you do it twenty times a day, which is why it is the right design for an action that is irreversible and that you should not be doing twenty times a day.
Principle 4: admin tools must be Cloudflare-Access-protected, not behind a username and password
The admin panel is the highest-value target on your infrastructure. It has access to every customer's data, the ability to issue refunds, and the ability to impersonate any user. Defending it with username-and-password authentication is inadequate. The right defense is to put it behind a corporate identity provider (Google Workspace, Okta, Microsoft Entra ID) via Cloudflare Access or an equivalent zero-trust gateway, with mandatory MFA enforced by the IdP.
This produces three benefits beyond the obvious authentication strength. First, the admin panel can be deactivated for an employee instantly by their manager removing them from the IdP group; there is no password-rotation race. Second, the IdP's audit log records every admin-panel access independently of the application's own logs, providing a check on whether the application's logs have been tampered with. Third, the admin panel can be public-internet-accessible without exposing the application to credential-stuffing attacks, because the IdP layer is what gates entry.
Principle 5: avoid the temptation to build a beautiful UI
The internal-tool failure mode is that a useful CRUD interface acquires a beautiful design system, sortable columns, saved filters, multi-line forms, and other production-product features over the course of a year. The team that maintains the admin tool is now also maintaining a second front-end codebase with its own design system, its own dependency stack, and its own bug surface.
The best admin tools are deliberately ugly. A page is a list of headlines and links. A form is HTML inputs with a submit button. There is no JavaScript framework. There is no design system. The page renders in 50 milliseconds because there is nothing to render. The team can ship a new admin operation in under an hour because there is no UI work to do — the new endpoint becomes a new link on the index page, with a form whose fields are derived from the operation's parameters.
Resist the temptation to make this prettier. The audience for the admin panel is twenty employees, none of whom are going to choose to use your product based on the admin panel's design. The audience for the customer-facing product is many more people who will choose based on design. Spend your design budget on the latter.
Principle 6: the admin panel is part of the product, not a separate system
A common pattern is to build the admin panel as a separate application — a separate Django project, a separate Next.js app, a separate codebase entirely. The argument is that this isolates risk and lets the admin panel evolve independently. The cost is that the admin panel inherits its own deployment pipeline, its own dependency upgrades, its own database connections, its own monitoring, and its own on-call rotation.
The pattern that scales better is to build the admin panel as a set of routes within the same application, gated by an admin-role check. The admin routes share the application's database connection pool, observability instrumentation, deployment pipeline, and dependency stack. Adding a new admin operation is the same kind of work as adding a new customer-facing endpoint, just with different authorization rules.
This works as long as the admin routes are clearly demarcated and the role check is unforgeable — typically a column on the user record and a middleware that checks it on every admin route. The Cloudflare Access layer provides defense-in-depth on top of the application-level role check.
The deeper observation
Internal admin tools are technical debt by default. Every team that does not invest in keeping them small ends up with a sprawling second product that consumes a meaningful fraction of engineering time and that nobody is excited to work on. The investment that prevents this is not a clever framework or a vendor SaaS — it is the discipline of treating each admin operation as a first-class business event, exposing it through a deliberately ugly interface, and refusing to add features that customers do not need. The reward for this discipline is that admin work stays out of the way of product work, and the team's capacity for shipping the actual product is not slowly eroded by maintenance of a tool that nobody outside the company will ever see.