The dashboard I can't build: running support for a zero-knowledge product
This week I tried to build an internal admin dashboard for Pwdly. It's almost empty — and that's exactly the point.

This week I sat down to build something every SaaS founder eventually builds: an internal admin dashboard. A single pane of glass to answer the questions that come up every day. Which features are people actually using? Where are users getting stuck? Who’s about to churn? Which support ticket is about which project?
I got about an hour in before I started laughing at my own screen. The dashboard was almost entirely empty. Not because I’d written bad SQL — because the data I wanted simply doesn’t exist on our servers. By design. By the entire architectural premise of the product.
Let me show you what I mean.
What a normal SaaS admin panel looks like
If you’ve worked at a typical B2B SaaS company, you’ve seen the dashboard I had in mind. Daily active users plotted against a 90-day rolling baseline. Feature adoption heatmaps. Per-account activity timelines. The ability to impersonate a user — or at least peek at their data — when a support ticket comes in. Search bars that let you type a customer’s email and see every project, every team member, every recent action.
That dashboard is not a luxury. It’s how product teams figure out what to build next, how support agents resolve tickets in minutes instead of hours, and how leadership decides where to invest. It is, broadly speaking, table stakes.
Now here’s what Pwdly’s equivalent looks like.
What our admin panel can actually show
A list of accounts. Email address. Plan tier. When the account was created. When the user last logged in. Aggregate billing status. The number of projects they belong to — but not the names of those projects. The number of credentials stored — but nothing about what they are. Total disk used by their encrypted blobs.
That’s it. That’s the whole panel.
I can tell you that User A has 12 projects and User B has 3. I cannot tell you what any of those projects are called, who’s in them, when they were last accessed, or what’s inside them. I can see that someone stored 47 credentials this month. I have no idea whether they’re database passwords, API keys, or birthday party Wi-Fi codes.
This isn’t a feature gap I’m going to ship around. It is mathematically impossible for me to know more. Every credential is encrypted in your browser with a key derived from your master phrase via Argon2id, then sealed with XChaCha20-Poly1305 before it ever leaves the device. The server only sees ciphertext. Even the project membership graph is partially shielded — invite codes are derived from a one-time secret that we never store in plaintext.
If I wanted to know what’s inside your project, I would have to break the cryptography. So would an attacker. So would a subpoena. That symmetry is the point.
What this actually feels like, day to day
It feels like working with one hand tied behind your back, and choosing to keep it there.
A support ticket arrives: “my shared project isn’t loading.” In a normal product I would open an admin tool, find their workspace, click into it, and see the broken state in five seconds. In Pwdly I have to ask the user to open their browser console, run a diagnostic that the user themselves triggers, and send me a sanitised bundle that contains zero credential data. Resolution time goes from minutes to a back-and-forth that can stretch across a day.
A product question arrives: “are people actually using the import-from-Chrome feature?” In a normal product I’d run a query. In Pwdly I can see that the import endpoint was hit N times this week — but I can’t see who, how often per user, whether they finished the flow, or whether the imported data was useful enough to keep. I have to ask. Out loud. To real humans. Like it’s 2008.
A churn signal arrives — except it doesn’t, because I have no behavioural telemetry to derive it from. The first I learn that someone’s leaving is when they cancel.
This is, genuinely, harder than running a normal SaaS. I’d be lying if I said otherwise.
How we work around it
The answer isn’t to weaken the architecture. It’s to lean harder on the things that don’t require breaking it.
Aggregate counters, never per-user behaviour. I can see that the password generator was used 14,000 times last week. I cannot see by whom or for what. That’s enough to make product decisions; it isn’t enough to profile anyone.
Talking to users. Surveys, intercom-style prompts that the user can dismiss forever, a public roadmap, and a changelog that doubles as a feedback loop. The qualitative replaces what would normally be quantitative.
Public, honest comms. This essay is part of that. If the operational cost of zero-knowledge is invisible to users, the architecture starts to feel like marketing. Saying it out loud — “here’s what we genuinely cannot see” — is the only way to make the trade real.
Why I wouldn’t change it
Every blind spot I have is also a blind spot for an attacker who breaches our database, a rogue employee who goes looking, and a court order that lands on our doorstep. The dashboard I can’t build is the same dashboard a future bad actor can’t build. That’s not a side effect of the architecture. It is the architecture.
If one day I open the admin panel and find that I can see your projects, your credentials, or your usage patterns, something has gone catastrophically wrong with the product I set out to make. The empty dashboard is the receipt that proves the promise.
So I’ll keep flying with one hand tied behind my back. Slower support. Fewer numbers on the wall. More conversations, fewer charts. It’s a worse experience for me, every single day. And it is exactly the experience I want you, the user, to be paying me for.
— Built with one hand tied behind our back, on purpose.
-- What a "normal" SaaS founder might try to write
SELECT
user_id,
project_name,
last_accessed_at,
feature_flags,
churn_score
FROM admin_dashboard_view
WHERE user_email = 'customer@example.com';
-- What actually exists in Pwdly
SELECT
user_id,
email,
plan_tier,
created_at,
last_login_at,
project_count,
credential_count,
encrypted_blob_bytes
FROM accounts
WHERE email = 'customer@example.com';
-- No project names. No credential metadata. No impersonation.
-- The data simply does not exist on the server.

